Bug 1323845: Part 6a - Support WebExtension-style experiment API provider extensions. r?aswan
MozReview-Commit-ID: E1IBFyzEwqU
--- 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);
}