Bug 1311180 Switch mozAddonManager to frame message managers r?rhelmer draft
authorAndrew Swan <aswan@mozilla.com>
Tue, 11 Oct 2016 16:40:24 -0700
changeset 427644 261842145a3c959541a92af58603463e723455b4
parent 426483 01ab78dd98805e150b0311cce2351d5b408f3001
child 534520 c997c451826461b3a1676cb31d3753ab07733a04
push id33075
push useraswan@mozilla.com
push dateThu, 20 Oct 2016 17:32:05 +0000
reviewersrhelmer
bugs1311180
milestone52.0a1
Bug 1311180 Switch mozAddonManager to frame message managers r?rhelmer MozReview-Commit-ID: GbX0VRn4HUF
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/addonManager.js
toolkit/mozapps/extensions/amWebAPI.js
toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -2880,17 +2880,17 @@ var AddonManagerInternal = {
     // helper to copy (and convert) the properties we care about
     copyProps(install, obj) {
       obj.state = AddonManager.stateToString(install.state);
       obj.error = AddonManager.errorToString(install.error);
       obj.progress = install.progress;
       obj.maxProgress = install.maxProgress;
     },
 
-    makeListener(id, target) {
+    makeListener(id, mm) {
       const events = [
         "onDownloadStarted",
         "onDownloadProgress",
         "onDownloadEnded",
         "onDownloadCancelled",
         "onDownloadFailed",
         "onInstallStarted",
         "onInstallEnded",
@@ -2898,17 +2898,17 @@ var AddonManagerInternal = {
         "onInstallFailed",
       ];
 
       let listener = {};
       events.forEach(event => {
         listener[event] = (install) => {
           let data = {event, id};
           AddonManager.webAPI.copyProps(install, data);
-          this.sendEvent(target, data);
+          this.sendEvent(mm, data);
         }
       });
       return listener;
     },
 
     forgetInstall(id) {
       let info = this.installs.get(id);
       if (!info) {
@@ -2939,17 +2939,17 @@ var AddonManagerInternal = {
           checkInstallUrl(options.url);
         } catch (err) {
           reject({message: err.message});
           return;
         }
 
         let newInstall = install => {
           let id = this.nextInstall++;
-          let listener = this.makeListener(id, target);
+          let listener = this.makeListener(id, target.messageManager);
           install.addListener(listener);
 
           this.installs.set(id, {install, target, listener});
 
           let result = {id};
           this.copyProps(install, result);
           resolve(result);
         };
@@ -3006,17 +3006,17 @@ var AddonManagerInternal = {
     clearInstalls(ids) {
       for (let id of ids) {
         this.forgetInstall(id);
       }
     },
 
     clearInstallsFrom(mm) {
       for (let [id, info] of this.installs) {
-        if (info.target == mm) {
+        if (info.target.messageManager == mm) {
           this.forgetInstall(id);
         }
       }
     },
   },
 };
 
 /**
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -33,32 +33,26 @@ const MSG_ADDON_EVENT      = "WebAPIAddo
 
 const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 var gSingleton = null;
 
-var gParentMM = null;
-
-
 function amManager() {
   Cu.import("resource://gre/modules/AddonManager.jsm");
   /*globals AddonManagerPrivate*/
 
-  let globalMM = Services.mm;
-  globalMM.loadFrameScript(CHILD_SCRIPT, true);
-  globalMM.addMessageListener(MSG_INSTALL_ENABLED, this);
-  globalMM.addMessageListener(MSG_INSTALL_ADDONS, this);
-
-  gParentMM = Services.ppmm;
-  gParentMM.addMessageListener(MSG_PROMISE_REQUEST, this);
-  gParentMM.addMessageListener(MSG_INSTALL_CLEANUP, this);
-  gParentMM.addMessageListener(MSG_ADDON_EVENT_REQ, this);
+  Services.mm.loadFrameScript(CHILD_SCRIPT, true);
+  Services.mm.addMessageListener(MSG_INSTALL_ENABLED, this);
+  Services.mm.addMessageListener(MSG_INSTALL_ADDONS, this);
+  Services.mm.addMessageListener(MSG_PROMISE_REQUEST, this);
+  Services.mm.addMessageListener(MSG_INSTALL_CLEANUP, this);
+  Services.mm.addMessageListener(MSG_ADDON_EVENT_REQ, this);
 
   Services.obs.addObserver(this, "message-manager-close", false);
   Services.obs.addObserver(this, "message-manager-disconnect", false);
 
   AddonManager.webAPI.setEventHandler(this.sendEvent);
 
   // Needed so receiveMessage can be called directly by JS callers
   this.wrappedJSObject = this;
@@ -225,24 +219,25 @@ amManager.prototype = {
         }
 
         return this.installAddonsFromWebpage(payload.mimetype,
           aMessage.target, payload.principalToInherit, payload.uris,
           payload.hashes, payload.names, payload.icons, callback);
       }
 
       case MSG_PROMISE_REQUEST: {
+        let mm = aMessage.target.messageManager;
         let resolve = (value) => {
-          aMessage.target.sendAsyncMessage(MSG_PROMISE_RESULT, {
+          mm.sendAsyncMessage(MSG_PROMISE_RESULT, {
             callbackID: payload.callbackID,
             resolve: value
           });
         }
         let reject = (value) => {
-          aMessage.target.sendAsyncMessage(MSG_PROMISE_RESULT, {
+          mm.sendAsyncMessage(MSG_PROMISE_RESULT, {
             callbackID: payload.callbackID,
             reject: value
           });
         }
 
         let API = AddonManager.webAPI;
         if (payload.type in API) {
           API[payload.type](aMessage.target, ...payload.args).then(resolve, reject);
@@ -254,34 +249,34 @@ amManager.prototype = {
       }
 
       case MSG_INSTALL_CLEANUP: {
         AddonManager.webAPI.clearInstalls(payload.ids);
         break;
       }
 
       case MSG_ADDON_EVENT_REQ: {
-        let target = aMessage.target;
+        let target = aMessage.target.messageManager;
         if (payload.enabled) {
           this._addAddonListener(target);
         } else {
           this._removeAddonListener(target);
         }
       }
     }
     return undefined;
   },
 
   childClosed(target) {
     AddonManager.webAPI.clearInstallsFrom(target);
     this._removeAddonListener(target);
   },
 
-  sendEvent(target, data) {
-    target.sendAsyncMessage(MSG_INSTALL_EVENT, data);
+  sendEvent(mm, data) {
+    mm.sendAsyncMessage(MSG_INSTALL_EVENT, data);
   },
 
   classID: Components.ID("{4399533d-08d1-458c-a87a-235f74451cfa}"),
   _xpcom_factory: {
     createInstance: function(aOuter, aIid) {
       if (aOuter != null)
         throw Components.Exception("Component does not support aggregation",
                                    Cr.NS_ERROR_NO_AGGREGATION);
--- a/toolkit/mozapps/extensions/amWebAPI.js
+++ b/toolkit/mozapps/extensions/amWebAPI.js
@@ -1,41 +1,41 @@
 /* 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;
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = 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,
+class APIBroker {
+  constructor(mm) {
+    this.mm = mm;
 
-  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.mm.addMessageListener(MSG_PROMISE_RESULT, this);
+    this.mm.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;
@@ -59,197 +59,211 @@ const APIBroker = {
       }
 
       case MSG_ADDON_EVENT: {
         if (this._eventListener) {
           this._eventListener(payload);
         }
       }
     }
-  },
+  }
 
-  sendRequest: function(type, ...args) {
+  sendRequest(type, ...args) {
     return new Promise(resolve => {
-      let callbackID = this._nextID++;
+      let callbackID = APIBroker._nextID++;
 
       this._promises.set(callbackID, resolve);
-      Services.cpmm.sendAsyncMessage(MSG_PROMISE_REQUEST, { type, callbackID, args });
+      this.mm.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});
+      this.mm.addMessageListener(MSG_ADDON_EVENT, this);
+      this.mm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: true});
     } else {
-      Services.cpmm.removeMessageListener(MSG_ADDON_EVENT, this);
-      Services.cpmm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: false});
+      this.mm.removeMessageListener(MSG_ADDON_EVENT, this);
+      this.mm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: false});
     }
-  },
+  }
 
-  sendCleanup: function(ids) {
+  sendCleanup(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];
+    this.mm.sendAsyncMessage(MSG_INSTALL_CLEANUP, { ids });
   }
 }
 
-function AddonInstall(window, properties) {
-  let id = properties.id;
-  APIBroker._installMap.set(id, this);
+APIBroker._nextID = 0;
 
-  this.window = window;
-  this.handlers = new Map();
+// Base class for building classes to back content-exposed interfaces.
+class APIObject {
+  init(window, broker, properties) {
+    this.window = window;
+    this.broker = broker;
 
-  for (let key of Object.keys(properties)) {
-    this[key] = properties[key];
+    // Copy any provided properties onto this object, webidl bindings
+    // will only expose to content what should be exposed.
+    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) {
+  /**
+   * Helper to implement an asychronous method visible to content, where
+   * the method is implemented by sending a message to the parent process
+   * and then wrapping the returned object or error in an appropriate object.
+   * This helper method ensures that:
+   *  - Returned Promise objects are from the content window
+   *  - Rejected Promises have Error objects from the content window
+   *  - Only non-internal errors are exposed to the caller
+   *
+   * @param {string} apiRequest The command to invoke in the parent process.
+   * @param {array<cloneable>} apiArgs The arguments to include with the
+   *                                   request to the parent process.
+   * @param {function} resultConvert If provided, a function called with the
+   *                                 result from the parent process as an
+   *                                 argument.  Used to convert the result
+   *                                 into something appropriate for content.
+   * @returns {Promise<any>} A Promise suitable for passing directly to content.
+   */
+  _apiTask(apiRequest, apiArgs, resultConverter) {
     let win = this.window;
-    let boundApiArgs = apiArgs.bind(this);
-    let boundProcessor = processor ? processor.bind(this) : null;
-
+    let broker = this.broker;
     return new win.Promise((resolve, reject) => {
-      Task.spawn(function* () {
-        let sendArgs = boundApiArgs(...args);
-        let result = yield APIBroker.sendRequest(apiRequest, ...sendArgs);
+      Task.spawn(function*() {
+        let result = yield broker.sendRequest(apiRequest, ...apiArgs);
         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);
+        if (resultConverter) {
+          obj = resultConverter(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]; }),
-};
+class Addon extends APIObject {
+  constructor(...args) {
+    super();
+    this.init(...args);
+  }
+
+  uninstall() {
+    return this._apiTask("addonUninstall", [this.id]);
+  }
 
-const INSTALL_EVENTS = [
-  "onDownloadStarted",
-  "onDownloadProgress",
-  "onDownloadEnded",
-  "onDownloadCancelled",
-  "onDownloadFailed",
-  "onInstallStarted",
-  "onInstallEnded",
-  "onInstallCancelled",
-  "onInstallFailed",
-];
+  setEnabled(value) {
+    return this._apiTask("addonSetEnabled", [this.id, value]);
+  }
+}
 
-AddonInstall.prototype = {
+class AddonInstall extends APIObject {
+  constructor(window, broker, properties) {
+    super();
+    this.init(window, broker, properties);
+
+    broker._installMap.set(properties.id, this);
+  }
+
   _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]; }),
-};
+  install() {
+    return this._apiTask("addonInstallDoInstall", [this.id]);
+  }
 
-function WebAPI() {
+  cancel() {
+    return this._apiTask("addonInstallCancel", [this.id]);
+  }
 }
 
-WebAPI.prototype = {
-  init(window) {
-    this.window = window;
+class WebAPI extends APIObject {
+  constructor() {
+    super();
     this.allInstalls = [];
     this.listenerCount = 0;
+  }
+
+  init(window) {
+    let mm = window
+        .QueryInterface(Ci.nsIInterfaceRequestor)
+        .getInterface(Ci.nsIDocShell)
+        .QueryInterface(Ci.nsIInterfaceRequestor)
+        .getInterface(Ci.nsIContentFrameMessageManager);
+    let broker = new APIBroker(mm);
+
+    super.init(window, broker, {});
 
     window.addEventListener("unload", event => {
-      APIBroker.sendCleanup(this.allInstalls);
+      this.broker.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);
-  }),
+  getAddonByID(id) {
+    return this._apiTask("getAddonByID", [id], addonInfo => {
+      if (!addonInfo) {
+        return null;
+      }
+      let addon = new Addon(this.window, this.broker, 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);
-  }),
+  createInstall(options) {
+    return this._apiTask("createInstall", [options], installInfo => {
+      if (!installInfo) {
+        return null;
+      }
+      let install = new AddonInstall(this.window, this.broker, installInfo);
+      this.allInstalls.push(installInfo.id);
+      return this.window.AddonInstall._create(this.window, install);
+    });
+  }
 
   eventListenerWasAdded(type) {
     if (this.listenerCount == 0) {
-      APIBroker.setAddonListener(data => {
+      this.broker.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);
+      this.broker.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])
-};
+  QueryInterface(iid) {
+    if (iid.equals(WebAPI.classID) || iid.equals(Ci.nsISupports)
+        || iid.equals(Ci.nsIDOMGlobalPropertyInitializer)) {
+      return this;
+    }
+    return Cr.NS_ERROR_NO_INTERFACE;
+  }
+}
 
+WebAPI.prototype.classID = Components.ID("{8866d8e3-4ea5-48b7-a891-13ba0ac15235}");
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebAPI]);
--- a/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
@@ -7,23 +7,23 @@ const ID = "webapi_install@tests.mozilla
 const XPI_LEN = 4782;
 
 function waitForClear() {
   const MSG = "WebAPICleanup";
   return new Promise(resolve => {
     let listener = {
       receiveMessage: function(msg) {
         if (msg.name == MSG) {
-          Services.ppmm.removeMessageListener(MSG, listener);
+          Services.mm.removeMessageListener(MSG, listener);
           resolve();
         }
       }
     };
 
-    Services.ppmm.addMessageListener(MSG, listener);
+    Services.mm.addMessageListener(MSG, listener, true);
   });
 }
 
 add_task(function* setup() {
   yield SpecialPowers.pushPrefEnv({
     set: [["extensions.webapi.testing", true],
           ["extensions.install.requireBuiltInCerts", false]],
   });
--- a/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
+++ b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
@@ -1,11 +1,14 @@
 <!DOCTYPE html>
 
 <html>
+<head>
+  <meta charset="utf-8">
+</head>
 <body>
 <p id="result"></p>
 <script type="text/javascript">
 let events = [];
 let resultEl = document.getElementById("result");
 [ "onEnabling",
   "onEnabled",
   "onDisabling",