Bug 1867190 - Add prefs for PHC probablities r=glandium
[gecko.git] / toolkit / mozapps / extensions / amWebAPI.sys.mjs
blobabe838af8915f7361ec7ddabf063625fb2cb2096
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 const lazy = {};
9 XPCOMUtils.defineLazyPreferenceGetter(
10   lazy,
11   "AMO_ABUSEREPORT",
12   "extensions.abuseReport.amWebAPI.enabled",
13   false
16 const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest";
17 const MSG_PROMISE_RESULT = "WebAPIPromiseResult";
18 const MSG_INSTALL_EVENT = "WebAPIInstallEvent";
19 const MSG_INSTALL_CLEANUP = "WebAPICleanup";
20 const MSG_ADDON_EVENT_REQ = "WebAPIAddonEventRequest";
21 const MSG_ADDON_EVENT = "WebAPIAddonEvent";
23 class APIBroker {
24   constructor(mm) {
25     this.mm = mm;
27     this._promises = new Map();
29     // _installMap maps integer ids to DOM AddonInstall instances
30     this._installMap = new Map();
32     this.mm.addMessageListener(MSG_PROMISE_RESULT, this);
33     this.mm.addMessageListener(MSG_INSTALL_EVENT, this);
35     this._eventListener = null;
36   }
38   receiveMessage(message) {
39     let payload = message.data;
41     switch (message.name) {
42       case MSG_PROMISE_RESULT: {
43         if (!this._promises.has(payload.callbackID)) {
44           return;
45         }
47         let resolve = this._promises.get(payload.callbackID);
48         this._promises.delete(payload.callbackID);
49         resolve(payload);
50         break;
51       }
53       case MSG_INSTALL_EVENT: {
54         let install = this._installMap.get(payload.id);
55         if (!install) {
56           let err = new Error(
57             `Got install event for unknown install ${payload.id}`
58           );
59           Cu.reportError(err);
60           return;
61         }
62         install._dispatch(payload);
63         break;
64       }
66       case MSG_ADDON_EVENT: {
67         if (this._eventListener) {
68           this._eventListener(payload);
69         }
70       }
71     }
72   }
74   sendRequest(type, ...args) {
75     return new Promise(resolve => {
76       let callbackID = APIBroker._nextID++;
78       this._promises.set(callbackID, resolve);
79       this.mm.sendAsyncMessage(MSG_PROMISE_REQUEST, { type, callbackID, args });
80     });
81   }
83   setAddonListener(callback) {
84     this._eventListener = callback;
85     if (callback) {
86       this.mm.addMessageListener(MSG_ADDON_EVENT, this);
87       this.mm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, { enabled: true });
88     } else {
89       this.mm.removeMessageListener(MSG_ADDON_EVENT, this);
90       this.mm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, { enabled: false });
91     }
92   }
94   sendCleanup(ids) {
95     this.setAddonListener(null);
96     this.mm.sendAsyncMessage(MSG_INSTALL_CLEANUP, { ids });
97   }
100 APIBroker._nextID = 0;
102 // Base class for building classes to back content-exposed interfaces.
103 class APIObject {
104   init(window, broker, properties) {
105     this.window = window;
106     this.broker = broker;
108     // Copy any provided properties onto this object, webidl bindings
109     // will only expose to content what should be exposed.
110     for (let key of Object.keys(properties)) {
111       this[key] = properties[key];
112     }
113   }
115   /**
116    * Helper to implement an asychronous method visible to content, where
117    * the method is implemented by sending a message to the parent process
118    * and then wrapping the returned object or error in an appropriate object.
119    * This helper method ensures that:
120    *  - Returned Promise objects are from the content window
121    *  - Rejected Promises have Error objects from the content window
122    *  - Only non-internal errors are exposed to the caller
123    *
124    * @param {string} apiRequest The command to invoke in the parent process.
125    * @param {array<cloneable>} apiArgs The arguments to include with the
126    *                                   request to the parent process.
127    * @param {function} resultConvert If provided, a function called with the
128    *                                 result from the parent process as an
129    *                                 argument.  Used to convert the result
130    *                                 into something appropriate for content.
131    * @returns {Promise<any>} A Promise suitable for passing directly to content.
132    */
133   _apiTask(apiRequest, apiArgs, resultConverter) {
134     let win = this.window;
135     let broker = this.broker;
136     return new win.Promise((resolve, reject) => {
137       (async function () {
138         let result = await broker.sendRequest(apiRequest, ...apiArgs);
139         if ("reject" in result) {
140           let err = new win.Error(result.reject.message);
141           // We don't currently put any other properties onto Errors
142           // generated by mozAddonManager.  If/when we do, they will
143           // need to get copied here.
144           reject(err);
145           return;
146         }
148         let obj = result.resolve;
149         if (resultConverter) {
150           obj = resultConverter(obj);
151         }
152         resolve(obj);
153       })().catch(err => {
154         Cu.reportError(err);
155         reject(new win.Error("Unexpected internal error"));
156       });
157     });
158   }
161 class Addon extends APIObject {
162   constructor(...args) {
163     super();
164     this.init(...args);
165   }
167   uninstall() {
168     return this._apiTask("addonUninstall", [this.id]);
169   }
171   setEnabled(value) {
172     return this._apiTask("addonSetEnabled", [this.id, value]);
173   }
176 class AddonInstall extends APIObject {
177   constructor(window, broker, properties) {
178     super();
179     this.init(window, broker, properties);
181     broker._installMap.set(properties.id, this);
182   }
184   _dispatch(data) {
185     // The message for the event includes updated copies of all install
186     // properties.  Use the usual "let webidl filter visible properties" trick.
187     for (let key of Object.keys(data)) {
188       this[key] = data[key];
189     }
191     let event = new this.window.Event(data.event);
192     this.__DOM_IMPL__.dispatchEvent(event);
193   }
195   install() {
196     return this._apiTask("addonInstallDoInstall", [this.id]);
197   }
199   cancel() {
200     return this._apiTask("addonInstallCancel", [this.id]);
201   }
204 export class WebAPI extends APIObject {
205   constructor() {
206     super();
207     this.allInstalls = [];
208     this.listenerCount = 0;
209   }
211   init(window) {
212     let mm = window.docShell.messageManager;
213     let broker = new APIBroker(mm);
215     super.init(window, broker, {});
217     window.addEventListener("unload", event => {
218       this.broker.sendCleanup(this.allInstalls);
219     });
220   }
222   getAddonByID(id) {
223     return this._apiTask("getAddonByID", [id], addonInfo => {
224       if (!addonInfo) {
225         return null;
226       }
227       let addon = new Addon(this.window, this.broker, addonInfo);
228       return this.window.Addon._create(this.window, addon);
229     });
230   }
232   createInstall(options) {
233     if (!Services.prefs.getBoolPref("xpinstall.enabled", true)) {
234       throw new this.window.Error("Software installation is disabled.");
235     }
237     const triggeringPrincipal = this.window.document.nodePrincipal;
239     let installOptions = {
240       ...options,
241       triggeringPrincipal,
242       // Provide the host from which the amWebAPI is being called
243       // (so that we can detect if the API is being used from the disco pane,
244       // AMO, testpilot or another unknown webpage).
245       sourceHost: this.window.location?.host,
246       sourceURL: this.window.location?.href,
247     };
248     return this._apiTask("createInstall", [installOptions], installInfo => {
249       if (!installInfo) {
250         return null;
251       }
252       let install = new AddonInstall(this.window, this.broker, installInfo);
253       this.allInstalls.push(installInfo.id);
254       return this.window.AddonInstall._create(this.window, install);
255     });
256   }
258   reportAbuse(id) {
259     return this._apiTask("addonReportAbuse", [id]);
260   }
262   get abuseReportPanelEnabled() {
263     return lazy.AMO_ABUSEREPORT;
264   }
266   eventListenerAdded(type) {
267     if (this.listenerCount == 0) {
268       this.broker.setAddonListener(data => {
269         let event = new this.window.AddonEvent(data.event, data);
270         this.__DOM_IMPL__.dispatchEvent(event);
271       });
272     }
273     this.listenerCount++;
274   }
276   eventListenerRemoved(type) {
277     this.listenerCount--;
278     if (this.listenerCount == 0) {
279       this.broker.setAddonListener(null);
280     }
281   }
284 WebAPI.prototype.QueryInterface = ChromeUtils.generateQI([
285   "nsIDOMGlobalPropertyInitializer",
287 WebAPI.prototype.classID = Components.ID(
288   "{8866d8e3-4ea5-48b7-a891-13ba0ac15235}"