Bug 1323845: Part 6a - Support WebExtension-style experiment API provider extensions. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Tue, 09 Jan 2018 17:20:55 -0800
changeset 718304 636292fe9bc958fc53321eb62779ca206aa9e73a
parent 718303 cee6f35737290fa7270b37d07987679051710d46
child 718305 2f8e7cfe997084b1566401bd42d8af6fe1625648
push id94869
push usermaglione.k@gmail.com
push dateWed, 10 Jan 2018 01:49:31 +0000
reviewersaswan
bugs1323845
milestone59.0a1
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
@@ -731,16 +731,21 @@ this.ExtensionData = class {
     this.whiteListedHosts = new MatchPatternSet(manifestData.originPermissions);
 
     return this.manifest;
   }
 
   getAPIManager() {
     let apiManagers = [Management];
 
+    for (let id of this.dependencies) {
+      let {extension} = WebExtensionPolicy.getByID(id);
+      apiManagers.push(extension.experimentAPIManager);
+    }
+
     if (this.modules) {
       this.experimentAPIManager =
         new ExtensionCommon.LazyAPIManager("main", this.modules.parent, this.schemaURLs);
 
       apiManagers.push(this.experimentAPIManager);
     }
 
     if (apiManagers.length == 1) {
@@ -1344,17 +1349,19 @@ this.Extension = class extends Extension
     }
 
     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
@@ -1369,16 +1376,17 @@ this.Extension = class extends Extension
       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() {
@@ -1545,16 +1553,18 @@ this.Extension = class extends Extension
     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();
       }
 
@@ -1565,16 +1575,17 @@ this.Extension = class extends Extension
       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,21 @@ 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);
+      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
@@ -111,16 +111,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,19 +721,21 @@ 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());
 
     if (params.setPermissionsChangedCallback) {
       params.setPermissionsChangedCallback(
         this.permissionsChanged.bind(this));
     }
   }
@@ -2600,17 +2602,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},
       };
     }
   }
@@ -2683,31 +2686,117 @@ class Namespace extends Map {
   }
 
   has(key) {
     this.init();
     return super.has(key);
   }
 }
 
+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);
+    }
+  }
+}
+
+class SchemaRoots extends Namespaces {
+  constructor(root, roots) {
+    roots = roots.map(root => root.rootSchema || root);
+
+    super(null, "", [], roots);
+
+    this.root = root;
+    this.roots = roots;
+    this._namespaces = new Map();
+  }
+
+  _getNamespace(name, create) {
+    let results = [];
+    for (let root of this.roots) {
+      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.roots) {
+      yield* root.getNamespaces(name);
+    }
+  }
+}
+
+let alreadyInjected = new DefaultWeakMap(() => new Set());
+
 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);
+    }
+  }
+
   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);
   }
 
   getOwnNamespace(name) {
     return super.getNamespace(name);
   }
 
   parseSchema(schema, path, extraProperties = []) {
     let allowedProperties = DEBUG && new Set(extraProperties);
@@ -2790,22 +2879,33 @@ 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.
+
+    let done = alreadyInjected.get(context);
+    if (!done.has(this)) {
+      done.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
@@ -492,16 +492,20 @@ ExtensionProcessScript.prototype = {
   },
 
   initExtensionDocument(policy, doc) {
     if (DocumentManager.globals.has(getMessageManager(doc.defaultView))) {
       DocumentManager.loadInto(policy, doc.defaultView);
     }
   },
 
+  getExtensionChild(id) {
+    return extensions.get(WebExtensionPolicy.getByID(id));
+  },
+
   preloadContentScript(contentScript) {
     contentScripts.get(contentScript).preload();
   },
 
   loadContentScript(contentScript, window) {
     if (DocumentManager.globals.has(getMessageManager(window))) {
       contentScripts.get(contentScript).injectInto(window);
     }