Bug 1323845: Part 6a - Support WebExtension-style experiment API provider extensions. r=aswan
authorKris Maglione <maglione.k@gmail.com>
Tue, 09 Jan 2018 17:20:55 -0800
changeset 450989 17ee1f919ec1d4a98c9a6ecaab695cf2501992c7
parent 450988 bbf3129f4a56db20b1806247bc8d4bab0e8a430f
child 450990 e8cb79d5fb06904ad04000042fb23360f145c4b6
push id8543
push userryanvm@gmail.com
push dateTue, 16 Jan 2018 14:33:22 +0000
treeherdermozilla-beta@a6525ed16a32 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan
bugs1323845
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1323845: Part 6a - Support WebExtension-style experiment API provider extensions. r=aswan MozReview-Commit-ID: E1IBFyzEwqU
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/Schemas.jsm
toolkit/components/extensions/extension-process-script.js
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -699,16 +699,23 @@ class ExtensionData {
     this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions);
 
     return this.manifest;
   }
 
   getAPIManager() {
     let apiManagers = [Management];
 
+    for (let id of this.dependencies) {
+      let policy = WebExtensionPolicy.getByID(id);
+      if (policy) {
+        apiManagers.push(policy.extension.experimentAPIManager);
+      }
+    }
+
     if (this.modules) {
       this.experimentAPIManager =
         new ExtensionCommon.LazyAPIManager("main", this.modules.parent, this.schemaURLs);
 
       apiManagers.push(this.experimentAPIManager);
     }
 
     if (apiManagers.length == 1) {
@@ -1324,17 +1331,19 @@ class Extension extends ExtensionData {
     }
 
     if (this.apiNames.size) {
       // Load Experiments APIs that this extension depends on.
       let apis = await Promise.all(
         Array.from(this.apiNames, api => ExtensionCommon.ExtensionAPIs.load(api)));
 
       for (let API of apis) {
-        this.apis.push(new API(this));
+        if (API) {
+          this.apis.push(new API(this));
+        }
       }
     }
 
     return manifest;
   }
 
   // Representation of the extension to send to content
   // processes. This should include anything the content process might
@@ -1349,16 +1358,17 @@ class Extension extends ExtensionData {
       resourceURL: this.resourceURL,
       baseURL: this.baseURI.spec,
       contentScripts: this.contentScripts,
       registeredContentScripts: new Map(),
       webAccessibleResources: this.webAccessibleResources.map(res => res.glob),
       whiteListedHosts: this.whiteListedHosts.patterns.map(pat => pat.pattern),
       localeData: this.localeData.serialize(),
       childModules: this.modules && this.modules.child,
+      dependencies: this.dependencies,
       permissions: this.permissions,
       principal: this.principal,
       optionalPermissions: this.manifest.optional_permissions,
       schemaURLs: this.schemaURLs,
     };
   }
 
   get contentScripts() {
@@ -1534,16 +1544,18 @@ class Extension extends ExtensionData {
     if (!WebExtensionPolicy.getByID(this.id)) {
       // The add-on manager doesn't handle async startup and shutdown,
       // so during upgrades and add-on restarts, startup() gets called
       // before the last shutdown has completed, and this fails when
       // there's another active add-on with the same ID.
       this.policy.active = true;
     }
 
+    this.policy.extension = this;
+
     TelemetryStopwatch.start("WEBEXT_EXTENSION_STARTUP_MS", this);
     try {
       await this.loadManifest();
 
       if (!this.hasShutdown) {
         await this.initLocale();
       }
 
@@ -1554,16 +1566,17 @@ class Extension extends ExtensionData {
       if (this.hasShutdown) {
         return;
       }
 
       GlobalManager.init(this);
 
       this.policy.active = false;
       this.policy = processScript.initExtension(this);
+      this.policy.extension = this;
 
       this.updatePermissions(this.startupReason);
 
       // The "startup" Management event sent on the extension instance itself
       // is emitted just before the Management "startup" event,
       // and it is used to run code that needs to be executed before
       // any of the "startup" listeners.
       this.emit("startup", this);
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -32,16 +32,21 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyModuleGetters(this, {
   ExtensionContent: "resource://gre/modules/ExtensionContent.jsm",
   ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.jsm",
   MessageChannel: "resource://gre/modules/MessageChannel.jsm",
   NativeApp: "resource://gre/modules/NativeMessaging.jsm",
   PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
 });
 
+XPCOMUtils.defineLazyGetter(
+  this, "processScript",
+  () => Cc["@mozilla.org/webextensions/extension-process-script;1"]
+          .getService().wrappedJSObject);
+
 Cu.import("resource://gre/modules/ExtensionCommon.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 const {
   DefaultMap,
   EventEmitter,
   LimitedSet,
   defineLazyGetter,
@@ -556,16 +561,17 @@ class BrowserExtensionContent extends Ev
     super();
 
     this.data = data;
     this.id = data.id;
     this.uuid = data.uuid;
     this.instanceId = data.instanceId;
 
     this.childModules = data.childModules;
+    this.dependencies = data.dependencies;
     this.schemaURLs = data.schemaURLs;
 
     this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
     Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
 
     defineLazyGetter(this, "scripts", () => {
       return data.contentScripts.map(scriptData => new ExtensionContent.Script(this, scriptData));
     });
@@ -635,16 +641,23 @@ class BrowserExtensionContent extends Ev
     /* eslint-enable mozilla/balanced-listeners */
 
     ExtensionManager.extensions.set(this.id, this);
   }
 
   getAPIManager() {
     let apiManagers = [ExtensionPageChild.apiManager];
 
+    for (let id of this.dependencies) {
+      let extension = processScript.getExtensionChild(id);
+      if (extension) {
+        apiManagers.push(extension.experimentAPIManager);
+      }
+    }
+
     if (this.childModules) {
       this.experimentAPIManager =
         new ExtensionCommon.LazyAPIManager("addon", this.childModules, this.schemaURLs);
 
       apiManagers.push(this.experimentAPIManager);
     }
 
     if (apiManagers.length == 1) {
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -116,16 +116,19 @@ class ExtensionAPI extends ExtensionUtil
   }
 }
 
 var ExtensionAPIs = {
   apis: new Map(),
 
   load(apiName) {
     let api = this.apis.get(apiName);
+    if (!api) {
+      return null;
+    }
 
     if (api.loadPromise) {
       return api.loadPromise;
     }
 
     let {script, schema} = api;
 
     let addonId = `${apiName}@experiments.addons.mozilla.org`;
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -721,22 +721,26 @@ class InjectionEntry {
 }
 
 /**
  * Holds methods that run the actual implementation of the extension APIs. These
  * methods are only called if the extension API invocation matches the signature
  * as defined in the schema. Otherwise an error is reported to the context.
  */
 class InjectionContext extends Context {
-  constructor(params) {
+  constructor(params, schemaRoot) {
     super(params, CONTEXT_FOR_INJECTION);
 
+    this.schemaRoot = schemaRoot;
+
     this.pendingEntries = new Set();
     this.children = new DefaultWeakMap(() => new Map());
 
+    this.injectedRoots = new Set();
+
     if (params.setPermissionsChangedCallback) {
       params.setPermissionsChangedCallback(
         this.permissionsChanged.bind(this));
     }
   }
 
   /**
    * Check whether the API should be injected.
@@ -2628,17 +2632,18 @@ class Namespace extends Map {
         return context.getDescriptor(entry, dest, name, this.path, this);
       });
     }
   }
 
   getDescriptor(path, context) {
     let obj = Cu.createObjectIn(context.cloneScope);
 
-    this.injectInto(obj, context);
+    let ns = context.schemaRoot.getNamespace(this.path.join("."));
+    ns.injectInto(obj, context);
 
     // Only inject the namespace object if it isn't empty.
     if (Object.keys(obj).length) {
       return {
         descriptor: {value: obj},
       };
     }
   }
@@ -2713,53 +2718,145 @@ class Namespace extends Map {
   has(key) {
     this.init();
     return super.has(key);
   }
 }
 
 
 /**
+ * A namespace which combines the children of an arbitrary number of
+ * sub-namespaces.
+ */
+class Namespaces extends Namespace {
+  constructor(root, name, path, namespaces) {
+    super(root, name, path);
+
+    this.namespaces = namespaces;
+  }
+
+  injectInto(obj, context) {
+    for (let ns of this.namespaces) {
+      ns.injectInto(obj, context);
+    }
+  }
+}
+
+/**
+ * A root schema which combines the contents of an arbitrary number of base
+ * schema roots.
+ */
+class SchemaRoots extends Namespaces {
+  constructor(root, bases) {
+    bases = bases.map(base => base.rootSchema || base);
+
+    super(null, "", [], bases);
+
+    this.root = root;
+    this.bases = bases;
+    this._namespaces = new Map();
+  }
+
+  _getNamespace(name, create) {
+    let results = [];
+    for (let root of this.bases) {
+      let ns = root.getNamespace(name, create);
+      if (ns) {
+        results.push(ns);
+      }
+    }
+
+    if (results.length == 1) {
+      return results[0];
+    }
+
+    if (results.length > 0) {
+      return new Namespaces(this.root, name, name.split("."), results);
+    }
+    return null;
+  }
+
+  getNamespace(name, create) {
+    let ns = this._namespaces.get(name);
+    if (!ns) {
+      ns = this._getNamespace(name, create);
+      if (ns) {
+        this._namespaces.set(name, ns);
+      }
+    }
+    return ns;
+  }
+
+  * getNamespaces(name) {
+    for (let root of this.bases) {
+      yield* root.getNamespaces(name);
+    }
+  }
+}
+
+/**
  * A root schema namespace containing schema data which is isolated from data in
  * other schema roots. May extend a base namespace, in which case schemas in
  * this root may refer to types in a base, but not vice versa.
  *
  * @param {SchemaRoot|Array<SchemaRoot>|null} base
  *        A base schema root (or roots) from which to derive, or null.
  * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON
  *        A map of schema URLs and corresponding JSON blobs from which to
  *        populate this root namespace.
  */
 class SchemaRoot extends Namespace {
   constructor(base, schemaJSON) {
     super(null, "", []);
 
+    if (Array.isArray(base)) {
+      base = new SchemaRoots(this, base);
+    }
+
     this.root = this;
     this.base = base;
     this.schemaJSON = schemaJSON;
   }
 
+  * getNamespaces(path) {
+    let name = path.join(".");
+
+    let ns = this.getNamespace(name, false);
+    if (ns) {
+      yield ns;
+    }
+
+    if (this.base) {
+      yield* this.base.getNamespaces(name);
+    }
+  }
+
   /**
    * Returns the sub-namespace with the given name. If the given namespace
    * doesn't already exist, attempts to find it in the base SchemaRoot before
    * creating a new empty namespace.
    *
    * @param {string} name
    *        The namespace to retrieve.
    * @param {boolean} [create = true]
    *        If true, an empty namespace should be created if one does not
    *        already exist.
    * @returns {Namespace|null}
    */
   getNamespace(name, create = true) {
-    let res = this.base && this.base.getNamespace(name, false);
-    if (res) {
-      return res;
+    let ns = super.getNamespace(name, false);
+    if (ns) {
+      return ns;
     }
-    return super.getNamespace(name, create);
+
+    ns = this.base && this.base.getNamespace(name, false);
+    if (ns) {
+      return ns;
+    }
+    return create && super.getNamespace(name, create);
   }
 
   /**
    * Like getNamespace, but does not take the base SchemaRoot into account.
    *
    * @param {string} name
    *        The namespace to retrieve.
    * @returns {Namespace}
@@ -2849,22 +2946,32 @@ class SchemaRoot extends Namespace {
    * Inject registered extension APIs into `dest`.
    *
    * @param {object} dest The root namespace for the APIs.
    *     This object is usually exposed to extensions as "chrome" or "browser".
    * @param {object} wrapperFuncs An implementation of the InjectionContext
    *     interface, which runs the actual functionality of the generated API.
    */
   inject(dest, wrapperFuncs) {
-    let context = new InjectionContext(wrapperFuncs);
-
-    if (this.base) {
-      this.base.injectInto(dest, context);
+    let context = new InjectionContext(wrapperFuncs, this);
+
+    this.injectInto(dest, context);
+  }
+
+  injectInto(dest, context) {
+    // For schema graphs where multiple schema roots have the same base, don't
+    // inject it more than once.
+
+    if (!context.injectedRoots.has(this)) {
+      context.injectedRoots.add(this);
+      if (this.base) {
+        this.base.injectInto(dest, context);
+      }
+      super.injectInto(dest, context);
     }
-    this.injectInto(dest, context);
   }
 
   /**
    * Normalize `obj` according to the loaded schema for `typeName`.
    *
    * @param {object} obj The object to normalize against the schema.
    * @param {string} typeName The name in the format namespace.propertyname
    * @param {object} context An implementation of Context. Any validation errors
--- a/toolkit/components/extensions/extension-process-script.js
+++ b/toolkit/components/extensions/extension-process-script.js
@@ -495,16 +495,23 @@ ExtensionProcessScript.prototype = {
   },
 
   initExtensionDocument(policy, doc) {
     if (DocumentManager.globals.has(getMessageManager(doc.defaultView))) {
       DocumentManager.loadInto(policy, doc.defaultView);
     }
   },
 
+  getExtensionChild(id) {
+    let policy = WebExtensionPolicy.getByID(id);
+    if (policy) {
+      return extensions.get(policy);
+    }
+  },
+
   preloadContentScript(contentScript) {
     contentScripts.get(contentScript).preload();
   },
 
   loadContentScript(contentScript, window) {
     if (DocumentManager.globals.has(getMessageManager(window))) {
       contentScripts.get(contentScript).injectInto(window);
     }