/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Task.jsm"); const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest"; const MSG_PROMISE_RESULT = "WebAPIPromiseResult"; const MSG_INSTALL_EVENT = "WebAPIInstallEvent"; const MSG_INSTALL_CLEANUP = "WebAPICleanup"; const MSG_ADDON_EVENT_REQ = "WebAPIAddonEventRequest"; const MSG_ADDON_EVENT = "WebAPIAddonEvent"; const APIBroker = { _nextID: 0, init() { this._promises = new Map(); // _installMap maps integer ids to DOM AddonInstall instances this._installMap = new Map(); Services.cpmm.addMessageListener(MSG_PROMISE_RESULT, this); Services.cpmm.addMessageListener(MSG_INSTALL_EVENT, this); this._eventListener = null; }, receiveMessage(message) { let payload = message.data; switch (message.name) { case MSG_PROMISE_RESULT: { if (!this._promises.has(payload.callbackID)) { return; } let resolve = this._promises.get(payload.callbackID); this._promises.delete(payload.callbackID); resolve(payload); break; } case MSG_INSTALL_EVENT: { let install = this._installMap.get(payload.id); if (!install) { let err = new Error(`Got install event for unknown install ${payload.id}`); Cu.reportError(err); return; } install._dispatch(payload); break; } case MSG_ADDON_EVENT: { if (this._eventListener) { this._eventListener(payload); } } } }, sendRequest: function(type, ...args) { return new Promise(resolve => { let callbackID = this._nextID++; this._promises.set(callbackID, resolve); Services.cpmm.sendAsyncMessage(MSG_PROMISE_REQUEST, { type, callbackID, args }); }); }, setAddonListener(callback) { this._eventListener = callback; if (callback) { Services.cpmm.addMessageListener(MSG_ADDON_EVENT, this); Services.cpmm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: true}); } else { Services.cpmm.removeMessageListener(MSG_ADDON_EVENT, this); Services.cpmm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: false}); } }, sendCleanup: function(ids) { this.setAddonListener(null); Services.cpmm.sendAsyncMessage(MSG_INSTALL_CLEANUP, { ids }); }, }; APIBroker.init(); function Addon(window, properties) { this.window = window; // We trust the webidl binding to broker access to our properties. for (let key of Object.keys(properties)) { this[key] = properties[key]; } } function AddonInstall(window, properties) { let id = properties.id; APIBroker._installMap.set(id, this); this.window = window; this.handlers = new Map(); for (let key of Object.keys(properties)) { this[key] = properties[key]; } } /** * API methods all return promises from content. They also follow a * similar pattern of sending a request to the parent process, then * wrapping the returned object or error appropriately for the page. * We must take care only to wrap and reject with errors that are meant * to be visible to content, and not internal errors. * This function is a wrapper to handle the common bits. * * apiRequest is the name of the command to invoke in the parent process * apiArgs is a callable that takes the content-provided args for the * method and returns the arguments to send in the request to * the parent process. * if processor is non-null, it is called on the returned object to * convert the result from the parent process back to an * object appropriate for content. * * Both apiArgs and processor are called with "this" bound to the value * that is held when the actual api method was called. */ function WebAPITask(apiRequest, apiArgs, processor) { return function(...args) { let win = this.window; let boundApiArgs = apiArgs.bind(this); let boundProcessor = processor ? processor.bind(this) : null; return new win.Promise((resolve, reject) => { Task.spawn(function* () { let sendArgs = boundApiArgs(...args); let result = yield APIBroker.sendRequest(apiRequest, ...sendArgs); if ("reject" in result) { let err = new win.Error(result.reject.message); // We don't currently put any other properties onto Errors // generated by mozAddonManager. If/when we do, they will // need to get copied here. reject(err); return; } let obj = result.resolve; if (boundProcessor) { obj = boundProcessor(obj); } resolve(obj); }).catch(err => { Cu.reportError(err); reject(new win.Error("Unexpected internal error")); }); }); } } Addon.prototype = { uninstall: WebAPITask("addonUninstall", function() { return [this.id]; }), setEnabled: WebAPITask("addonSetEnabled", function(value) { return [this.id, value]; }), }; const INSTALL_EVENTS = [ "onDownloadStarted", "onDownloadProgress", "onDownloadEnded", "onDownloadCancelled", "onDownloadFailed", "onInstallStarted", "onInstallEnded", "onInstallCancelled", "onInstallFailed", ]; AddonInstall.prototype = { _dispatch(data) { // The message for the event includes updated copies of all install // properties. Use the usual "let webidl filter visible properties" trick. for (let key of Object.keys(data)) { this[key] = data[key]; } let event = new this.window.Event(data.event); this.__DOM_IMPL__.dispatchEvent(event); }, install: WebAPITask("addonInstallDoInstall", function() { return [this.id]; }), cancel: WebAPITask("addonInstallCancel", function() { return [this.id]; }), }; function WebAPI() { } WebAPI.prototype = { init(window) { this.window = window; this.allInstalls = []; this.listenerCount = 0; window.addEventListener("unload", event => { APIBroker.sendCleanup(this.allInstalls); }); }, getAddonByID: WebAPITask("getAddonByID", id => [id], function(addonInfo) { if (!addonInfo) { return null; } let addon = new Addon(this.window, addonInfo); return this.window.Addon._create(this.window, addon); }), createInstall: WebAPITask("createInstall", options => [options], function(installInfo) { if (!installInfo) { return null; } let install = new AddonInstall(this.window, installInfo); this.allInstalls.push(installInfo.id); return this.window.AddonInstall._create(this.window, install); }), eventListenerWasAdded(type) { if (this.listenerCount == 0) { APIBroker.setAddonListener(data => { let event = new this.window.AddonEvent(data.event, data); this.__DOM_IMPL__.dispatchEvent(event); }); } this.listenerCount++; }, eventListenerWasRemoved(type) { this.listenerCount--; if (this.listenerCount == 0) { APIBroker.setAddonListener(null); } }, classID: Components.ID("{8866d8e3-4ea5-48b7-a891-13ba0ac15235}"), contractID: "@mozilla.org/addon-web-api/manager;1", QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer]) }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebAPI]);