Bug 1309906 - part1: adapt webext-oop internals for devtools contexts and APIs. draft
authorLuca Greco <lgreco@mozilla.com>
Mon, 14 Nov 2016 21:38:20 +0100
changeset 438593 2c02f0b0857404f1bc8e8ef3e1352805d78003b0
parent 438410 a516c754042c438a5c1499171ca525a980ecb911
child 438594 fb7fddacc116fce0a92d8d7e3cd30376e7a04294
push id35772
push userluca.greco@alcacoop.it
push dateMon, 14 Nov 2016 20:39:49 +0000
bugs1309906
milestone53.0a1
Bug 1309906 - part1: adapt webext-oop internals for devtools contexts and APIs. MozReview-Commit-ID: E0gxV271N31
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionParent.jsm
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -27,16 +27,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "NativeApp",
                                   "resource://gre/modules/NativeMessaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 
 const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon";
+const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools";
 
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 const {
   DefaultMap,
   EventManager,
   SingletonEventManager,
@@ -458,16 +459,39 @@ var apiManager = new class extends Schem
 
   registerSchemaAPI(namespace, envType, getAPI) {
     if (envType == "addon_child") {
       super.registerSchemaAPI(namespace, envType, getAPI);
     }
   }
 }();
 
+var devtoolsAPIManager = new class extends SchemaAPIManager {
+  constructor() {
+    super("devtools");
+    this.initialized = false;
+  }
+
+  generateAPIs(...args) {
+    if (!this.initialized) {
+      this.initialized = true;
+      for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS)) {
+        this.loadScript(value);
+      }
+    }
+    return super.generateAPIs(...args);
+  }
+
+  registerSchemaAPI(namespace, envType, getAPI) {
+    if (envType == "devtools_child") {
+      super.registerSchemaAPI(namespace, envType, getAPI);
+    }
+  }
+}();
+
 /**
  * An object that runs an remote implementation of an API.
  */
 class ProxyAPIImplementation extends SchemaAPIInterface {
   /**
    * @param {string} namespace The full path to the namespace that contains the
    *     `name` member. This may contain dots, e.g. "storage.local".
    * @param {string} name The name of the method or property.
@@ -681,16 +705,29 @@ class ChildAPIManager {
     // Do not generate content script APIs, unless explicitly allowed.
     if (this.context.envType === "content_child" &&
         !allowedContexts.includes("content")) {
       return false;
     }
     if (allowedContexts.includes("addon_parent_only")) {
       return false;
     }
+
+    // Do not generate devtools APIs, unless explicitly allowed.
+    if (this.context.envType === "devtools_child" &&
+        !allowedContexts.includes("devtools")) {
+      return false;
+    }
+
+    // Do not generate devtools APIs, unless explicitly allowed.
+    if (this.context.envType !== "devtools_child" &&
+        allowedContexts.includes("devtools_only")) {
+      return false;
+    }
+
     return true;
   }
 
   getImplementation(namespace, name) {
     let obj = namespace.split(".").reduce(
       (object, prop) => object && object[prop],
       this.localApis);
 
@@ -706,42 +743,42 @@ class ChildAPIManager {
     return new ProxyAPIImplementation(namespace, name, this);
   }
 
   hasPermission(permission) {
     return this.context.extension.hasPermission(permission);
   }
 }
 
-class ExtensionPageContextChild extends BaseContext {
+class ExtensionBaseContextChild extends BaseContext {
   /**
-   * This ExtensionPageContextChild represents a privileged addon
-   * execution environment that has full access to the WebExtensions
-   * APIs (provided that the correct permissions have been requested).
-   *
-   * This is the child side of the ExtensionPageContextParent class
-   * defined in ExtensionParent.jsm.
+   * This ExtensionBaseContextChild represents an addon execution environment
+   * that is running in an addon or devtools child process.
    *
    * @param {BrowserExtensionContent} extension This context's owner.
    * @param {object} params
+   * @param {string} params.envType One of "addon_child" or "devtools_child".
    * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
-   * @param {string} params.viewType One of "background", "popup" or "tab".
-   *     "background" and "tab" are used by `browser.extension.getViews`.
-   *     "popup" is only used internally to identify page action and browser
-   *     action popups and options_ui pages.
+   * @param {string} params.viewType One of "background", "popup", "tab",
+   *   "devtools_page" or "devtools_panel".
    * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
    */
   constructor(extension, params) {
-    super("addon_child", extension);
+    if (!params.envType) {
+      throw new Error("Missing envType");
+    }
+
     if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
       // This check is temporary. It should be removed once the proxy creation
       // is asynchronous.
-      throw new Error("ExtensionPageContextChild cannot be created in child processes");
+      throw new Error("ExtensionContext cannot be created in child processes");
     }
 
+    super(params.envType, extension);
+
     let {viewType, uri, contentWindow, tabId} = params;
     this.viewType = viewType;
     this.uri = uri || extension.baseURI;
 
     this.setContentWindow(contentWindow);
 
     // This is the MessageSender property passed to extension.
     // It can be augmented by the "page-open" hook.
@@ -764,18 +801,16 @@ class ExtensionPageContextChild extends 
     Schemas.exportLazyGetter(contentWindow, "chrome", () => {
       let chromeApiWrapper = Object.create(this.childManager);
       chromeApiWrapper.isChromeCompat = true;
 
       let chromeObj = Cu.createObjectIn(contentWindow);
       Schemas.inject(chromeObj, chromeApiWrapper);
       return chromeObj;
     });
-
-    this.extension.views.add(this);
   }
 
   get cloneScope() {
     return this.contentWindow;
   }
 
   get principal() {
     return this.contentWindow.document.nodePrincipal;
@@ -803,41 +838,113 @@ class ExtensionPageContextChild extends 
       return;
     }
 
     if (this.contentWindow) {
       this.contentWindow.close();
     }
 
     super.unload();
