--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -86,16 +86,17 @@ if (!AppConstants.RELEASE_BUILD) {
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
BaseContext,
EventEmitter,
SchemaAPIManager,
LocaleData,
Messenger,
instanceOf,
+ LocalAPIImplementation,
flushJarCache,
} = ExtensionUtils;
const LOGGER_ID_BASE = "addons.webextension.";
const UUID_MAP_PREF = "extensions.webextensions.uuids";
const LEAVE_STORAGE_PREF = "extensions.webextensions.keepStorageOnUninstall";
const LEAVE_UUID_PREF = "extensions.webextensions.keepUuidOnUninstall";
@@ -260,17 +261,17 @@ class ProxyContext extends ExtensionCont
super(extension, params);
// TODO(robwu): Get ProxyContext to inherit from BaseContext instead of
// ExtensionContext and let callers specify the environment type.
this.envType = "content_parent";
this.messageManager = messageManager;
this.principal_ = principal;
this.apiObj = {};
- GlobalManager.injectInObject(this, null, this.apiObj);
+ GlobalManager.injectInObject(this, false, this.apiObj);
this.listenerProxies = new Map();
this.sandbox = Cu.Sandbox(principal, {});
}
get principal() {
return this.principal_;
@@ -281,29 +282,32 @@ class ProxyContext extends ExtensionCont
}
get externallyVisible() {
return false;
}
}
function findPathInObject(obj, path) {
- // Split any nested namespace (e.g devtools.inspectedWindow) element
- // and concatenate them into a flatten array.
- path = path.reduce((acc, el) => {
- return acc.concat(el.split("."));
- }, []);
-
- for (let elt of path) {
+ for (let elt of path.split(".")) {
// If we get a null object before reaching the requested path
// (e.g. the API object is returned only on particular kind of contexts instead
// of based on WebExtensions permissions, like it happens for the devtools APIs),
// stop searching and return undefined.
+ // TODO(robwu): This should never be reached. If an API is not available for
+ // a context, it should be declared as such in the schema and enforced by
+ // `shouldInject`, for instance using the same logic that is used to opt-in
+ // to APIs in content scripts.
+ // If this check is kept, then there is a discrepancy between APIs depending
+ // on whether it is generated locally or remotely: Non-existing local APIs
+ // are excluded in `shouldInject` by this check, but remote APIs do not have
+ // this information and will therefore cause the schema API generator to
+ // create an API that proxies to a non-existing API implementation.
if (!obj || !(elt in obj)) {
- return undefined;
+ return null;
}
obj = obj[elt];
}
return obj;
}
@@ -385,17 +389,17 @@ let ParentAPIManager = {
}
let args = data.args;
args = Cu.cloneInto(args, context.sandbox);
if (data.callId) {
args = args.concat(callback);
}
try {
- findPathInObject(context.apiObj, data.path)[data.name](...args);
+ findPathInObject(context.apiObj, data.path)(...args);
} catch (e) {
let msg = e.message || "API failed";
target.messageManager.sendAsyncMessage("API:CallResult", {
childId: data.childId,
callId: data.callId,
lastError: msg,
});
}
@@ -403,33 +407,30 @@ let ParentAPIManager = {
addListener(data, target) {
let context = this.proxyContexts.get(data.childId);
function listener(...listenerArgs) {
target.messageManager.sendAsyncMessage("API:RunListener", {
childId: data.childId,
path: data.path,
- name: data.name,
args: listenerArgs,
});
}
- let ref = data.path.concat(data.name).join(".");
- context.listenerProxies.set(ref, listener);
+ context.listenerProxies.set(data.path, listener);
let args = Cu.cloneInto(data.args, context.sandbox);
- findPathInObject(context.apiObj, data.path)[data.name].addListener(listener, ...args);
+ findPathInObject(context.apiObj, data.path).addListener(listener, ...args);
},
removeListener(data) {
let context = this.proxyContexts.get(data.childId);
- let ref = data.path.concat(data.name).join(".");
- let listener = context.listenerProxies.get(ref);
- findPathInObject(context.apiObj, data.path)[data.name].removeListener(listener);
+ let listener = context.listenerProxies.get(data.path);
+ findPathInObject(context.apiObj, data.path).removeListener(listener);
},
};
ParentAPIManager.init();
// All moz-extension URIs use a machine-specific UUID rather than the
// extension's own ID in the host component. This makes it more
// difficult for web pages to detect whether a user has a given add-on
@@ -560,110 +561,69 @@ GlobalManager = {
this.initialized = false;
}
},
getExtension(extensionId) {
return this.extensionMap.get(extensionId);
},
- injectInObject(context, defaultCallback, dest) {
+ injectInObject(context, isChromeCompat, dest) {
let apis = {
extensionTypes: {},
};
Management.generateAPIs(context, apis);
SchemaAPIManager.generateAPIs(context, context.extension.apis, apis);
let schemaWrapper = {
+ isChromeCompat,
+
get principal() {
return context.principal;
},
get cloneScope() {
return context.cloneScope;
},
hasPermission(permission) {
return context.extension.hasPermission(permission);
},
- callFunction(pathObj, path, name, args) {
- return pathObj[name](...args);
- },
-
- callFunctionNoReturn(pathObj, path, name, args) {
- pathObj[name](...args);
- },
-
- callAsyncFunction(pathObj, path, name, args, callback) {
- // We pass an empty stub function as a default callback for
- // the `chrome` API, so promise objects are not returned,
- // and lastError values are reported immediately.
- if (callback === null) {
- callback = defaultCallback;
- }
-
- let promise;
- try {
- promise = pathObj[name](...args) || Promise.resolve();
- } catch (e) {
- promise = Promise.reject(e);
- }
-
- return context.wrapPromise(promise, callback);
- },
-
shouldInject(namespace, name, restrictions) {
// Do not generate content script APIs, unless explicitly allowed.
if (context.envType === "content_parent" &&
(!restrictions || !restrictions.includes("content"))) {
return false;
}
- return findPathInObject(apis, [namespace]);
- },
-
- getProperty(pathObj, path, name) {
- return pathObj[name];
+ return findPathInObject(apis, namespace) !== null;
},
- setProperty(pathObj, path, name, value) {
- pathObj[name] = value;
- },
-
- addListener(pathObj, path, name, listener, args) {
- pathObj[name].addListener.call(null, listener, ...args);
- },
- removeListener(pathObj, path, name, listener) {
- pathObj[name].removeListener.call(null, listener);
- },
- hasListener(pathObj, path, name, listener) {
- return pathObj[name].hasListener.call(null, listener);
+ getImplementation(namespace, name) {
+ let pathObj = findPathInObject(apis, namespace);
+ return new LocalAPIImplementation(pathObj, name, context);
},
};
Schemas.inject(dest, schemaWrapper);
},
observe(document, topic, data) {
let contentWindow = document.defaultView;
if (!contentWindow) {
return;
}
let inject = context => {
- // We create two separate sets of bindings, one for the `chrome`
- // global, and one for the `browser` global. The latter returns
- // Promise objects if a callback is not passed, while the former
- // does not.
- let injectObject = (name, defaultCallback) => {
+ let injectObject = (name, isChromeCompat) => {
let browserObj = Cu.createObjectIn(contentWindow, {defineAs: name});
- this.injectInObject(context, defaultCallback, browserObj);
+ this.injectInObject(context, isChromeCompat, browserObj);
};
- injectObject("browser", null);
- injectObject("chrome", () => {});
+ injectObject("browser", false);
+ injectObject("chrome", true);
};
let id = ExtensionManagement.getAddonIdForWindow(contentWindow);
// We don't inject privileged APIs into sub-frames of a UI page.
const {FULL_PRIVILEGES} = ExtensionManagement.API_LEVELS;
if (ExtensionManagement.getAPILevelForWindow(contentWindow, id) !== FULL_PRIVILEGES) {
return;
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1411,18 +1411,253 @@ function detectLanguage(text) {
return {
language: lang.languageCode,
percentage: lang.percent,
};
}),
}));
}
+/**
+ * An object that runs the implementation of a schema API. Instantiations of
+ * this interfaces are used by Schemas.jsm.
+ *
+ * @interface
+ */
+class SchemaAPIInterface {
+ /**
+ * Calls this as a function that returns its return value.
+ *
+ * @abstract
+ * @param {Array} args The parameters for the function.
+ * @returns {*} The return value of the invoked function.
+ */
+ callFunction(args) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Calls this as a function and ignores its return value.
+ *
+ * @abstract
+ * @param {Array} args The parameters for the function.
+ */
+ callFunctionNoReturn(args) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Calls this as a function that completes asynchronously.
+ *
+ * @abstract
+ * @param {Array} args The parameters for the function.
+ * @param {function(*)} [callback] The callback to be called when the function
+ * completes.
+ * @returns {Promise|undefined} Must be void if `callback` is set, and a
+ * promise otherwise. The promise is resolved when the function completes.
+ */
+ callAsyncFunction(args, callback) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Retrieves the value of this as a property.
+ *
+ * @abstract
+ * @returns {*} The value of the property.
+ */
+ getProperty() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Assigns the value to this as property.
+ *
+ * @abstract
+ * @param {string} value The new value of the property.
+ */
+ setProperty(value) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Registers a `listener` to this as an event.
+ *
+ * @abstract
+ * @param {function} listener The callback to be called when the event fires.
+ * @param {Array} args Extra parameters for EventManager.addListener.
+ * @see EventManager.addListener
+ */
+ addListener(listener, args) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Checks whether `listener` is listening to this as an event.
+ *
+ * @abstract
+ * @param {function} listener The event listener.
+ * @returns {boolean} Whether `listener` is registered with this as an event.
+ * @see EventManager.hasListener
+ */
+ hasListener(listener) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Unregisters `listener` from this as an event.
+ *
+ * @abstract
+ * @param {function} listener The event listener.
+ * @see EventManager.removeListener
+ */
+ removeListener(listener) {
+ throw new Error("Not implemented");
+ }
+}
+
+/**
+ * An object that runs a locally implemented API.
+ */
+class LocalAPIImplementation extends SchemaAPIInterface {
+ /**
+ * Constructs an implementation of the `name` method or property of `pathObj`.
+ *
+ * @param {object} pathObj The object containing the member with name `name`.
+ * @param {string} name The name of the implemented member.
+ * @param {BaseContext} context The context in which the schema is injected.
+ */
+ constructor(pathObj, name, context) {
+ super();
+ this.pathObj = pathObj;
+ this.name = name;
+ this.context = context;
+ }
+
+ callFunction(args) {
+ return this.pathObj[this.name](...args);
+ }
+
+ callFunctionNoReturn(args) {
+ this.pathObj[this.name](...args);
+ }
+
+ callAsyncFunction(args, callback) {
+ let promise;
+ try {
+ promise = this.pathObj[this.name](...args) || Promise.resolve();
+ } catch (e) {
+ promise = Promise.reject(e);
+ }
+ return this.context.wrapPromise(promise, callback);
+ }
+
+ getProperty() {
+ return this.pathObj[this.name];
+ }
+
+ setProperty(value) {
+ this.pathObj[this.name] = value;
+ }
+
+ addListener(listener, args) {
+ this.pathObj[this.name].addListener.call(null, listener, ...args);
+ }
+
+ hasListener(listener) {
+ return this.pathObj[this.name].hasListener.call(null, listener);
+ }
+
+ removeListener(listener) {
+ this.pathObj[this.name].removeListener.call(null, listener);
+ }
+}
+
let nextId = 1;
+/**
+ * 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.
+ * @param {ChildAPIManager} childApiManager The owner of this implementation.
+ */
+ constructor(namespace, name, childApiManager) {
+ super();
+ this.path = `${namespace}.${name}`;
+ this.childApiManager = childApiManager;
+ }
+
+ callFunctionNoReturn(args) {
+ this.childApiManager.messageManager.sendAsyncMessage("API:Call", {
+ childId: this.childApiManager.id,
+ path: this.path,
+ args,
+ });
+ }
+
+ callAsyncFunction(args, callback) {
+ let callId = nextId++;
+ let deferred = PromiseUtils.defer();
+ this.childApiManager.callPromises.set(callId, deferred);
+
+ this.childApiManager.messageManager.sendAsyncMessage("API:Call", {
+ childId: this.childApiManager.id,
+ callId,
+ path: this.path,
+ args,
+ });
+
+ return this.childApiManager.context.wrapPromise(deferred.promise, callback);
+ }
+
+ addListener(listener, args) {
+ let set = this.childApiManager.listeners.get(this.path);
+ if (!set) {
+ set = new Set();
+ this.childApiManager.listeners.set(this.path, set);
+ }
+
+ set.add(listener);
+
+ if (set.size == 1) {
+ args = args.slice(1);
+
+ this.childApiManager.messageManager.sendAsyncMessage("API:AddListener", {
+ childId: this.childApiManager.id,
+ path: this.path,
+ args,
+ });
+ }
+ }
+
+ removeListener(listener) {
+ let set = this.childApiManager.listeners.get(this.path);
+ if (!set) {
+ return;
+ }
+ set.remove(listener);
+
+ if (set.size == 0) {
+ this.childApiManager.messageManager.sendAsyncMessage("Extension:RemoveListener", {
+ childId: this.childApiManager.id,
+ path: this.path,
+ });
+ }
+ }
+
+ hasListener(listener) {
+ let set = this.childApiManager.listeners.get(this.path);
+ return set ? set.has(listener) : false;
+ }
+}
+
// We create one instance of this class for every extension context
// that needs to use remote APIs. It uses the message manager to
// communicate with the ParentAPIManager singleton in
// Extension.jsm. It handles asynchronous function calls as well as
// event listeners.
class ChildAPIManager {
constructor(context, messageManager, localApis, contextData) {
this.context = context;
@@ -1453,18 +1688,17 @@ class ChildAPIManager {
receiveMessage({name, data}) {
if (data.childId != this.id) {
return;
}
switch (name) {
case "API:RunListener":
- let ref = data.path.concat(data.name).join(".");
- let listeners = this.listeners.get(ref);
+ let listeners = this.listeners.get(data.path);
for (let callback of listeners) {
runSafe(this.context, callback, ...data.args);
}
break;
case "API:CallResult":
let deferred = this.callPromises.get(data.callId);
if (data.lastError) {
@@ -1480,150 +1714,46 @@ class ChildAPIManager {
close() {
this.messageManager.sendAsyncMessage("API:CloseProxyContext", {childId: this.id});
}
get cloneScope() {
return this.context.cloneScope;
}
- callFunction(pathObj, path, name, args) {
- if (pathObj) {
- return pathObj[name](...args);
- }
- throw new Error("Not implemented");
- }
-
- callFunctionNoReturn(pathObj, path, name, args) {
- if (pathObj) {
- pathObj[name](...args);
- return;
- }
- this.messageManager.sendAsyncMessage("API:Call", {
- childId: this.id,
- path, name, args,
- });
- }
-
- callAsyncFunction(pathObj, path, name, args, callback) {
- if (pathObj) {
- let promise;
- try {
- promise = pathObj[name](...args) || Promise.resolve();
- } catch (e) {
- promise = Promise.reject(e);
- }
- return this.context.wrapPromise(promise, callback);
- }
- let callId = nextId++;
- let deferred = PromiseUtils.defer();
- this.callPromises.set(callId, deferred);
-
- this.messageManager.sendAsyncMessage("API:Call", {
- childId: this.id,
- callId,
- path, name, args,
- });
-
- return this.context.wrapPromise(deferred.promise, callback);
- }
-
shouldInject(namespace, name, restrictions) {
// Do not generate content script APIs, unless explicitly allowed.
if (this.context.envType === "content_child" &&
(!restrictions || !restrictions.includes("content"))) {
return false;
}
+ return true;
+ }
+
+ getImplementation(namespace, name) {
let pathObj = this.localApis;
if (pathObj) {
for (let part of namespace.split(".")) {
pathObj = pathObj[part];
if (!pathObj) {
break;
}
}
- if (pathObj && (name === null || name in pathObj)) {
- return pathObj;
+ if (pathObj && name in pathObj) {
+ return new LocalAPIImplementation(pathObj, name, this.context);
}
}
// No local API found, defer implementation to the parent.
- return true;
+ return new ProxyAPIImplementation(namespace, name, this);
}
hasPermission(permission) {
return this.context.extension.permissions.has(permission);
}
-
- getProperty(pathObj, path, name) {
- if (pathObj) {
- return pathObj[name];
- }
- throw new Error("Not implemented");
- }
-
- setProperty(pathObj, path, name, value) {
- if (pathObj) {
- pathObj[name] = value;
- return;
- }
- throw new Error("Not implemented");
- }
-
- addListener(pathObj, path, name, listener, args) {
- if (pathObj) {
- pathObj[name].addListener(listener, ...args);
- return;
- }
- let ref = path.concat(name).join(".");
- let set;
- if (this.listeners.has(ref)) {
- set = this.listeners.get(ref);
- } else {
- set = new Set();
- this.listeners.set(ref, set);
- }
-
- set.add(listener);
-
- if (set.size == 1) {
- args = args.slice(1);
-
- this.messageManager.sendAsyncMessage("API:AddListener", {
- childId: this.id,
- path, name, args,
- });
- }
- }
-
- removeListener(pathObj, path, name, listener) {
- if (pathObj) {
- pathObj[name].removeListener(listener);
- return;
- }
- let ref = path.concat(name).join(".");
- let set = this.listeners.get(ref) || new Set();
- set.remove(listener);
-
- if (set.size == 0) {
- this.messageManager.sendAsyncMessage("Extension:RemoveListener", {
- childId: this.id,
- path, name,
- });
- }
- }
-
- hasListener(pathObj, path, name, listener) {
- if (pathObj) {
- return pathObj[name].hasListener(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.
*/
@@ -1798,16 +1928,18 @@ this.ExtensionUtils = {
runSafeSync,
runSafeSyncWithoutClone,
runSafeWithoutClone,
BaseContext,
DefaultWeakMap,
EventEmitter,
EventManager,
IconDetails,
+ LocalAPIImplementation,
LocaleData,
Messenger,
PlatformInfo,
+ SchemaAPIInterface,
SingletonEventManager,
SpreadArgs,
ChildAPIManager,
SchemaAPIManager,
};
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -90,25 +90,17 @@ const CONTEXT_FOR_VALIDATION = [
"logError",
];
// Methods of Context that are used by Schemas.inject.
// Callers of Schemas.inject should implement all of these methods.
const CONTEXT_FOR_INJECTION = [
...CONTEXT_FOR_VALIDATION,
"shouldInject",
- "callFunction",
- "callFunctionNoReturn",
- "callAsyncFunction",
- "getProperty",
- "setProperty",
-
- "addListener",
- "hasListener",
- "removeListener",
+ "getImplementation",
];
/**
* A context for schema validation and error reporting. This class is only used
* internally within Schemas.
*/
class Context {
/**
@@ -119,27 +111,28 @@ class Context {
this.params = params;
this.path = [];
this.preprocessors = {
localize(value, context) {
return value;
},
};
+ this.isChromeCompat = false;
this.currentChoices = new Set();
this.choicePathIndex = 0;
for (let method of overridableMethods) {
if (method in params) {
this[method] = params[method].bind(params);
}
}
- let props = ["preprocessors"];
+ let props = ["preprocessors", "isChromeCompat"];
for (let prop of props) {
if (prop in params) {
if (prop in this && typeof this[prop] == "object") {
Object.assign(this[prop], params[prop]);
} else {
this[prop] = params[prop];
}
}
@@ -336,162 +329,45 @@ class Context {
* as defined in the schema. Otherwise an error is reported to the context.
*/
class InjectionContext extends Context {
constructor(params) {
super(params, CONTEXT_FOR_INJECTION);
}
/**
- * Called before injecting an API. The return value is used to determine
- * whether to inject the API, and if so what the value of the `pathObj`
- * parameter should be when the methods of this interface are called.
- * - If falsey, the API is not injected.
- * - If `true`, the API is injected and the `pathObj` parameter is `null`.
- * - If an object, the `pathObj` parameter is this object. The object SHOULD
- * have a property `name`.
- *
- * With the above contract, a local API implementation can simply be
- * implemented as follows:
- *
- * callFunction(pathObj, path, name, args) {
- * if (pathObj)
- * return pathObj[name];
- * // else the local API does not exist, so fall back or throw an error.
- * }
+ * Check whether the API should be injected.
*
* @abstract
* @param {string} namespace The namespace of the API. This may contain dots,
* e.g. in the case of "devtools.inspectedWindow".
* @param {string} [name] The name of the property in the namespace.
* `null` if we are checking whether the namespace should be injected.
* @param {Array} restrictions An arbitrary list of restrictions as declared
* by the schema for a given API node.
- * @returns {*} An object with the property `name`, `true` or a falsey value.
+ * @returns {boolean} Whether the API should be injected.
*/
shouldInject(namespace, name, restrictions) {
throw new Error("Not implemented");
}
/**
- * Calls function `path`.`name` and returns its return value.
- *
- * @abstract
- * @param {*} pathObj See `shouldInject`.
- * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
- * @param {string} name The method name, e.g. "get".
- * @param {Array} args The parameters for the function.
- * @returns {*} The return value of the invoked function.
- */
- callFunction(pathObj, path, name, args) {
- throw new Error("Not implemented");
- }
-
- /**
- * Calls function `path`.`name` and ignores its return value.
- *
- * @abstract
- * @param {*} pathObj See `shouldInject`.
- * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
- * @param {string} name The method name, e.g. "get".
- * @param {Array} args The parameters for the function.
- */
- callFunctionNoReturn(pathObj, path, name, args) {
- throw new Error("Not implemented");
- }
-
- /**
- * Call function `path`.`name` that completes asynchronously.
- *
- * @abstract
- * @param {*} pathObj See `shouldInject`.
- * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
- * @param {string} name The method name, e.g. "get".
- * @param {Array} args The parameters for the function.
- * @param {function(*)} [callback] The callback to be called when the function
- * completes.
- * @returns {Promise|undefined} Must be void if `callback` is set, and a
- * promise otherwise. The promise is resolved when the function completes.
- */
- callAsyncFunction(pathObj, path, name, args, callback) {
- throw new Error("Not implemented");
- }
-
- /**
- * Retrieves the value of property `path`.`name`.
+ * Generate the implementation for `namespace`.`name`.
*
* @abstract
- * @param {*} pathObj See `shouldInject`.
- * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
- * @param {string} name The property name.
- * @returns {*} The value of the property.
- */
- getProperty(pathObj, path, name) {
- throw new Error("Not implemented");
- }
-
- /**
- * Assigns the value of property `path`.`name`.
- *
- * @abstract
- * @param {*} pathObj See `shouldInject`.
- * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
- * @param {string} name The property name.
- * @param {string} value The new value of the property.
+ * @param {string} namespace The full path to the namespace of the API, minus
+ * the name of the method or property. E.g. "storage.local".
+ * @param {string} name The name of the method, property or event.
+ * @returns {SchemaAPIInterface} The implementation of the API.
*/
- setProperty(pathObj, path, name, value) {
- throw new Error("Not implemented");
- }
-
- /**
- * Registers `listener` for event `path`.`name`.
- *
- * @abstract
- * @param {*} pathObj See `shouldInject`.
- * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
- * @param {string} name The event name, e.g. "onChanged"
- * @param {function} listener The callback to be called when the event fires.
- * @param {Array} args Extra parameters for EventManager.addListener.
- * @see EventManager.addListener
- */
- addListener(pathObj, path, name, listener, args) {
- throw new Error("Not implemented");
- }
-
- /**
- * Checks whether `listener` is listening to event `path`.`name`.
- *
- * @abstract
- * @param {*} pathObj See `shouldInject`.
- * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
- * @param {string} name The event name, e.g. "onChanged"
- * @param {function} listener The event listener.
- * @returns {boolean} Whether `listener` was added to event `path`.`name`.
- * @see EventManager.hasListener
- */
- hasListener(pathObj, path, name, listener) {
- throw new Error("Not implemented");
- }
-
- /**
- * Unregisters `listener` from event `path`.`name`.
- *
- * @abstract
- * @param {*} pathObj See `shouldInject`.
- * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
- * @param {string} name The event name, e.g. "onChanged"
- * @param {function} listener The event listener.
- * @see EventManager.removeListener
- */
- removeListener(pathObj, path, name, listener) {
+ getImplementation(namespace, name) {
throw new Error("Not implemented");
}
}
-
/**
* The methods in this singleton represent the "format" specifier for
* JSON Schema string types.
*
* Each method either returns a normalized version of the original
* value, or throws an error if the value is not valid for the given
* format.
*/
@@ -654,23 +530,23 @@ class Entry {
}
/**
* Injects JS values for the entry into the extension API
* namespace. The default implementation is to do nothing.
* `context` is used to call the actual implementation
* of a given function or event.
*
- * @param {*} pathObj See `shouldInject`.
+ * @param {SchemaAPIInterface} apiImpl The implementation of the API.
* @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
* @param {string} name The method name, e.g. "get".
* @param {object} dest The object where `path`.`name` should be stored.
* @param {InjectionContext} context
*/
- inject(pathObj, path, name, dest, context) {
+ inject(apiImpl, path, name, dest, context) {
}
}
// Corresponds either to a type declared in the "types" section of the
// schema or else to any type object used throughout the schema.
class Type extends Entry {
// Takes a value, checks that it has the correct type, and returns a
// "normalized" version of the value. The normalized version will
@@ -851,17 +727,17 @@ class StringType extends Type {
return r;
}
checkBaseType(baseType) {
return baseType == "string";
}
- inject(pathObj, path, name, dest, context) {
+ inject(apiImpl, path, name, dest, context) {
if (this.enumeration) {
let obj = Cu.createObjectIn(dest, {defineAs: name});
for (let e of this.enumeration) {
let key = e.toUpperCase();
obj[key] = e;
}
}
}
@@ -1168,17 +1044,17 @@ class FunctionType extends Type {
// particular value. Essentially this is a constant.
class ValueProperty extends Entry {
constructor(schema, name, value) {
super(schema);
this.name = name;
this.value = value;
}
- inject(pathObj, path, name, dest, context) {
+ inject(apiImpl, path, name, dest, context) {
dest[name] = this.value;
}
}
// Represents a "property" defined in a schema namespace that is not a
// constant.
class TypeProperty extends Entry {
constructor(schema, namespaceName, name, type, writable) {
@@ -1188,41 +1064,41 @@ class TypeProperty extends Entry {
this.type = type;
this.writable = writable;
}
throwError(context, msg) {
throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`);
}
- inject(pathObj, path, name, dest, context) {
+ inject(apiImpl, path, name, dest, context) {
if (this.unsupported) {
return;
}
let getStub = () => {
this.checkDeprecated(context);
- return context.getProperty(pathObj, path, name);
+ return apiImpl.getProperty();
};
let desc = {
configurable: false,
enumerable: true,
get: Cu.exportFunction(getStub, dest),
};
if (this.writable) {
let setStub = (value) => {
let normalized = this.type.normalize(value, context);
if (normalized.error) {
this.throwError(context, normalized.error);
}
- context.setProperty(pathObj, path, name, normalized.value);
+ apiImpl.setProperty(normalized.value);
};
desc.set = Cu.exportFunction(setStub, dest);
}
Object.defineProperty(dest, name, desc);
}
}
@@ -1241,37 +1117,37 @@ class SubModuleProperty extends Entry {
constructor(schema, name, namespaceName, reference, properties) {
super(schema);
this.name = name;
this.namespaceName = namespaceName;
this.reference = reference;
this.properties = properties;
}
- inject(pathObj, path, name, dest, context) {
+ inject(apiImpl, path, name, dest, context) {
let obj = Cu.createObjectIn(dest, {defineAs: name});
let ns = Schemas.namespaces.get(this.namespaceName);
let type = ns.get(this.reference);
if (!type && this.reference.includes(".")) {
let [namespaceName, ref] = this.reference.split(".");
ns = Schemas.namespaces.get(namespaceName);
type = ns.get(ref);
}
if (!type || !(type instanceof SubModuleType)) {
throw new Error(`Internal error: ${this.namespaceName}.${this.reference} is not a sub-module`);
}
let functions = type.functions;
for (let fun of functions) {
let subpath = path.concat(name);
- let pathObj = context.shouldInject(subpath.join("."), fun.name, fun.restrictions || ns.defaultRestrictions);
- if (pathObj) {
- pathObj = pathObj === true ? null : pathObj;
- fun.inject(pathObj, subpath, fun.name, obj, context);
+ let namespace = subpath.join(".");
+ if (context.shouldInject(namespace, fun.name, fun.restrictions || ns.defaultRestrictions)) {
+ let apiImpl = context.getImplementation(namespace, fun.name);
+ fun.inject(apiImpl, subpath, fun.name, obj, context);
}
}
// TODO: Inject this.properties.
}
}
// This class is a base class for FunctionEntrys and Events. It takes
@@ -1370,17 +1246,17 @@ class FunctionEntry extends CallEntry {
this.unsupported = unsupported;
this.returns = returns;
this.permissions = permissions;
this.isAsync = type.isAsync;
this.hasAsyncCallback = type.hasAsyncCallback;
}
- inject(pathObj, path, name, dest, context) {
+ inject(apiImpl, path, name, dest, context) {
if (this.unsupported) {
return;
}
if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
return;
}
@@ -1388,29 +1264,35 @@ class FunctionEntry extends CallEntry {
if (this.isAsync) {
stub = (...args) => {
this.checkDeprecated(context);
let actuals = this.checkParameters(args, context);
let callback = null;
if (this.hasAsyncCallback) {
callback = actuals.pop();
}
- return context.callAsyncFunction(pathObj, path, name, actuals, callback);
+ if (callback === null && context.isChromeCompat) {
+ // We pass an empty stub function as a default callback for
+ // the `chrome` API, so promise objects are not returned,
+ // and lastError values are reported immediately.
+ callback = () => {};
+ }
+ return apiImpl.callAsyncFunction(actuals, callback);
};
} else if (!this.returns) {
stub = (...args) => {
this.checkDeprecated(context);
let actuals = this.checkParameters(args, context);
- return context.callFunctionNoReturn(pathObj, path, name, actuals);
+ return apiImpl.callFunctionNoReturn(actuals);
};
} else {
stub = (...args) => {
this.checkDeprecated(context);
let actuals = this.checkParameters(args, context);
- return context.callFunction(pathObj, path, name, actuals);
+ return apiImpl.callFunction(actuals);
};
}
Cu.exportFunction(stub, dest, {defineAs: name});
}
}
// Represents an "event" defined in a schema namespace.
class Event extends CallEntry {
@@ -1424,39 +1306,39 @@ class Event extends CallEntry {
checkListener(listener, context) {
let r = this.type.normalize(listener, context);
if (r.error) {
this.throwError(context, "Invalid listener");
}
return r.value;
}
- inject(pathObj, path, name, dest, context) {
+ inject(apiImpl, path, name, dest, context) {
if (this.unsupported) {
return;
}
if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
return;
}
let addStub = (listener, ...args) => {
listener = this.checkListener(listener, context);
let actuals = this.checkParameters(args, context);
- context.addListener(pathObj, this.path, name, listener, actuals);
+ apiImpl.addListener(listener, actuals);
};
let removeStub = (listener) => {
listener = this.checkListener(listener, context);
- context.removeListener(pathObj, this.path, name, listener);
+ apiImpl.removeListener(listener);
};
let hasStub = (listener) => {
listener = this.checkListener(listener, context);
- return context.hasListener(pathObj, this.path, name, listener);
+ return apiImpl.hasListener(listener);
};
let obj = Cu.createObjectIn(dest, {defineAs: name});
Cu.exportFunction(addStub, obj, {defineAs: "addListener"});
Cu.exportFunction(removeStub, obj, {defineAs: "removeListener"});
Cu.exportFunction(hasStub, obj, {defineAs: "hasListener"});
}
}
@@ -1881,20 +1763,19 @@ this.Schemas = {
}
if (!wrapperFuncs.shouldInject(namespace, null, ns.restrictions)) {
continue;
}
let obj = Cu.createObjectIn(dest, {defineAs: namespace});
for (let [name, entry] of ns) {
- let pathObj = context.shouldInject(namespace, name, entry.restrictions || ns.defaultRestrictions);
- if (pathObj) {
- pathObj = pathObj === true ? null : pathObj;
- entry.inject(pathObj, [namespace], name, obj, context);
+ if (context.shouldInject(namespace, name, entry.restrictions || ns.defaultRestrictions)) {
+ let apiImpl = context.getImplementation(namespace, name);
+ entry.inject(apiImpl, [namespace], name, obj, context);
}
}
// Remove the namespace object if it is empty
if (!Object.keys(obj).length) {
delete dest[namespace];
// process the next namespace.
continue;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -1,12 +1,15 @@
"use strict";
Components.utils.import("resource://gre/modules/Schemas.jsm");
Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
+
+let {LocalAPIImplementation, SchemaAPIInterface} = ExtensionUtils;
let json = [
{namespace: "testing",
properties: {
PROP1: {value: 20},
prop2: {type: "string"},
prop3: {
@@ -66,16 +69,17 @@ let json = [
{
id: "submodule",
type: "object",
functions: [
{
name: "sub_foo",
type: "function",
parameters: [],
+ returns: "integer",
},
],
},
],
functions: [
{
name: "foo",
@@ -381,16 +385,52 @@ function checkErrors(errors) {
`${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`);
}
talliedErrors.length = 0;
}
let permissions = new Set();
+class TallyingAPIImplementation extends SchemaAPIInterface {
+ constructor(namespace, name) {
+ super();
+ this.namespace = namespace;
+ this.name = name;
+ }
+
+ callFunction(args) {
+ tally("call", this.namespace, this.name, args);
+ }
+
+ callFunctionNoReturn(args) {
+ tally("call", this.namespace, this.name, args);
+ }
+
+ getProperty() {
+ tally("get", this.namespace, this.name);
+ }
+
+ setProperty(value) {
+ tally("set", this.namespace, this.name, value);
+ }
+
+ addListener(listener, args) {
+ tally("addListener", this.namespace, this.name, [listener, args]);
+ }
+
+ removeListener(listener) {
+ tally("removeListener", this.namespace, this.name, [listener]);
+ }
+
+ hasListener(listener) {
+ tally("hasListener", this.namespace, this.name, [listener]);
+ }
+}
+
let wrapper = {
url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
checkLoadURL(url) {
return !url.startsWith("chrome:");
},
preprocessors: {
@@ -402,60 +442,33 @@ let wrapper = {
logError(message) {
talliedErrors.push(message);
},
hasPermission(permission) {
return permissions.has(permission);
},
- callFunction(pathObj, path, name, args) {
- let ns = path.join(".");
- tally("call", ns, name, args);
- },
-
- callFunctionNoReturn(pathObj, path, name, args) {
- let ns = path.join(".");
- tally("call", ns, name, args);
- },
-
shouldInject(ns) {
return ns != "do-not-inject";
},
- getProperty(pathObj, path, name) {
- let ns = path.join(".");
- tally("get", ns, name);
- },
-
- setProperty(pathObj, path, name, value) {
- let ns = path.join(".");
- tally("set", ns, name, value);
- },
-
- addListener(pathObj, path, name, listener, args) {
- let ns = path.join(".");
- tally("addListener", ns, name, [listener, args]);
- },
- removeListener(pathObj, path, name, listener) {
- let ns = path.join(".");
- tally("removeListener", ns, name, [listener]);
- },
- hasListener(pathObj, path, name, listener) {
- let ns = path.join(".");
- tally("hasListener", ns, name, [listener]);
+ getImplementation(namespace, name) {
+ return new TallyingAPIImplementation(namespace, name);
},
};
add_task(function* () {
let url = "data:," + JSON.stringify(json);
yield Schemas.load(url);
let root = {};
+ tallied = null;
Schemas.inject(root, wrapper);
+ do_check_eq(tallied, null);
do_check_eq(root.testing.PROP1, 20, "simple value property");
do_check_eq(root.testing.type1.VALUE1, "value1", "enum type");
do_check_eq(root.testing.type1.VALUE2, "value2", "enum type");
do_check_eq("inject" in root, true, "namespace 'inject' should be injected");
do_check_eq("do-not-inject" in root, false, "namespace 'do-not-inject' should not be injected");
@@ -1268,8 +1281,86 @@ add_task(function* testNestedNamespace()
// ok(instanceOfCustomType.url,
// "Got the expected property defined in the CustomType instance)
//
// ok(instanceOfCustomType.onEvent &&
// instanceOfCustomType.onEvent.addListener &&
// typeof instanceOfCustomType.onEvent.addListener == "function",
// "Got the expected event defined in the CustomType instance");
});
+
+add_task(function* testLocalAPIImplementation() {
+ let countGet2 = 0;
+ let countProp3 = 0;
+ let countProp3SubFoo = 0;
+
+ let testingApiObj = {
+ get PROP1() {
+ // PROP1 is a schema-defined constant.
+ throw new Error("Unexpected get PROP1");
+ },
+ get prop2() {
+ ++countGet2;
+ return "prop2 val";
+ },
+ get prop3() {
+ throw new Error("Unexpected get prop3");
+ },
+ set prop3(v) {
+ // prop3 is a submodule, defined as a function, so the API should not pass
+ // through assignment to prop3.
+ throw new Error("Unexpected set prop3");
+ },
+ };
+ let submoduleApiObj = {
+ get sub_foo() {
+ ++countProp3;
+ return () => {
+ return ++countProp3SubFoo;
+ };
+ },
+ };
+
+ let localWrapper = {
+ shouldInject(ns) {
+ return ns == "testing" || ns == "testing.prop3";
+ },
+ getImplementation(ns, name) {
+ do_check_true(ns == "testing" || ns == "testing.prop3");
+ if (ns == "testing.prop3" && name == "sub_foo") {
+ // It is fine to use `null` here because we don't call async functions.
+ return new LocalAPIImplementation(submoduleApiObj, name, null);
+ }
+ // It is fine to use `null` here because we don't call async functions.
+ return new LocalAPIImplementation(testingApiObj, name, null);
+ },
+ };
+
+ let root = {};
+ Schemas.inject(root, localWrapper);
+ do_check_eq(countGet2, 0);
+ do_check_eq(countProp3, 0);
+ do_check_eq(countProp3SubFoo, 0);
+
+ do_check_eq(root.testing.PROP1, 20);
+
+ do_check_eq(root.testing.prop2, "prop2 val");
+ do_check_eq(countGet2, 1);
+
+ do_check_eq(root.testing.prop2, "prop2 val");
+ do_check_eq(countGet2, 2);
+
+ do_print(JSON.stringify(root.testing));
+ do_check_eq(root.testing.prop3.sub_foo(), 1);
+ do_check_eq(countProp3, 1);
+ do_check_eq(countProp3SubFoo, 1);
+
+ do_check_eq(root.testing.prop3.sub_foo(), 2);
+ do_check_eq(countProp3, 2);
+ do_check_eq(countProp3SubFoo, 2);
+
+ root.testing.prop3.sub_foo = () => { return "overwritten"; };
+ do_check_eq(root.testing.prop3.sub_foo(), "overwritten");
+
+ root.testing.prop3 = {sub_foo() { return "overwritten again"; }};
+ do_check_eq(root.testing.prop3.sub_foo(), "overwritten again");
+ do_check_eq(countProp3SubFoo, 2);
+});
deleted file mode 100644
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_pathObj.js
+++ /dev/null
@@ -1,156 +0,0 @@
-"use strict";
-
-Components.utils.import("resource://gre/modules/Schemas.jsm");
-
-function generateNsWithPropValue(val) {
- let count = 0;
- return {
- get count() { return count; },
- get prop1() {
- ++count;
- return val;
- },
- };
-}
-
-let pathObjApiJson = [
- {
- namespace: "ret_false",
- properties: {
- // Nb. Somehow a string is a valid value for the "object" type.
- prop1: {type: "object", "value": "value of ret-false"},
- },
- },
- {
- namespace: "ret_false_noval",
- properties: {
- prop1: {type: "object"},
- },
- },
- {
- namespace: "dot.obj",
- properties: {
- prop1: {type: "object"},
- },
- },
- {
- namespace: "ret_obj",
- properties: {
- prop1: {type: "object"},
- },
- },
- {
- namespace: "ret_true",
- properties: {
- prop1: {type: "object", value: "value of ret-true"},
- },
- },
- {
- namespace: "ret_true_noval",
- properties: {
- prop1: {type: "object"},
- },
- },
- {
- namespace: "with_submodule",
- types: [{
- id: "subtype",
- type: "object",
- // Properties in submodules are not supported (yet), so we use functions.
- functions: [{
- name: "retObj",
- type: "function",
- parameters: [],
- returns: {type: "string"},
- }, {
- name: "retFalse",
- type: "function",
- parameters: [],
- returns: {type: "string"},
- }, {
- name: "retTrue",
- type: "function",
- parameters: [],
- returns: {type: "string"},
- }],
- }],
- properties: {
- mySub: {$ref: "subtype"},
- },
- },
-];
-add_task(function* testPathObj() {
- let url = "data:," + JSON.stringify(pathObjApiJson);
- yield Schemas.load(url);
-
- let localApi = generateNsWithPropValue("localApi value");
- let dotApi = generateNsWithPropValue("dotApi value");
- let submoduleApi = {
- _count: 0,
- retObj() {
- return "submoduleApi value " + (++submoduleApi._count);
- },
- };
- let localWrapper = {
- shouldInject(ns, name) {
- if (name === null) {
- return true; // Accept any namespace. Properties are checked below.
- }
- if (ns == "ret_obj") {
- return localApi;
- } else if (ns == "dot.obj") {
- return dotApi;
- } else if (ns == "ret_true" || ns == "ret_true_noval") {
- return true;
- } else if (ns == "ret_false" || ns == "ret_false_noval") {
- return false;
- } else if (ns == "with_submodule") {
- if (name == "subtype" || name == "mySub") {
- return true;
- }
- } else if (ns == "with_submodule.mySub") {
- if (name == "retTrue") {
- return true;
- } else if (name == "retFalse") {
- return false;
- } else if (name == "retObj") {
- return submoduleApi;
- }
- }
- throw new Error(`Unexpected shouldInject call: ${ns} ${name}`);
- },
- getProperty(pathObj, path, name) {
- if (pathObj) {
- return pathObj[name];
- }
- return "fallback,pathObj=" + pathObj;
- },
- callFunction(pathObj, path, name, args) {
- if (pathObj) {
- return pathObj[name](...args);
- }
- return "fallback call,pathObj=" + pathObj;
- },
- };
-
- let root = {};
- Schemas.inject(root, localWrapper);
-
- do_check_eq(0, localApi.count);
- do_check_eq(0, dotApi.count);
- do_check_eq(Object.keys(root).sort().join(),
- "dot,ret_obj,ret_true,ret_true_noval,with_submodule");
-
- do_check_eq("fallback,pathObj=null", root.ret_true_noval.prop1);
- do_check_eq("value of ret-true", root.ret_true.prop1);
- do_check_eq("localApi value", root.ret_obj.prop1);
- do_check_eq(1, localApi.count);
- do_check_eq("dotApi value", root.dot.obj.prop1);
- do_check_eq(1, dotApi.count);
-
- do_check_eq(Object.keys(root.with_submodule).sort().join(), "mySub");
- let mySub = root.with_submodule.mySub;
- do_check_eq(Object.keys(mySub).sort().join(), "retObj,retTrue");
- do_check_eq("fallback call,pathObj=null", mySub.retTrue());
- do_check_eq("submoduleApi value 1", mySub.retObj());
-});
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas_restrictions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_restrictions.js
@@ -61,27 +61,33 @@ let schemaJson = [
}],
}],
properties: {
prop1: {$ref: "subtype"},
prop2: {$ref: "subtype", restrictions: ["test_eleven"]},
},
},
];
-add_task(function* testPathObj() {
+add_task(function* testRestrictions() {
let url = "data:," + JSON.stringify(schemaJson);
yield Schemas.load(url);
let results = {};
let localWrapper = {
shouldInject(ns, name, restrictions) {
restrictions = restrictions ? restrictions.join() : "none";
name = name === null ? ns : ns + "." + name;
results[name] = restrictions;
return true;
},
+ getImplementation() {
+ // The actual implementation is not significant for this test.
+ // Let's take this opportunity to see if schema generation is free of
+ // exceptions even when somehow getImplementation does not return an
+ // implementation.
+ },
};
let root = {};
Schemas.inject(root, localWrapper);
function verify(path, expected) {
let result = results[path];
do_check_eq(result, expected);
@@ -116,10 +122,18 @@ add_task(function* testPathObj() {
verify("with_submodule.prop1.restrictNo", "test_nine");
verify("with_submodule.prop1.restrictYes", "test_ten");
verify("with_submodule.prop2", "test_eleven");
// Note: test_nine inherits restrictions from the namespace, not from
// submodule. There is no "defaultRestrictions" for submodule types to not
// complicate things.
verify("with_submodule.prop1.restrictNo", "test_nine");
verify("with_submodule.prop1.restrictYes", "test_ten");
+
+ // This is a constant, so it does not matter that getImplementation does not
+ // return an implementation since the API injector should take care of it.
+ do_check_eq(root.noRestrict.prop3, 1);
+
+ Assert.throws(() => root.noRestrict.prop1,
+ /undefined/,
+ "Should throw when the implementation is absent.");
});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -39,17 +39,16 @@ skip-if = release_build
[test_ext_onmessage_removelistener.js]
[test_ext_runtime_connect_no_receiver.js]
[test_ext_runtime_getPlatformInfo.js]
[test_ext_runtime_sendMessage.js]
[test_ext_runtime_sendMessage_errors.js]
[test_ext_runtime_sendMessage_no_receiver.js]
[test_ext_schemas.js]
[test_ext_schemas_api_injection.js]
-[test_ext_schemas_pathObj.js]
[test_ext_schemas_restrictions.js]
[test_ext_simple.js]
[test_ext_storage.js]
[test_getAPILevelForWindow.js]
[test_locale_converter.js]
[test_locale_data.js]
[test_native_messaging.js]
skip-if = os == "android"