--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -29,17 +29,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
"@mozilla.org/addons/content-policy;1",
"nsIAddonContentPolicy");
XPCOMUtils.defineLazyGetter(this, "StartupCache", () => ExtensionParent.StartupCache);
-this.EXPORTED_SYMBOLS = ["Schemas"];
+this.EXPORTED_SYMBOLS = ["SchemaRoot", "Schemas"];
const {DEBUG} = AppConstants;
const isParentProcess = Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
function readJSON(url) {
return new Promise((resolve, reject) => {
NetUtil.asyncFetch({uri: url, loadUsingSystemPrincipal: true}, (inputStream, status) => {
@@ -1111,33 +1111,35 @@ class Type extends Entry {
static get EXTRA_PROPERTIES() {
return ["description", "deprecated", "preprocess", "postprocess", "allowedContexts"];
}
/**
* Parses the given schema object and returns an instance of this
* class which corresponds to its properties.
*
+ * @param {SchemaRoot} root
+ * The root schema for this type.
* @param {object} schema
* A JSON schema object which corresponds to a definition of
* this type.
* @param {Array<string>} path
* The path to this schema object from the root schema,
* corresponding to the property names and array indices
* traversed during parsing in order to arrive at this schema
* object.
* @param {Array<string>} [extraProperties]
* An array of extra property names which are valid for this
* schema in the current context.
* @returns {Type}
* An instance of this type which corresponds to the given
* schema object.
* @static
*/
- static parseSchema(schema, path, extraProperties = []) {
+ static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
return new this(schema);
}
/**
* Checks that all of the properties present in the given schema
* object are valid properties for this type, and throws if invalid.
@@ -1221,20 +1223,20 @@ class AnyType extends Type {
}
// An untagged union type.
class ChoiceType extends Type {
static get EXTRA_PROPERTIES() {
return ["choices", ...super.EXTRA_PROPERTIES];
}
- static parseSchema(schema, path, extraProperties = []) {
+ static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
- let choices = schema.choices.map(t => Schemas.parseSchema(t, path));
+ let choices = schema.choices.map(t => root.parseSchema(t, path));
return new this(schema, choices);
}
constructor(schema, choices) {
super(schema);
this.choices = choices;
}
@@ -1286,37 +1288,38 @@ class ChoiceType extends Type {
}
// This is a reference to another type--essentially a typedef.
class RefType extends Type {
static get EXTRA_PROPERTIES() {
return ["$ref", ...super.EXTRA_PROPERTIES];
}
- static parseSchema(schema, path, extraProperties = []) {
+ static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
let ref = schema.$ref;
- let ns = path[0];
+ let ns = path.join(".");
if (ref.includes(".")) {
- [ns, ref] = ref.split(".");
+ [, ns, ref] = /^(.*)\.(.*?)$/.exec(ref);
}
- return new this(schema, ns, ref);
+ return new this(root, schema, ns, ref);
}
// For a reference to a type named T declared in namespace NS,
// namespaceName will be NS and reference will be T.
- constructor(schema, namespaceName, reference) {
+ constructor(root, schema, namespaceName, reference) {
super(schema);
+ this.root = root;
this.namespaceName = namespaceName;
this.reference = reference;
}
get targetType() {
- let ns = Schemas.getNamespace(this.namespaceName);
+ let ns = this.root.getNamespace(this.namespaceName);
let type = ns.get(this.reference);
if (!type) {
throw new Error(`Internal error: Type ${this.reference} not found`);
}
return type;
}
normalize(value, context) {
@@ -1330,17 +1333,17 @@ class RefType extends Type {
}
class StringType extends Type {
static get EXTRA_PROPERTIES() {
return ["enum", "minLength", "maxLength", "pattern", "format",
...super.EXTRA_PROPERTIES];
}
- static parseSchema(schema, path, extraProperties = []) {
+ static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
let enumeration = schema.enum || null;
if (enumeration) {
// The "enum" property is either a list of strings that are
// valid values or else a list of {name, description} objects,
// where the .name values are the valid values.
enumeration = enumeration.map(e => {
@@ -1462,31 +1465,31 @@ let FunctionEntry;
let Event;
let SubModuleType;
class ObjectType extends Type {
static get EXTRA_PROPERTIES() {
return ["properties", "patternProperties", ...super.EXTRA_PROPERTIES];
}
- static parseSchema(schema, path, extraProperties = []) {
+ static parseSchema(root, schema, path, extraProperties = []) {
if ("functions" in schema) {
- return SubModuleType.parseSchema(schema, path, extraProperties);
+ return SubModuleType.parseSchema(root, schema, path, extraProperties);
}
if (DEBUG && !("$extend" in schema)) {
// Only allow extending "properties" and "patternProperties".
extraProperties = ["additionalProperties", "isInstanceOf", ...extraProperties];
}
this.checkSchemaProperties(schema, path, extraProperties);
let parseProperty = (schema, extraProps = []) => {
return {
- type: Schemas.parseSchema(schema, path,
- DEBUG && ["unsupported", "onError", "permissions", "default", ...extraProps]),
+ type: root.parseSchema(schema, path,
+ DEBUG && ["unsupported", "onError", "permissions", "default", ...extraProps]),
optional: schema.optional || false,
unsupported: schema.unsupported || false,
onError: schema.onError || null,
default: schema.default === undefined ? null : schema.default,
};
};
// Parse explicit "properties" object.
@@ -1514,17 +1517,17 @@ class ObjectType extends Type {
// Parse "additionalProperties" schema.
let additionalProperties = null;
if (schema.additionalProperties) {
let type = schema.additionalProperties;
if (type === true) {
type = {"type": "any"};
}
- additionalProperties = Schemas.parseSchema(type, path);
+ additionalProperties = root.parseSchema(type, path);
}
return new this(schema, properties, additionalProperties, patternProperties, schema.isInstanceOf || null);
}
constructor(schema, properties, additionalProperties, patternProperties, isInstanceOf) {
super(schema);
this.properties = properties;
@@ -1695,29 +1698,29 @@ class ObjectType extends Type {
// This type is just a placeholder to be referred to by
// SubModuleProperty. No value is ever expected to have this type.
SubModuleType = class SubModuleType extends Type {
static get EXTRA_PROPERTIES() {
return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
}
- static parseSchema(schema, path, extraProperties = []) {
+ static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
// The path we pass in here is only used for error messages.
path = [...path, schema.id];
let functions = schema.functions.filter(fun => !fun.unsupported)
- .map(fun => FunctionEntry.parseSchema(fun, path));
+ .map(fun => FunctionEntry.parseSchema(root, fun, path));
let events = [];
if (schema.events) {
events = schema.events.filter(event => !event.unsupported)
- .map(event => Event.parseSchema(event, path));
+ .map(event => Event.parseSchema(root, event, path));
}
return new this(functions, events);
}
constructor(functions, events) {
super();
this.functions = functions;
@@ -1745,17 +1748,17 @@ class NumberType extends Type {
}
}
class IntegerType extends Type {
static get EXTRA_PROPERTIES() {
return ["minimum", "maximum", ...super.EXTRA_PROPERTIES];
}
- static parseSchema(schema, path, extraProperties = []) {
+ static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
return new this(schema, schema.minimum || -Infinity, schema.maximum || Infinity);
}
constructor(schema, minimum, maximum) {
super(schema);
this.minimum = minimum;
@@ -1802,20 +1805,20 @@ class BooleanType extends Type {
}
}
class ArrayType extends Type {
static get EXTRA_PROPERTIES() {
return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
}
- static parseSchema(schema, path, extraProperties = []) {
+ static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
- let items = Schemas.parseSchema(schema.items, path, ["onError"]);
+ let items = root.parseSchema(schema.items, path, ["onError"]);
return new this(schema, items, schema.minItems || 0, schema.maxItems || Infinity);
}
constructor(schema, itemType, minItems, maxItems) {
super(schema);
this.itemType = itemType;
this.minItems = minItems;
@@ -1863,34 +1866,34 @@ class ArrayType extends Type {
}
class FunctionType extends Type {
static get EXTRA_PROPERTIES() {
return ["parameters", "async", "returns", "requireUserInput",
...super.EXTRA_PROPERTIES];
}
- static parseSchema(schema, path, extraProperties = []) {
+ static parseSchema(root, schema, path, extraProperties = []) {
this.checkSchemaProperties(schema, path, extraProperties);
let isAsync = !!schema.async;
let isExpectingCallback = typeof schema.async === "string";
let parameters = null;
if ("parameters" in schema) {
parameters = [];
for (let param of schema.parameters) {
// Callbacks default to optional for now, because of promise
// handling.
let isCallback = isAsync && param.name == schema.async;
if (isCallback) {
isExpectingCallback = false;
}
parameters.push({
- type: Schemas.parseSchema(param, path, ["name", "optional", "default"]),
+ type: root.parseSchema(param, path, ["name", "optional", "default"]),
name: param.name,
optional: param.optional == null ? isCallback : param.optional,
default: param.default == undefined ? null : param.default,
});
}
}
let hasAsyncCallback = false;
if (isAsync) {
@@ -2014,34 +2017,35 @@ class SubModuleProperty extends Entry {
// form of sub-module properties, where "$ref" points to a
// SubModuleType containing a list of functions and "properties" is
// a list of additional simple properties.
//
// name: Name of the property stuff is being added to.
// namespaceName: Namespace in which the property lives.
// reference: Name of the type defining the functions to add to the property.
// properties: Additional properties to add to the module (unsupported).
- constructor(schema, path, name, reference, properties, permissions) {
+ constructor(root, schema, path, name, reference, properties, permissions) {
super(schema);
+ this.root = root;
this.name = name;
this.path = path;
this.namespaceName = path.join(".");
this.reference = reference;
this.properties = properties;
this.permissions = permissions;
}
getDescriptor(path, context) {
let obj = Cu.createObjectIn(context.cloneScope);
- let ns = Schemas.getNamespace(this.namespaceName);
+ let ns = this.root.getNamespace(this.namespaceName);
let type = ns.get(this.reference);
if (!type && this.reference.includes(".")) {
let [namespaceName, ref] = this.reference.split(".");
- ns = Schemas.getNamespace(namespaceName);
+ ns = this.root.getNamespace(namespaceName);
type = ns.get(ref);
}
if (DEBUG) {
if (!type || !(type instanceof SubModuleType)) {
throw new Error(`Internal error: ${this.namespaceName}.${this.reference} ` +
`is not a sub-module`);
}
@@ -2163,29 +2167,29 @@ class CallEntry extends Entry {
});
return fixedArgs;
}
}
// Represents a "function" defined in a schema namespace.
FunctionEntry = class FunctionEntry extends CallEntry {
- static parseSchema(schema, path) {
+ static parseSchema(root, schema, path) {
// When not in DEBUG mode, we just need to know *if* this returns.
let returns = !!schema.returns;
if (DEBUG && "returns" in schema) {
returns = {
- type: Schemas.parseSchema(schema.returns, path, ["optional", "name"]),
+ type: root.parseSchema(schema.returns, path, ["optional", "name"]),
optional: schema.returns.optional || false,
name: "result",
};
}
return new this(schema, path, schema.name,
- Schemas.parseSchema(
+ root.parseSchema(
schema, path,
["name", "unsupported", "returns",
"permissions",
"allowAmbiguousOptionalArguments"]),
schema.unsupported || false,
schema.allowAmbiguousOptionalArguments || false,
returns,
schema.permissions || null);
@@ -2288,30 +2292,30 @@ FunctionEntry = class FunctionEntry exte
}
};
// Represents an "event" defined in a schema namespace.
//
// TODO Bug 1369722: we should be able to remove the eslint-disable-line that follows
// once Bug 1369722 has been fixed.
Event = class Event extends CallEntry { // eslint-disable-line no-native-reassign
- static parseSchema(event, path) {
+ static parseSchema(root, event, path) {
let extraParameters = Array.from(event.extraParameters || [], param => ({
- type: Schemas.parseSchema(param, path, ["name", "optional", "default"]),
+ type: root.parseSchema(param, path, ["name", "optional", "default"]),
name: param.name,
optional: param.optional || false,
default: param.default == undefined ? null : param.default,
}));
let extraProperties = ["name", "unsupported", "permissions", "extraParameters",
// We ignore these properties for now.
"returns", "filters"];
return new this(event, path, event.name,
- Schemas.parseSchema(event, path, extraProperties),
+ root.parseSchema(event, path, extraProperties),
extraParameters,
event.unsupported || false,
event.permissions || null);
}
constructor(schema, path, name, type, extraParameters, unsupported, permissions) {
super(schema, path, name, extraParameters);
this.type = type;
@@ -2382,19 +2386,21 @@ const TYPES = Object.freeze(Object.assig
const LOADERS = {
events: "loadEvent",
functions: "loadFunction",
properties: "loadProperty",
types: "loadType",
};
class Namespace extends Map {
- constructor(name, path) {
+ constructor(root, name, path) {
super();
+ this.root = root;
+
this._lazySchemas = [];
this.initialized = false;
this.name = name;
this.path = name ? [...path, name] : [...path];
this.superNamespace = null;
@@ -2416,17 +2422,17 @@ class Namespace extends Map {
for (let prop of ["permissions", "allowedContexts", "defaultContexts"]) {
if (schema[prop]) {
this[prop] = schema[prop];
}
}
if (schema.$import) {
- this.superNamespace = Schemas.getNamespace(schema.$import);
+ this.superNamespace = this.root.getNamespace(schema.$import);
}
}
/**
* Initializes the keys of this namespace based on the schema objects
* added via previous `addSchema` calls.
*/
init() {
@@ -2511,68 +2517,69 @@ class Namespace extends Map {
return this.get(key);
}
loadType(name, type) {
if ("$extend" in type) {
return this.extendType(type);
}
- return Schemas.parseSchema(type, this.path, ["id"]);
+ return this.root.parseSchema(type, this.path, ["id"]);
}
extendType(type) {
let targetType = this.get(type.$extend);
// Only allow extending object and choices types for now.
if (targetType instanceof ObjectType) {
type.type = "object";
} else if (DEBUG) {
if (!targetType) {
throw new Error(`Internal error: Attempt to extend a nonexistant type ${type.$extend}`);
} else if (!(targetType instanceof ChoiceType)) {
throw new Error(`Internal error: Attempt to extend a non-extensible type ${type.$extend}`);
}
}
- let parsed = Schemas.parseSchema(type, this.path, ["$extend"]);
+ let parsed = this.root.parseSchema(type, this.path, ["$extend"]);
if (DEBUG && parsed.constructor !== targetType.constructor) {
throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
}
targetType.extend(parsed);
return targetType;
}
loadProperty(name, prop) {
if ("$ref" in prop) {
if (!prop.unsupported) {
- return new SubModuleProperty(prop, this.path, name,
+ return new SubModuleProperty(this.root,
+ prop, this.path, name,
prop.$ref, prop.properties || {},
prop.permissions || null);
}
} else if ("value" in prop) {
return new ValueProperty(prop, name, prop.value);
} else {
// We ignore the "optional" attribute on properties since we
// don't inject anything here anyway.
- let type = Schemas.parseSchema(prop, [this.name], ["optional", "permissions", "writable"]);
+ let type = this.root.parseSchema(prop, [this.name], ["optional", "permissions", "writable"]);
return new TypeProperty(prop, this.path, name, type, prop.writable || false,
prop.permissions || null);
}
}
loadFunction(name, fun) {
- return FunctionEntry.parseSchema(fun, this.path);
+ return FunctionEntry.parseSchema(this.root, fun, this.path);
}
loadEvent(name, event) {
- return Event.parseSchema(event, this.path);
+ return Event.parseSchema(this.root, event, this.path);
}
/**
* Injects the properties of this namespace into the given object.
*
* @param {object} dest
* The object into which to inject the namespace properties.
* @param {InjectionContext} context
@@ -2629,91 +2636,217 @@ class Namespace extends Map {
/**
* Returns a Namespace object for the given namespace name. If a
* namespace object with this name does not already exist, it is
* created. If the name contains any '.' characters, namespaces are
* recursively created, for each dot-separated component.
*
* @param {string} name
* The name of the sub-namespace to retrieve.
+ * @param {boolean} [create = true]
+ * If true, create any intermediate namespaces which don't
+ * exist.
*
* @returns {Namespace}
*/
- getNamespace(name) {
+ getNamespace(name, create = true) {
let subName;
let idx = name.indexOf(".");
if (idx > 0) {
subName = name.slice(idx + 1);
name = name.slice(0, idx);
}
let ns = super.get(name);
if (!ns) {
- ns = new Namespace(name, this.path);
+ if (!create) {
+ return null;
+ }
+ ns = new Namespace(this.root, name, this.path);
this.set(name, ns);
}
if (subName) {
return ns.getNamespace(subName);
}
return ns;
}
+ getOwnNamespace(name) {
+ return this.getNamespace(name);
+ }
+
has(key) {
this.init();
return super.has(key);
}
}
+class SchemaRoot extends Namespace {
+ constructor(base, schemaJSON) {
+ super(null, "", []);
+
+ this.root = this;
+ this.base = base;
+ this.schemaJSON = schemaJSON;
+ }
+
+ getNamespace(name, create = true) {
+ let res = this.base && this.base.getNamespace(name, false);
+ if (res) {
+ return res;
+ }
+ return super.getNamespace(name, create);
+ }
+
+ getOwnNamespace(name) {
+ return super.getNamespace(name);
+ }
+
+ parseSchema(schema, path, extraProperties = []) {
+ let allowedProperties = DEBUG && new Set(extraProperties);
+
+ if ("choices" in schema) {
+ return ChoiceType.parseSchema(this, schema, path, allowedProperties);
+ } else if ("$ref" in schema) {
+ return RefType.parseSchema(this, schema, path, allowedProperties);
+ }
+
+ let type = TYPES[schema.type];
+
+ if (DEBUG) {
+ allowedProperties.add("type");
+
+ if (!("type" in schema)) {
+ throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
+ }
+
+ if (!type) {
+ throw new Error(`Unexpected type ${schema.type}`);
+ }
+ }
+
+ return type.parseSchema(this, schema, path, allowedProperties);
+ }
+
+ parseSchemas() {
+ for (let [key, schema] of this.schemaJSON.entries()) {
+ try {
+ if (typeof schema.deserialize === "function") {
+ schema = schema.deserialize(global);
+
+ // If we're in the parent process, we need to keep the
+ // StructuredCloneHolder blob around in order to send to future child
+ // processes. If we're in a child, we have no further use for it, so
+ // just store the deserialized schema data in its place.
+ if (!isParentProcess) {
+ this.schemaJSON.set(key, schema);
+ }
+ }
+
+ this.loadSchema(schema);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+
+ loadSchema(json) {
+ for (let namespace of json) {
+ this.getOwnNamespace(namespace.namespace)
+ .addSchema(namespace);
+ }
+ }
+
+ /**
+ * Checks whether a given object has the necessary permissions to
+ * expose the given namespace.
+ *
+ * @param {string} namespace
+ * The top-level namespace to check permissions for.
+ * @param {object} wrapperFuncs
+ * Wrapper functions for the given context.
+ * @param {function} wrapperFuncs.hasPermission
+ * A function which, when given a string argument, returns true
+ * if the context has the given permission.
+ * @returns {boolean}
+ * True if the context has permission for the given namespace.
+ */
+ checkPermissions(namespace, wrapperFuncs) {
+ let ns = this.getNamespace(namespace);
+ if (ns && ns.permissions) {
+ return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
+ }
+ return true;
+ }
+
+ /**
+ * 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);
+ }
+ 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
+ * are reported to the given context.
+ * @returns {object} The normalized object.
+ */
+ normalize(obj, typeName, context) {
+ let [namespaceName, prop] = typeName.split(".");
+ let ns = this.getNamespace(namespaceName);
+ let type = ns.get(prop);
+
+ let result = type.normalize(obj, new Context(context));
+ if (result.error) {
+ return {error: forceString(result.error)};
+ }
+ return result;
+ }
+}
+
this.Schemas = {
initialized: false,
REVOKE: Symbol("@@revoke"),
// Maps a schema URL to the JSON contained in that schema file. This
// is useful for sending the JSON across processes.
schemaJSON: new Map(),
// A separate map of schema JSON which should be available in all
// content processes.
contentSchemaJSON: new Map(),
- // Map[<schema-name> -> Map[<symbol-name> -> Entry]]
- // This keeps track of all the schemas that have been loaded so far.
- rootNamespace: new Namespace("", []),
+ _rootSchema: null,
+
+ get rootSchema() {
+ if (!this.initialized) {
+ this.init();
+ }
+ return this._rootSchema;
+ },
getNamespace(name) {
- return this.rootNamespace.getNamespace(name);
- },
-
- parseSchema(schema, path, extraProperties = []) {
- let allowedProperties = DEBUG && new Set(extraProperties);
-
- if ("choices" in schema) {
- return ChoiceType.parseSchema(schema, path, allowedProperties);
- } else if ("$ref" in schema) {
- return RefType.parseSchema(schema, path, allowedProperties);
- }
-
- let type = TYPES[schema.type];
-
- if (DEBUG) {
- allowedProperties.add("type");
-
- if (!("type" in schema)) {
- throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
- }
-
- if (!type) {
- throw new Error(`Unexpected type ${schema.type}`);
- }
- }
-
- return type.parseSchema(schema, path, allowedProperties);
+ return this.rootSchema.getNamespace(name);
},
init() {
if (this.initialized) {
return;
}
this.initialized = true;
@@ -2754,57 +2887,23 @@ this.Schemas = {
break;
}
},
_needFlush: true,
flushSchemas() {
if (this._needFlush) {
this._needFlush = false;
- XPCOMUtils.defineLazyGetter(this, "rootNamespace",
- () => this.parseSchemas());
- }
- },
-
- parseSchemas() {
- this._needFlush = true;
-
- Object.defineProperty(this, "rootNamespace", {
- enumerable: true,
- configurable: true,
- value: new Namespace("", []),
- });
-
- for (let [key, schema] of this.schemaJSON.entries()) {
- try {
- if (typeof schema.deserialize === "function") {
- schema = schema.deserialize(global);
-
- // If we're in the parent process, we need to keep the
- // StructuredCloneHolder blob around in order to send to future child
- // processes. If we're in a child, we have no further use for it, so
- // just store the deserialized schema data in its place.
- if (!isParentProcess) {
- this.schemaJSON.set(key, schema);
- }
- }
-
- this.loadSchema(schema);
- } catch (e) {
- Cu.reportError(e);
- }
- }
-
- return this.rootNamespace;
- },
-
- loadSchema(json) {
- for (let namespace of json) {
- this.getNamespace(namespace.namespace)
- .addSchema(namespace);
+ XPCOMUtils.defineLazyGetter(this, "_rootSchema", () => {
+ this._needFlush = true;
+
+ let rootSchema = new SchemaRoot(null, this.schemaJSON);
+ rootSchema.parseSchemas();
+ return rootSchema;
+ });
}
},
_loadCachedSchemasPromise: null,
loadCachedSchemas() {
if (!this._loadCachedSchemasPromise) {
this._loadCachedSchemasPromise = StartupCache.schemas.getAll().then(results => {
return results;
@@ -2867,64 +2966,38 @@ this.Schemas = {
* Wrapper functions for the given context.
* @param {function} wrapperFuncs.hasPermission
* A function which, when given a string argument, returns true
* if the context has the given permission.
* @returns {boolean}
* True if the context has permission for the given namespace.
*/
checkPermissions(namespace, wrapperFuncs) {
- if (!this.initialized) {
- this.init();
- }
-
- let ns = this.getNamespace(namespace);
- if (ns && ns.permissions) {
- return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
- }
- return true;
+ return this.rootSchema.checkPermissions(namespace, wrapperFuncs);
},
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) {
- if (!this.initialized) {
- this.init();
- }
-
- let context = new InjectionContext(wrapperFuncs);
-
- this.rootNamespace.injectInto(dest, context);
+ this.rootSchema.inject(dest, wrapperFuncs);
},
/**
* 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
* are reported to the given context.
* @returns {object} The normalized object.
*/
normalize(obj, typeName, context) {
- if (!this.initialized) {
- this.init();
- }
-
- let [namespaceName, prop] = typeName.split(".");
- let ns = this.getNamespace(namespaceName);
- let type = ns.get(prop);
-
- let result = type.normalize(obj, new Context(context));
- if (result.error) {
- return {error: forceString(result.error)};
- }
- return result;
+ return this.rootSchema.normalize(obj, typeName, context);
},
};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas_roots.js
@@ -0,0 +1,239 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Components.utils.import("resource://gre/modules/Schemas.jsm");
+Components.utils.import("resource://gre/modules/ExtensionCommon.jsm");
+
+let {SchemaAPIInterface} = ExtensionCommon;
+
+const global = this;
+
+let baseSchemaJSON = [
+ {
+ namespace: "base",
+
+ properties: {
+ PROP1: {value: 42},
+ },
+
+ types: [
+ {
+ id: "type1",
+ type: "string",
+ "enum": ["value1", "value2", "value3"],
+ },
+ ],
+
+ functions: [
+ {
+ name: "foo",
+ type: "function",
+ parameters: [
+ {name: "arg1", $ref: "type1"},
+ ],
+ },
+ ],
+ },
+];
+
+let experimentFooJSON = [
+ {
+ namespace: "experiments.foo",
+ types: [
+ {
+ id: "typeFoo",
+ type: "string",
+ "enum": ["foo1", "foo2", "foo3"],
+ },
+ ],
+
+ functions: [
+ {
+ name: "foo",
+ type: "function",
+ parameters: [
+ {name: "arg1", $ref: "typeFoo"},
+ {name: "arg2", $ref: "base.type1"},
+ ],
+ },
+ ],
+ },
+];
+
+let experimentBarJSON = [
+ {
+ namespace: "experiments.bar",
+ types: [
+ {
+ id: "typeBar",
+ type: "string",
+ "enum": ["bar1", "bar2", "bar3"],
+ },
+ ],
+
+ functions: [
+ {
+ name: "bar",
+ type: "function",
+ parameters: [
+ {name: "arg1", $ref: "typeBar"},
+ {name: "arg2", $ref: "base.type1"},
+ ],
+ },
+ ],
+ },
+];
+
+
+let tallied = null;
+
+function tally(kind, ns, name, args) {
+ tallied = [kind, ns, name, args];
+}
+
+function verify(...args) {
+ do_check_eq(JSON.stringify(tallied), JSON.stringify(args));
+ tallied = null;
+}
+
+let talliedErrors = [];
+
+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);
+ if (this.name === "sub_foo") {
+ return 13;
+ }
+ }
+
+ 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/",
+
+ cloneScope: global,
+
+ checkLoadURL(url) {
+ return !url.startsWith("chrome:");
+ },
+
+ preprocessors: {
+ localize(value, context) {
+ return value.replace(/__MSG_(.*?)__/g, (m0, m1) => `${m1.toUpperCase()}`);
+ },
+ },
+
+ logError(message) {
+ talliedErrors.push(message);
+ },
+
+ hasPermission(permission) {
+ return permissions.has(permission);
+ },
+
+ shouldInject(ns, name) {
+ return name != "do-not-inject";
+ },
+
+ getImplementation(namespace, name) {
+ return new TallyingAPIImplementation(namespace, name);
+ },
+};
+
+add_task(async function() {
+ let baseSchemas = new Map([
+ ["resource://schemas/base.json", baseSchemaJSON],
+ ]);
+ let experimentSchemas = new Map([
+ ["resource://experiment-foo/schema.json", experimentFooJSON],
+ ["resource://experiment-bar/schema.json", experimentBarJSON],
+ ]);
+
+ let baseSchema = new SchemaRoot(null, baseSchemas);
+ let schema = new SchemaRoot(baseSchema, experimentSchemas);
+
+ baseSchema.parseSchemas();
+ schema.parseSchemas();
+
+ let root = {};
+ let base = {};
+
+ tallied = null;
+
+ baseSchema.inject(base, wrapper);
+ schema.inject(root, wrapper);
+
+ equal(typeof base.base, "object", "base.base exists");
+ equal(typeof root.base, "object", "root.base exists");
+ equal(typeof base.experiments, "undefined", "base.experiments exists not");
+ equal(typeof root.experiments, "object", "root.experiments exists");
+ equal(typeof root.experiments.foo, "object", "root.experiments.foo exists");
+ equal(typeof root.experiments.bar, "object", "root.experiments.bar exists");
+
+ do_check_eq(tallied, null);
+
+ do_check_eq(root.base.PROP1, 42, "root.base.PROP1");
+ do_check_eq(base.base.PROP1, 42, "root.base.PROP1");
+
+ root.base.foo("value2");
+ verify("call", "base", "foo", ["value2"]);
+
+ base.base.foo("value3");
+ verify("call", "base", "foo", ["value3"]);
+
+
+ root.experiments.foo.foo("foo2", "value1");
+ verify("call", "experiments.foo", "foo", ["foo2", "value1"]);
+
+ root.experiments.bar.bar("bar2", "value1");
+ verify("call", "experiments.bar", "bar", ["bar2", "value1"]);
+
+
+ Assert.throws(() => root.base.foo("Meh."),
+ /Type error for parameter arg1/,
+ "root.base.foo()");
+
+ Assert.throws(() => base.base.foo("Meh."),
+ /Type error for parameter arg1/,
+ "base.base.foo()");
+
+ Assert.throws(() => root.experiments.foo.foo("Meh."),
+ /Incorrect argument types/,
+ "root.experiments.foo.foo()");
+
+ Assert.throws(() => root.experiments.bar.bar("Meh."),
+ /Incorrect argument types/,
+ "root.experiments.bar.bar()");
+});