Bug 1287010 - Move Management logic to SchemaAPIManager draft
authorRob Wu <rob@robwu.nl>
Wed, 17 Aug 2016 17:51:21 -0700
changeset 405205 41489b4af08983c629ed82947783c7c713a2e0e3
parent 405204 51066cfef54f236f6956fc31c17ea4dce82a7fd6
child 405206 b1894098615b40d7d1d19779abceb92d8cee3622
push id27432
push userbmo:rob@robwu.nl
push dateThu, 25 Aug 2016 02:36:24 +0000
bugs1287010, 1295082
milestone51.0a1
Bug 1287010 - Move Management logic to SchemaAPIManager - Moved Management logic to ExtensionUtils (as SchemaAPIManager) so that the logic can be used by addon and content processes. - Remove the `context.extension.hasPermission(api.permission)` check in `generateAPIs` because the only user (`registeredPrivilegedAPI`) was removed before in bug 1295082. - Add new category "webextension-scripts-content", intended for registering the few scripts that must be loaded in a content process. MozReview-Commit-ID: 81nhblV8YE6
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ExtensionUtils.jsm
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -54,19 +54,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "loadExtScriptInScope",
-                                  "resource://gre/modules/ExtensionGlobalScope.jsm");
-
 XPCOMUtils.defineLazyGetter(this, "require", () => {
   let obj = {};
   Cu.import("resource://devtools/shared/Loader.jsm", obj);
   return obj.require;
 });
 
 Cu.import("resource://gre/modules/ExtensionContent.jsm");
 Cu.import("resource://gre/modules/ExtensionManagement.jsm");
@@ -84,16 +81,17 @@ let schemaURLs = new Set();
 if (!AppConstants.RELEASE_BUILD) {
   schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
 }
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   BaseContext,
   EventEmitter,
+  SchemaAPIManager,
   LocaleData,
   Messenger,
   instanceOf,
   flushJarCache,
 } = ExtensionUtils;
 
 const LOGGER_ID_BASE = "addons.webextension.";
 const UUID_MAP_PREF = "extensions.webextensions.uuids";