-    this.extension.views.delete(this);
   }
 }
 
-defineLazyGetter(ExtensionPageContextChild.prototype, "messenger", function() {
+defineLazyGetter(ExtensionBaseContextChild.prototype, "messenger", function() {
   let filter = {extensionId: this.extension.id};
   let optionalFilter = {};
   // Addon-generated messages (not necessarily from the same process as the
   // addon itself) are sent to the main process, which forwards them via the
   // parent process message manager. Specific replies can be sent to the frame
   // message manager.
   return new Messenger(this, [Services.cpmm, this.messageManager], this.sender,
                        filter, optionalFilter);
 });
 
+class ExtensionPageContextChild extends ExtensionBaseContextChild {
+  /**
+   * This ExtensionPageContextChild represents a privileged addon
+   * execution environment that has full access to the WebExtensions
+   * APIs (provided that the correct permissions have been requested).
+   *
+   * This is the child side of the ExtensionPageContextParent class
+   * defined in ExtensionParent.jsm.
+   *
+   * @param {BrowserExtensionContent} extension This context's owner.
+   * @param {object} params
+   * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
+   * @param {string} params.viewType One of "background", "popup" or "tab".
+   *     "background" and "tab" are used by `browser.extension.getViews`.
+   *     "popup" is only used internally to identify page action and browser
+   *     action popups and options_ui pages.
+   * @param {number} [params.tabId] This tab's ID, used if viewType is "tab".
+   */
+  constructor(extension, params) {
+    super(extension, Object.assign(params, {envType: "addon_child"}));
+
+    this.extension.views.add(this);
+  }
+
+  unload() {
+    super.unload();
+    this.extension.views.delete(this);
+  }
+}
+
 defineLazyGetter(ExtensionPageContextChild.prototype, "childManager", function() {
   let localApis = {};
   apiManager.generateAPIs(this, localApis);
 
+  let childManager = new ChildAPIManager(this, this.messageManager, localApis, {
+    envType: "addon_parent",
+    viewType: this.viewType,
+    url: this.uri.spec,
+    incognito: this.incognito,
+  });
+
+  this.callOnClose(childManager);
+
   if (this.viewType == "background") {
     apiManager.global.initializeBackgroundPage(this.contentWindow);
   }
 
+  return childManager;
+});
+
+class DevtoolsContextChild extends ExtensionBaseContextChild {
+  /**
+   * This DevtoolsContextChild represents a devtools-related addon execution
+   * environment that has access to the devtools API namespace and to the same subset
+   * of APIs available in a content script execution environment.
+   *
+   * @param {BrowserExtensionContent} extension This context's owner.
+   * @param {object} params
+   * @param {nsIDOMWindow} params.contentWindow The window where the addon runs.
+   * @param {string} params.viewType One of "devtools_page" or "devtools_panel".
+   * @param {object} [params.devtoolsToolboxInfo] This devtools toolbox's information,
+   *   used if viewType is "devtools_page" or "devtools_panel".
+   */
+  constructor(extension, params) {
+    super(extension, Object.assign(params, {envType: "devtools_child"}));
+
+    this.devtoolsToolboxInfo = params.devtoolsToolboxInfo;
+
+    this.extension.devtoolsViews.add(this);
+  }
+
+  unload() {
+    super.unload();
+    this.extension.devtoolsViews.delete(this);
+  }
+}
+
+defineLazyGetter(DevtoolsContextChild.prototype, "childManager", function() {
+  let localApis = {};
+  devtoolsAPIManager.generateAPIs(this, localApis);
+
   let childManager = new ChildAPIManager(this, this.messageManager, localApis, {
-    envType: "addon_parent",
+    envType: "devtools_parent",
     viewType: this.viewType,
     url: this.uri.spec,
     incognito: this.incognito,
   });
 
   this.callOnClose(childManager);
 
   return childManager;
@@ -886,16 +993,21 @@ class ContentGlobal {
 
   receiveMessage({name, data}) {
     switch (name) {
       case "Extension:InitExtensionView":
         // The view type is initialized once and then fixed.
         this.global.removeMessageListener("Extension:InitExtensionView", this);
         let {viewType, url} = data;
         this.viewType = viewType;
+
+        if (data.devtoolsToolboxInfo) {
+          this.devtoolsToolboxInfo = data.devtoolsToolboxInfo;
+        }
+
         this.global.addEventListener("DOMContentLoaded", this);
         if (url) {
           // TODO(robwu): Remove this check. It is only here because the popup
           // implementation does not always load a URL at the initialization,
           // and the logic is too complex to fix at once.
           let {document} = this.global.content;
           this.initialDocuments.add(document);
           document.location.replace(url);
@@ -986,21 +1098,31 @@ ExtensionChild = {
       return;
     }
 
     let mm = contentWindow
       .QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsIDocShell)
       .QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsIContentFrameMessageManager);
-    let {viewType, tabId} = this.contentGlobals.get(mm).ensureInitialized();
+    let {
+      viewType, tabId,
+      devtoolsToolboxInfo,
+    } = this.contentGlobals.get(mm).ensureInitialized();
 
     let uri = contentWindow.document.documentURIObject;
 
-    context = new ExtensionPageContextChild(extension, {viewType, contentWindow, uri, tabId});
+    if (devtoolsToolboxInfo) {
+      context = new DevtoolsContextChild(extension, {
+        viewType, contentWindow, uri, tabId, devtoolsToolboxInfo,
+      });
+    } else {
+      context = new ExtensionPageContextChild(extension, {viewType, contentWindow, uri, tabId});
+    }
+
     this.extensionContexts.set(windowId, context);
   },
 
   /**
    * Close the ExtensionPageContextChild belonging to the given window, if any.
    *
    * @param {number} windowId The inner window ID of the destroyed context.
    */
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -537,27 +537,30 @@ class LocalAPIImplementation extends Sch
  * ext-*.js scripts and the instantiator can communicate with each other.
  */
 class SchemaAPIManager extends EventEmitter {
   /**
    * @param {string} processType
    *     "main" - The main, one and only chrome browser process.
    *     "addon" - An addon process.
    *     "content" - A content process.
+   *     "devtools" - A devtools process.
    */
   constructor(processType) {
     super();
     this.processType = processType;
     this.global = this._createExtGlobal();
     this._scriptScopes = [];
     this._schemaApis = {
       addon_parent: [],
       addon_child: [],
       content_parent: [],
       content_child: [],
+      devtools_parent: [],
+      devtools_child: [],
     };
   }
 
   /**
    * Create a global object that is used as the shared global for all ext-*.js
    * scripts that are loaded via `loadScript`.
    *
    * @returns {object} A sandbox that is used as the global by `loadScript`.
@@ -603,16 +606,18 @@ class SchemaAPIManager extends EventEmit
    *     Intended to match the namespace of the generated API, but not used at
    *     the moment - see bugzil.la/1295774.
    * @param {string} envType Restricts the API to contexts that run in the
    *    given environment. Must be one of the following:
    *     - "addon_parent" - addon APIs that runs in the main process.
    *     - "addon_child" - addon APIs that runs in an addon process.
    *     - "content_parent" - content script APIs that runs in the main process.
    *     - "content_child" - content script APIs that runs in a content process.
+   *     - "devtools_parent" - devtools APIs that runs in the main process.
+   *     - "devtools_child" - devtools APIs that runs in a devtools process.
    * @param {function(BaseContext)} getAPI A function that returns an object
    *     that will be merged with |chrome| and |browser|. The next example adds
    *     the create, update and remove methods to the tabs API.
    *
    *     registerSchemaAPI("tabs", "addon_parent", (context) => ({
    *       tabs: { create, update },
    *     }));
    *     registerSchemaAPI("tabs", "addon_parent", (context) => ({
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -785,16 +785,19 @@ class BrowserExtensionContent extends Ev
     this.localeData = new LocaleData(data.localeData);
 
     this.manifest = data.manifest;
     this.baseURI = Services.io.newURI(data.baseURL, null, null);
 
     // Only used in addon processes.
     this.views = new Set();
 
+    // Only used for devtools views.
+    this.devtoolsViews = new Set();
+
     let uri = Services.io.newURI(data.resourceURL, null, null);
 
     if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
       // Extension.jsm takes care of this in the parent.
       ExtensionManagement.startupExtension(this.uuid, uri, this);
     }
   }
 
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -90,17 +90,18 @@ let apiManager = new class extends Schem
       this.loadScript(value);
     }
 
     this.initialized = promise;
     return this.initialized;
   }
 
   registerSchemaAPI(namespace, envType, getAPI) {
-    if (envType == "addon_parent" || envType == "content_parent") {
+    if (envType == "addon_parent" || envType == "content_parent" ||
+        envType == "devtools_parent") {
       super.registerSchemaAPI(namespace, envType, getAPI);
     }
   }
 }();
 
 // Subscribes to messages related to the extension messaging API and forwards it
 // to the relevant message manager. The "sender" field for the `onMessage` and
 // `onConnect` events are updated if needed.
@@ -316,16 +317,18 @@ class ContentScriptContextParent extends
  * background script, a tab page, or a popup, running in
  * ExtensionChild.jsm.
  */
 class ExtensionPageContextParent extends ProxyContextParent {
   constructor(envType, extension, params, xulBrowser) {
     super(envType, extension, params, xulBrowser, extension.principal);
 
     this.viewType = params.viewType;
+
+    extension.emit("extension-proxy-context-load", this);
   }
 
   // The window that contains this context. This may change due to moving tabs.
   get xulWindow() {
     return this.xulBrowser.ownerGlobal;
   }
 
   get windowId() {
@@ -420,17 +423,17 @@ ParentAPIManager = {
     }
 
     let extension = GlobalManager.getExtension(extensionId);
     if (!extension) {
       throw new Error(`No WebExtension found with ID ${extensionId}`);
     }
 
     let context;
-    if (envType == "addon_parent") {
+    if (envType == "addon_parent" || envType == "devtools_parent") {
       // Privileged addon contexts can only be loaded in documents whose main
       // frame is also the same addon.
       if (principal.URI.prePath !== extension.baseURI.prePath ||
           !target.contentPrincipal.subsumes(principal)) {
         throw new Error(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
       }
       context = new ExtensionPageContextParent(envType, extension, data, target);
     } else if (envType == "content_parent") {