--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -14,19 +14,30 @@ const global = this;
Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
+ DefaultMap,
instanceOf,
} = ExtensionUtils;
+class DeepMap extends DefaultMap {
+ constructor() {
+ super(() => new DeepMap());
+ }
+
+ getPath(...keys) {
+ return keys.reduce((map, prop) => map.get(prop), this);
+ }
+}
+
XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
"@mozilla.org/addons/content-policy;1",
"nsIAddonContentPolicy");
this.EXPORTED_SYMBOLS = ["Schemas"];
/* globals Schemas, URL */
@@ -52,16 +63,68 @@ function readJSON(url) {
resolve(JSON.parse(text));
} catch (e) {
reject(e);
}
});
});
}
+/**
+ * Defines a lazy getter for the given property on the given object. Any
+ * security wrappers are waived on the object before the property is
+ * defined, and the getter and setter methods are wrapped for the target
+ * scope.
+ *
+ * The given getter function is guaranteed to be called only once, even
+ * if the target scope retrieves the wrapped getter from the property
+ * descriptor and calls it directly.
+ *
+ * @param {object} object
+ * The object on which to define the getter.
+ * @param {string|Symbol} prop
+ * The property name for which to define the getter.
+ * @param {function} getter
+ * The function to call in order to generate the final property
+ * value.
+ */
+function exportLazyGetter(object, prop, getter) {
+ object = Cu.waiveXrays(object);
+
+ let redefine = value => {
+ if (value === undefined) {
+ delete object[prop];
+ } else {
+ Object.defineProperty(object, prop, {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value,
+ });
+ }
+
+ getter = null;
+
+ return value;
+ };
+
+ Object.defineProperty(object, prop, {
+ enumerable: true,
+ configurable: true,
+
+ get: Cu.exportFunction(function() {
+ return redefine(getter.call(this));
+ }, object),
+
+ set: Cu.exportFunction(value => {
+ redefine(value);
+ }, object),
+ });
+}
+
// Parses a regular expression, with support for the Python extended
// syntax that allows setting flags by including the string (?im)
function parsePattern(pattern) {
let flags = "";
let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
if (match) {
[, flags, pattern] = match;
}
@@ -869,21 +932,23 @@ class StringType extends Type {
}
checkBaseType(baseType) {
return baseType == "string";
}
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;
- }
+ exportLazyGetter(dest, name, () => {
+ let obj = Cu.createObjectIn(dest);
+ for (let e of this.enumeration) {
+ obj[e.toUpperCase()] = e;
+ }
+ return obj;
+ });
}
}
}
let SubModuleType;
class ObjectType extends Type {
static get EXTRA_PROPERTIES() {
return ["properties", "patternProperties", ...super.EXTRA_PROPERTIES];
@@ -1418,41 +1483,45 @@ class SubModuleProperty extends Entry {
super(schema);
this.name = name;
this.namespaceName = namespaceName;
this.reference = reference;
this.properties = properties;
}
inject(apiImpl, path, name, dest, context) {
- let obj = Cu.createObjectIn(dest, {defineAs: name});
+ exportLazyGetter(dest, name, () => {
+ let obj = Cu.createObjectIn(dest);
- 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 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 namespace = subpath.join(".");
- let allowedContexts = fun.allowedContexts.length ? fun.allowedContexts : ns.defaultContexts;
- if (context.shouldInject(namespace, fun.name, allowedContexts)) {
- let apiImpl = context.getImplementation(namespace, fun.name);
- fun.inject(apiImpl, subpath, fun.name, obj, context);
+ let functions = type.functions;
+ for (let fun of functions) {
+ let subpath = path.concat(name);
+ let namespace = subpath.join(".");
+ let allowedContexts = fun.allowedContexts.length ? fun.allowedContexts : ns.defaultContexts;
+ if (context.shouldInject(namespace, fun.name, allowedContexts)) {
+ let apiImpl = context.getImplementation(namespace, fun.name);
+ fun.inject(apiImpl, subpath, fun.name, obj, context);
+ }
}
- }
+
+ // TODO: Inject this.properties.
- // TODO: Inject this.properties.
+ return obj;
+ });
}
}
// This class is a base class for FunctionEntrys and Events. It takes
// care of validating parameter lists (i.e., handling of optional
// parameters and parameter type checking).
class CallEntry extends Entry {
constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) {
@@ -1557,47 +1626,49 @@ class FunctionEntry extends CallEntry {
if (this.unsupported) {
return;
}
if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
return;
}
- let stub;
- if (this.isAsync) {
- stub = (...args) => {
- this.checkDeprecated(context);
- let actuals = this.checkParameters(args, context);
- let callback = null;
- if (this.hasAsyncCallback) {
- callback = actuals.pop();
- }
- 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 apiImpl.callFunctionNoReturn(actuals);
- };
- } else {
- stub = (...args) => {
- this.checkDeprecated(context);
- let actuals = this.checkParameters(args, context);
- return apiImpl.callFunction(actuals);
- };
- }
- Cu.exportFunction(stub, dest, {defineAs: name});
+ exportLazyGetter(dest, name, () => {
+ let stub;
+ if (this.isAsync) {
+ stub = (...args) => {
+ this.checkDeprecated(context);
+ let actuals = this.checkParameters(args, context);
+ let callback = null;
+ if (this.hasAsyncCallback) {
+ callback = actuals.pop();
+ }
+ 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 apiImpl.callFunctionNoReturn(actuals);
+ };
+ } else {
+ stub = (...args) => {
+ this.checkDeprecated(context);
+ let actuals = this.checkParameters(args, context);
+ return apiImpl.callFunction(actuals);
+ };
+ }
+ return Cu.exportFunction(stub, dest);
+ });
}
}
// Represents an "event" defined in a schema namespace.
class Event extends CallEntry {
constructor(schema, path, name, type, extraParameters, unsupported, permissions) {
super(schema, path, name, extraParameters);
this.type = type;
@@ -1617,36 +1688,41 @@ class Event extends CallEntry {
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);
- apiImpl.addListener(listener, actuals);
- };
+ exportLazyGetter(dest, name, () => {
+ let addStub = (listener, ...args) => {
+ listener = this.checkListener(listener, context);
+ let actuals = this.checkParameters(args, context);
+ apiImpl.addListener(listener, actuals);
+ };
+
+ let removeStub = (listener) => {
+ listener = this.checkListener(listener, context);
+ apiImpl.removeListener(listener);
+ };
- let removeStub = (listener) => {
- listener = this.checkListener(listener, context);
- apiImpl.removeListener(listener);
- };
+ let hasStub = (listener) => {
+ listener = this.checkListener(listener, context);
+ return apiImpl.hasListener(listener);
+ };
+
+ let obj = Cu.createObjectIn(dest);
- let hasStub = (listener) => {
- listener = this.checkListener(listener, context);
- return apiImpl.hasListener(listener);
- };
+ Cu.exportFunction(addStub, obj, {defineAs: "addListener"});
+ Cu.exportFunction(removeStub, obj, {defineAs: "removeListener"});
+ Cu.exportFunction(hasStub, obj, {defineAs: "hasListener"});
- 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"});
+ return obj;
+ });
}
}
const TYPES = Object.freeze(Object.assign(Object.create(null), {
any: AnyType,
array: ArrayType,
boolean: BooleanType,
function: FunctionType,
@@ -1666,16 +1742,17 @@ this.Schemas = {
// Map[<schema-name> -> Map[<symbol-name> -> Entry]]
// This keeps track of all the schemas that have been loaded so far.
namespaces: new Map(),
register(namespaceName, symbol, value) {
let ns = this.namespaces.get(namespaceName);
if (!ns) {
ns = new Map();
+ ns.name = namespaceName;
ns.permissions = null;
ns.allowedContexts = [];
ns.defaultContexts = [];
this.namespaces.set(namespaceName, ns);
}
ns.set(symbol, value);
},
@@ -1916,74 +1993,89 @@ this.Schemas = {
checkPermissions(namespace, wrapperFuncs) {
let ns = this.namespaces.get(namespace);
if (ns && ns.permissions) {
return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
}
return true;
},
+ exportLazyGetter,
+
/**
* 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);
- for (let [namespace, ns] of this.namespaces) {
- if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) {
- continue;
- }
+ let createNamespace = ns => {
+ let obj = Cu.createObjectIn(dest);
- if (!wrapperFuncs.shouldInject(namespace, null, ns.allowedContexts)) {
- continue;
- }
+ for (let [name, entry] of ns) {
+ let allowedContexts = entry.allowedContexts;
+ if (!allowedContexts.length) {
+ allowedContexts = ns.defaultContexts;
+ }
- let obj = Cu.createObjectIn(dest, {defineAs: namespace});
- for (let [name, entry] of ns) {
- let allowedContexts = entry.allowedContexts.length ? entry.allowedContexts : ns.defaultContexts;
- if (context.shouldInject(namespace, name, allowedContexts)) {
- let apiImpl = context.getImplementation(namespace, name);
- entry.inject(apiImpl, [namespace], name, obj, context);
+ if (context.shouldInject(ns.name, name, allowedContexts)) {
+ let apiImpl = context.getImplementation(ns.name, name);
+ entry.inject(apiImpl, [ns.name], name, obj, context);
}
}
// Remove the namespace object if it is empty
- if (!Object.keys(obj).length) {
- delete dest[namespace];
- // process the next namespace.
+ if (Object.keys(obj).length) {
+ return obj;
+ }
+ };
+
+ let createNestedNamespaces = (parent, namespaces) => {
+ for (let [prop, namespace] of namespaces) {
+ if (namespace instanceof DeepMap) {
+ exportLazyGetter(parent, prop, () => {
+ let obj = Cu.createObjectIn(parent);
+ createNestedNamespaces(obj, namespace);
+ return obj;
+ });
+ } else {
+ exportLazyGetter(parent, prop,
+ () => createNamespace(namespace));
+ }
+ }
+ };
+
+ let nestedNamespaces = new DeepMap();
+ for (let ns of this.namespaces.values()) {
+ if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) {
continue;
}
- // If the nested namespaced API object (e.g devtools.inspectedWindow) is not empty,
- // then turn `dest["nested.namespace"]` into `dest["nested"]["namespace"]`.
- if (namespace.includes(".")) {
- let apiObj = dest[namespace];
- delete dest[namespace];
+ if (!wrapperFuncs.shouldInject(ns.name, null, ns.allowedContexts)) {
+ continue;
+ }
- let nsLevels = namespace.split(".");
- let currentObj = dest;
- for (let nsLevel of nsLevels.slice(0, -1)) {
- if (!currentObj[nsLevel]) {
- // Create the namespace level if it doesn't exist yet.
- currentObj = Cu.createObjectIn(currentObj, {defineAs: nsLevel});
- } else {
- // Move currentObj to the nested object if it already exists.
- currentObj = currentObj[nsLevel];
- }
- }
+ if (ns.name.includes(".")) {
+ let path = ns.name.split(".");
+ let leafName = path.pop();
- // Copy the apiObj as the final nested level.
- currentObj[nsLevels.pop()] = apiObj;
+ let parent = nestedNamespaces.getPath(...path);
+
+ parent.set(leafName, ns);
+ } else {
+ exportLazyGetter(dest, ns.name,
+ () => createNamespace(ns));
}
}
+
+ createNestedNamespaces(dest, nestedNamespaces);
},
/**
* 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