@@ -110,26 +108,21 @@ const COMMENT_REGEXP = new RegExp(String
     )
 
     //.*
   `.replace(/\s+/g, ""), "gm");
 
 var ExtensionContext, GlobalManager;
 
 // This object loads the ext-*.js scripts that define the extension API.
-var Management = {
-  initialized: null,
-  scopes: [],
-  schemaApis: {
-    addon_parent: [],
-    addon_child: [],
-    content_parent: [],
-    content_child: [],
-  },
-  emitter: new EventEmitter(),
+var Management = new class extends SchemaAPIManager {
+  constructor() {
+    super("main");
+    this.initialized = null;
+  }
 
   // Loads all the ext-*.js scripts currently registered.
   lazyInit() {
     if (this.initialized) {
       return this.initialized;
     }
 
     // Load order matters here. The base manifest defines types which are
@@ -141,99 +134,35 @@ var Management = {
       }
       for (let url of schemaURLs) {
         promises.push(Schemas.load(url));
       }
       return Promise.all(promises);
     });
 
     for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS)) {
-      loadExtScriptInScope(value, this);
+      this.loadScript(value);
     }
 
     this.initialized = promise;
     return this.initialized;
-  },
+  }
 
-  /**
-   * Called by an ext-*.js script to register an API.
-   *
-   * @param {string} namespace The API namespace.
-   *     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.
-   * @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) => ({
-   *       tabs: { remove },
-   *     }));
-   */
   registerSchemaAPI(namespace, envType, getAPI) {
-    this.schemaApis[envType].push({namespace, getAPI});
+    if (envType == "addon_parent" || envType == "content_parent") {
+      super.registerSchemaAPI(namespace, envType, getAPI);
+    }
     if (envType === "addon_child") {
       // TODO(robwu): Remove this. It is a temporary hack to ease the transition
       // from ext-*.js running in the parent to APIs running in a child process.
       // This can be removed once there is a dedicated ExtensionContext with type
       // "addon_child".
-      this.schemaApis.addon_parent.push({namespace, getAPI});
-    }
-  },
-
-  // Mash together all the APIs from apis into obj.
-  generateAPIs(context, apis, obj) {
-    // Recursively copy properties from source to dest.
-    function copy(dest, source) {
-      for (let prop in source) {
-        let desc = Object.getOwnPropertyDescriptor(source, prop);
-        if (typeof(desc.value) == "object") {
-          if (!(prop in dest)) {
-            dest[prop] = {};
-          }
-          copy(dest[prop], source[prop]);
-        } else {
-          Object.defineProperty(dest, prop, desc);
-        }
-      }
+      super.registerSchemaAPI(namespace, "addon_parent", getAPI);
     }
-
-    for (let api of apis) {
-      if (api.permission) {
-        if (!context.extension.hasPermission(api.permission)) {
-          continue;
-        }
-      }
-
-      api = api.getAPI(context);
-      copy(obj, api);
-    }
-  },
-
-  // The ext-*.js scripts can ask to be notified for certain hooks.
-  on(hook, callback) {
-    this.emitter.on(hook, callback);
-  },
-
-  // Ask to run all the callbacks that are registered for a given hook.
-  emit(hook, ...args) {
-    return this.emitter.emit(hook, ...args);
-  },
-
-  off(hook, callback) {
-    this.emitter.off(hook, callback);
-  },
+  }
 };
 
 // An extension page is an execution context for any extension content
 // that runs in the chrome process. It's used for background pages
 // (type="background"), popups (type="popup"), and any extension
 // content loaded into browser tabs (type="tab").
 //
 // |params| is an object with the following properties:
@@ -628,18 +557,18 @@ GlobalManager = {
   getExtension(extensionId) {
     return this.extensionMap.get(extensionId);
   },
 
   injectInObject(context, defaultCallback, dest) {
     let apis = {
       extensionTypes: {},
     };
-    Management.generateAPIs(context, Management.schemaApis[context.envType], apis);
-    Management.generateAPIs(context, context.extension.apis, apis);
+    Management.generateAPIs(context, apis);
+    SchemaAPIManager.generateAPIs(context, context.extension.apis, apis);
 
     let schemaWrapper = {
       get principal() {
         return context.principal;
       },
 
       get cloneScope() {
         return context.cloneScope;
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -49,27 +49,53 @@ var {
   LocaleData,
   Messenger,
   injectAPI,
   flushJarCache,
   detectLanguage,
   getInnerWindowID,
   promiseDocumentReady,
   ChildAPIManager,
+  SchemaAPIManager,
 } = ExtensionUtils;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
 
+const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content";
+
 function isWhenBeforeOrSame(when1, when2) {
   let table = {"document_start": 0,
                "document_end": 1,
                "document_idle": 2};
   return table[when1] <= table[when2];
 }
 
+var apiManager = new class extends SchemaAPIManager {
+  constructor() {
+    super("content");
+    this.initialized = false;
+  }
+
+  generateAPIs(...args) {
+    if (!this.initialized) {
+      this.initialized = true;
+      for (let [/* name */, value] of XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT)) {
+        this.loadScript(value);
+      }
+    }
+    return super.generateAPIs(...args);
+  }
+
+  registerSchemaAPI(namespace, envType, getAPI) {
+    if (envType == "content_child") {
+      super.registerSchemaAPI(namespace, envType, getAPI);
+    }
+  }
+};
+
 // This is the fairly simple API that we inject into content
 // scripts.
 var api = context => {
   return {
     runtime: {
       connect: function(extensionId, connectInfo) {
         if (!connectInfo) {
           connectInfo = extensionId;
@@ -414,16 +440,17 @@ class ExtensionContext extends BaseConte
       type: "content_script",
       url,
       incognito,
     });
 
     Schemas.inject(this.chromeObj, this.childManager);
 
     injectAPI(api(this), this.chromeObj);
+    injectAPI(apiManager.generateAPIs(this), this.chromeObj);
 
     // This is an iframe with content script API enabled. (See Bug 1214658 for rationale)
     if (isExtensionPage) {
       Cu.waiveXrays(this.contentWindow).chrome = this.chromeObj;
       Cu.waiveXrays(this.contentWindow).browser = this.chromeObj;
     }
   }
 
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -28,16 +28,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "loadExtScriptInScope",
+                                  "resource://gre/modules/ExtensionGlobalScope.jsm");
+
 function getConsole() {
   return new ConsoleAPI({
     maxLogLevelPref: "extensions.webextensions.log.level",
     prefix: "WebExtensions",
   });
 }
 
 XPCOMUtils.defineLazyGetter(this, "console", getConsole);
@@ -1560,16 +1563,118 @@ class ChildAPIManager {
   hasListener(pathObj, path, name, listener) {
     let ref = path.concat(name).join(".");
     let set = this.listeners.get(ref) || new Set();
     return set.has(listener);
   }
 }
 
 /**
+ * This object loads the ext-*.js scripts that define the extension API.
+ *
+ * This class instance is shared with the scripts that it loads, so that the
+ * 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.
+   */
+  constructor(processType) {
+    super();
+    this.processType = processType;
+    this._schemaApis = {
+      addon_parent: [],
+      addon_child: [],
+      content_parent: [],
+      content_child: [],
+    };
+  }
+
+  loadScript(scriptUrl) {
+    loadExtScriptInScope(scriptUrl, this);
+  }
+
+  /**
+   * Called by an ext-*.js script to register an API.
+   *
+   * @param {string} namespace The API namespace.
+   *     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.
+   * @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) => ({
+   *       tabs: { remove },
+   *     }));
+   */
+  registerSchemaAPI(namespace, envType, getAPI) {
+    this._schemaApis[envType].push({namespace, getAPI});
+  }
+
+  /**
+   * Exports all registered scripts to `obj`.
+   *
+   * @param {BaseContext} context The context for which the API bindings are
+   *     generated.
+   * @param {object} obj The destination of the API.
+   */
+  generateAPIs(context, obj) {
+    let apis = this._schemaApis[context.envType];
+    if (!apis) {
+      Cu.reportError(`No APIs have been registered for ${context.envType}`);
+      return;
+    }
+    SchemaAPIManager.generateAPIs(context, apis, obj);
+  }
+
+  /**
+   * Mash together all the APIs from `apis` into `obj`.
+   *
+   * @param {BaseContext} context The context for which the API bindings are
+   *     generated.
+   * @param {Array} apis A list of objects, see `registerSchemaAPI`.
+   * @param {object} obj The destination of the API.
+   */
+  static generateAPIs(context, apis, obj) {
+    // Recursively copy properties from source to dest.
+    function copy(dest, source) {
+      for (let prop in source) {
+        let desc = Object.getOwnPropertyDescriptor(source, prop);
+        if (typeof(desc.value) == "object") {
+          if (!(prop in dest)) {
+            dest[prop] = {};
+          }
+          copy(dest[prop], source[prop]);
+        } else {
+          Object.defineProperty(dest, prop, desc);
+        }
+      }
+    }
+
+    for (let api of apis) {
+      api = api.getAPI(context);
+      copy(obj, api);
+    }
+  }
+}
+
+/**
  * Convert any of several different representations of a date/time to a Date object.
  * Accepts several formats:
  * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
  * either a number or a string.
  *
  * @param {Date|string|number} date
  *      The date to convert.
  * @returns {Date}
@@ -1605,9 +1710,10 @@ this.ExtensionUtils = {
   EventManager,
   IconDetails,
   LocaleData,
   Messenger,
   PlatformInfo,
   SingletonEventManager,
   SpreadArgs,
   ChildAPIManager,
+  SchemaAPIManager,
 };