diff options
Diffstat (limited to 'toolkit/components/webextensions/Schemas.jsm')
-rw-r--r-- | toolkit/components/webextensions/Schemas.jsm | 2143 |
1 files changed, 0 insertions, 2143 deletions
diff --git a/toolkit/components/webextensions/Schemas.jsm b/toolkit/components/webextensions/Schemas.jsm deleted file mode 100644 index 159211c79..000000000 --- a/toolkit/components/webextensions/Schemas.jsm +++ /dev/null @@ -1,2143 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -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 */ - -function readJSON(url) { - return new Promise((resolve, reject) => { - NetUtil.asyncFetch({uri: url, loadUsingSystemPrincipal: true}, (inputStream, status) => { - if (!Components.isSuccessCode(status)) { - // Convert status code to a string - let e = Components.Exception("", status); - reject(new Error(`Error while loading '${url}' (${e.name})`)); - return; - } - try { - let text = NetUtil.readInputStreamToString(inputStream, inputStream.available()); - - // Chrome JSON files include a license comment that we need to - // strip off for this to be valid JSON. As a hack, we just - // look for the first '[' character, which signals the start - // of the JSON content. - let index = text.indexOf("["); - text = text.slice(index); - - 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), - }); -} - -const POSTPROCESSORS = { - convertImageDataToURL(imageData, context) { - let document = context.cloneScope.document; - let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); - canvas.width = imageData.width; - canvas.height = imageData.height; - canvas.getContext("2d").putImageData(imageData, 0, 0); - - return canvas.toDataURL("image/png"); - }, -}; - -// 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; - } - return new RegExp(pattern, flags); -} - -function getValueBaseType(value) { - let t = typeof(value); - if (t == "object") { - if (value === null) { - return "null"; - } else if (Array.isArray(value)) { - return "array"; - } else if (Object.prototype.toString.call(value) == "[object ArrayBuffer]") { - return "binary"; - } - } else if (t == "number") { - if (value % 1 == 0) { - return "integer"; - } - } - return t; -} - -// Methods of Context that are used by Schemas.normalize. These methods can be -// overridden at the construction of Context. -const CONTEXT_FOR_VALIDATION = [ - "checkLoadURL", - "hasPermission", - "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", - "getImplementation", -]; - -/** - * A context for schema validation and error reporting. This class is only used - * internally within Schemas. - */ -class Context { - /** - * @param {object} params Provides the implementation of this class. - * @param {Array<string>} overridableMethods - */ - constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) { - this.params = params; - - this.path = []; - this.preprocessors = { - localize(value, context) { - return value; - }, - }; - this.postprocessors = POSTPROCESSORS; - 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", "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]; - } - } - } - } - - get choicePath() { - let path = this.path.slice(this.choicePathIndex); - return path.join("."); - } - - get cloneScope() { - return this.params.cloneScope; - } - - get url() { - return this.params.url; - } - - get principal() { - return this.params.principal || Services.scriptSecurityManager.createNullPrincipal({}); - } - - /** - * Checks whether `url` may be loaded by the extension in this context. - * - * @param {string} url The URL that the extension wished to load. - * @returns {boolean} Whether the context may load `url`. - */ - checkLoadURL(url) { - let ssm = Services.scriptSecurityManager; - try { - ssm.checkLoadURIStrWithPrincipal(this.principal, url, - ssm.DISALLOW_INHERIT_PRINCIPAL); - } catch (e) { - return false; - } - return true; - } - - /** - * Checks whether this context has the given permission. - * - * @param {string} permission - * The name of the permission to check. - * - * @returns {boolean} True if the context has the given permission. - */ - hasPermission(permission) { - return false; - } - - /** - * Returns an error result object with the given message, for return - * by Type normalization functions. - * - * If the context has a `currentTarget` value, this is prepended to - * the message to indicate the location of the error. - * - * @param {string} errorMessage - * The error message which will be displayed when this is the - * only possible matching schema. - * @param {string} choicesMessage - * The message describing the valid what constitutes a valid - * value for this schema, which will be displayed when multiple - * schema choices are available and none match. - * - * A caller may pass `null` to prevent a choice from being - * added, but this should *only* be done from code processing a - * choices type. - * @returns {object} - */ - error(errorMessage, choicesMessage = undefined) { - if (choicesMessage !== null) { - let {choicePath} = this; - if (choicePath) { - choicesMessage = `.${choicePath} must ${choicesMessage}`; - } - - this.currentChoices.add(choicesMessage); - } - - if (this.currentTarget) { - return {error: `Error processing ${this.currentTarget}: ${errorMessage}`}; - } - return {error: errorMessage}; - } - - /** - * Creates an `Error` object belonging to the current unprivileged - * scope. If there is no unprivileged scope associated with this - * context, the message is returned as a string. - * - * If the context has a `currentTarget` value, this is prepended to - * the message, in the same way as for the `error` method. - * - * @param {string} message - * @returns {Error} - */ - makeError(message) { - let {error} = this.error(message); - if (this.cloneScope) { - return new this.cloneScope.Error(error); - } - return error; - } - - /** - * Logs the given error to the console. May be overridden to enable - * custom logging. - * - * @param {Error|string} error - */ - logError(error) { - Cu.reportError(error); - } - - /** - * Returns the name of the value currently being normalized. For a - * nested object, this is usually approximately equivalent to the - * JavaScript property accessor for that property. Given: - * - * { foo: { bar: [{ baz: x }] } } - * - * When processing the value for `x`, the currentTarget is - * 'foo.bar.0.baz' - */ - get currentTarget() { - return this.path.join("."); - } - - /** - * Executes the given callback, and returns an array of choice strings - * passed to {@see #error} during its execution. - * - * @param {function} callback - * @returns {object} - * An object with a `result` property containing the return - * value of the callback, and a `choice` property containing - * an array of choices. - */ - withChoices(callback) { - let {currentChoices, choicePathIndex} = this; - - let choices = new Set(); - this.currentChoices = choices; - this.choicePathIndex = this.path.length; - - try { - let result = callback(); - - return {result, choices: Array.from(choices)}; - } finally { - this.currentChoices = currentChoices; - this.choicePathIndex = choicePathIndex; - - choices = Array.from(choices); - if (choices.length == 1) { - currentChoices.add(choices[0]); - } else if (choices.length) { - let n = choices.length - 1; - choices[n] = `or ${choices[n]}`; - - this.error(null, `must either [${choices.join(", ")}]`); - } - } - } - - /** - * Appends the given component to the `currentTarget` path to indicate - * that it is being processed, calls the given callback function, and - * then restores the original path. - * - * This is used to identify the path of the property being processed - * when reporting type errors. - * - * @param {string} component - * @param {function} callback - * @returns {*} - */ - withPath(component, callback) { - this.path.push(component); - try { - return callback(); - } finally { - this.path.pop(); - } - } -} - -/** - * Holds methods that run the actual implementation of the extension APIs. These - * methods are only called if the extension API invocation matches the signature - * as defined in the schema. Otherwise an error is reported to the context. - */ -class InjectionContext extends Context { - constructor(params) { - super(params, CONTEXT_FOR_INJECTION); - } - - /** - * 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<string>} allowedContexts A list of additional contexts in which - * this API should be available. May include any of: - * "main" - The main chrome browser process. - * "addon" - An addon process. - * "content" - A content process. - * @returns {boolean} Whether the API should be injected. - */ - shouldInject(namespace, name, allowedContexts) { - throw new Error("Not implemented"); - } - - /** - * Generate the implementation for `namespace`.`name`. - * - * @abstract - * @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. - */ - 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. - */ -const FORMATS = { - url(string, context) { - let url = new URL(string).href; - - if (!context.checkLoadURL(url)) { - throw new Error(`Access denied for URL ${url}`); - } - return url; - }, - - relativeUrl(string, context) { - if (!context.url) { - // If there's no context URL, return relative URLs unresolved, and - // skip security checks for them. - try { - new URL(string); - } catch (e) { - return string; - } - } - - let url = new URL(string, context.url).href; - - if (!context.checkLoadURL(url)) { - throw new Error(`Access denied for URL ${url}`); - } - return url; - }, - - strictRelativeUrl(string, context) { - // Do not accept a string which resolves as an absolute URL, or any - // protocol-relative URL. - if (!string.startsWith("//")) { - try { - new URL(string); - } catch (e) { - return FORMATS.relativeUrl(string, context); - } - } - - throw new SyntaxError(`String ${JSON.stringify(string)} must be a relative URL`); - }, - - contentSecurityPolicy(string, context) { - let error = contentPolicyService.validateAddonCSP(string); - if (error != null) { - throw new SyntaxError(error); - } - return string; - }, - - date(string, context) { - // A valid ISO 8601 timestamp. - const PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/; - if (!PATTERN.test(string)) { - throw new Error(`Invalid date string ${string}`); - } - // Our pattern just checks the format, we could still have invalid - // values (e.g., month=99 or month=02 and day=31). Let the Date - // constructor do the dirty work of validating. - if (isNaN(new Date(string))) { - throw new Error(`Invalid date string ${string}`); - } - return string; - }, -}; - -// Schema files contain namespaces, and each namespace contains types, -// properties, functions, and events. An Entry is a base class for -// types, properties, functions, and events. -class Entry { - constructor(schema = {}) { - /** - * If set to any value which evaluates as true, this entry is - * deprecated, and any access to it will result in a deprecation - * warning being logged to the browser console. - * - * If the value is a string, it will be appended to the deprecation - * message. If it contains the substring "${value}", it will be - * replaced with a string representation of the value being - * processed. - * - * If the value is any other truthy value, a generic deprecation - * message will be emitted. - */ - this.deprecated = false; - if ("deprecated" in schema) { - this.deprecated = schema.deprecated; - } - - /** - * @property {string} [preprocessor] - * If set to a string value, and a preprocessor of the same is - * defined in the validation context, it will be applied to this - * value prior to any normalization. - */ - this.preprocessor = schema.preprocess || null; - - /** - * @property {string} [postprocessor] - * If set to a string value, and a postprocessor of the same is - * defined in the validation context, it will be applied to this - * value after any normalization. - */ - this.postprocessor = schema.postprocess || null; - - /** - * @property {Array<string>} allowedContexts A list of allowed contexts - * to consider before generating the API. - * These are not parsed by the schema, but passed to `shouldInject`. - */ - this.allowedContexts = schema.allowedContexts || []; - } - - /** - * Preprocess the given value with the preprocessor declared in - * `preprocessor`. - * - * @param {*} value - * @param {Context} context - * @returns {*} - */ - preprocess(value, context) { - if (this.preprocessor) { - return context.preprocessors[this.preprocessor](value, context); - } - return value; - } - - /** - * Postprocess the given result with the postprocessor declared in - * `postprocessor`. - * - * @param {object} result - * @param {Context} context - * @returns {object} - */ - postprocess(result, context) { - if (result.error || !this.postprocessor) { - return result; - } - - let value = context.postprocessors[this.postprocessor](result.value, context); - return {value}; - } - - /** - * Logs a deprecation warning for this entry, based on the value of - * its `deprecated` property. - * - * @param {Context} context - * @param {value} [value] - */ - logDeprecation(context, value = null) { - let message = "This property is deprecated"; - if (typeof(this.deprecated) == "string") { - message = this.deprecated; - if (message.includes("${value}")) { - try { - value = JSON.stringify(value); - } catch (e) { - value = String(value); - } - message = message.replace(/\$\{value\}/g, () => value); - } - } - - context.logError(context.makeError(message)); - } - - /** - * Checks whether the entry is deprecated and, if so, logs a - * deprecation message. - * - * @param {Context} context - * @param {value} [value] - */ - checkDeprecated(context, value = null) { - if (this.deprecated) { - this.logDeprecation(context, value); - } - } - - /** - * 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 {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(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 { - /** - * @property {Array<string>} EXTRA_PROPERTIES - * An array of extra properties which may be present for - * schemas of this type. - */ - 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 {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 = []) { - 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. - * - * @param {object} schema - * A JSON schema object. - * @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>} [extra] - * An array of extra property names which are valid for this - * schema in the current context. - * @throws {Error} - * An error describing the first invalid property found in the - * schema object. - */ - static checkSchemaProperties(schema, path, extra = []) { - let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]); - - for (let prop of Object.keys(schema)) { - if (!allowedSet.has(prop)) { - throw new Error(`Internal error: Namespace ${path.join(".")} has invalid type property "${prop}" in type "${schema.id || JSON.stringify(schema)}"`); - } - } - } - - // Takes a value, checks that it has the correct type, and returns a - // "normalized" version of the value. The normalized version will - // include "nulls" in place of omitted optional properties. The - // result of this function is either {error: "Some type error"} or - // {value: <normalized-value>}. - normalize(value, context) { - return context.error("invalid type"); - } - - // Unlike normalize, this function does a shallow check to see if - // |baseType| (one of the possible getValueBaseType results) is - // valid for this type. It returns true or false. It's used to fill - // in optional arguments to functions before actually type checking - - checkBaseType(baseType) { - return false; - } - - // Helper method that simply relies on checkBaseType to implement - // normalize. Subclasses can choose to use it or not. - normalizeBase(type, value, context) { - if (this.checkBaseType(getValueBaseType(value))) { - this.checkDeprecated(context, value); - return {value: this.preprocess(value, context)}; - } - - let choice; - if (/^[aeiou]/.test(type)) { - choice = `be an ${type} value`; - } else { - choice = `be a ${type} value`; - } - - return context.error(`Expected ${type} instead of ${JSON.stringify(value)}`, - choice); - } -} - -// Type that allows any value. -class AnyType extends Type { - normalize(value, context) { - this.checkDeprecated(context, value); - return this.postprocess({value}, context); - } - - checkBaseType(baseType) { - return true; - } -} - -// An untagged union type. -class ChoiceType extends Type { - static get EXTRA_PROPERTIES() { - return ["choices", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(schema, path, extraProperties = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - let choices = schema.choices.map(t => Schemas.parseSchema(t, path)); - return new this(schema, choices); - } - - constructor(schema, choices) { - super(schema); - this.choices = choices; - } - - extend(type) { - this.choices.push(...type.choices); - - return this; - } - - normalize(value, context) { - this.checkDeprecated(context, value); - - let error; - let {choices, result} = context.withChoices(() => { - for (let choice of this.choices) { - let r = choice.normalize(value, context); - if (!r.error) { - return r; - } - - error = r; - } - }); - - if (result) { - return result; - } - if (choices.length <= 1) { - return error; - } - - let n = choices.length - 1; - choices[n] = `or ${choices[n]}`; - - let message = `Value must either: ${choices.join(", ")}`; - - return context.error(message, null); - } - - checkBaseType(baseType) { - return this.choices.some(t => t.checkBaseType(baseType)); - } -} - -// 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 = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - let ref = schema.$ref; - let ns = path[0]; - if (ref.includes(".")) { - [ns, ref] = ref.split("."); - } - return new this(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) { - super(schema); - this.namespaceName = namespaceName; - this.reference = reference; - } - - get targetType() { - let ns = Schemas.namespaces.get(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) { - this.checkDeprecated(context, value); - return this.targetType.normalize(value, context); - } - - checkBaseType(baseType) { - return this.targetType.checkBaseType(baseType); - } -} - -class StringType extends Type { - static get EXTRA_PROPERTIES() { - return ["enum", "minLength", "maxLength", "pattern", "format", - ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(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 => { - if (typeof(e) == "object") { - return e.name; - } - return e; - }); - } - - let pattern = null; - if (schema.pattern) { - try { - pattern = parsePattern(schema.pattern); - } catch (e) { - throw new Error(`Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}`); - } - } - - let format = null; - if (schema.format) { - if (!(schema.format in FORMATS)) { - throw new Error(`Internal error: Invalid string format ${schema.format}`); - } - format = FORMATS[schema.format]; - } - return new this(schema, enumeration, - schema.minLength || 0, - schema.maxLength || Infinity, - pattern, - format); - } - - constructor(schema, enumeration, minLength, maxLength, pattern, format) { - super(schema); - this.enumeration = enumeration; - this.minLength = minLength; - this.maxLength = maxLength; - this.pattern = pattern; - this.format = format; - } - - normalize(value, context) { - let r = this.normalizeBase("string", value, context); - if (r.error) { - return r; - } - value = r.value; - - if (this.enumeration) { - if (this.enumeration.includes(value)) { - return this.postprocess({value}, context); - } - - let choices = this.enumeration.map(JSON.stringify).join(", "); - - return context.error(`Invalid enumeration value ${JSON.stringify(value)}`, - `be one of [${choices}]`); - } - - if (value.length < this.minLength) { - return context.error(`String ${JSON.stringify(value)} is too short (must be ${this.minLength})`, - `be longer than ${this.minLength}`); - } - if (value.length > this.maxLength) { - return context.error(`String ${JSON.stringify(value)} is too long (must be ${this.maxLength})`, - `be shorter than ${this.maxLength}`); - } - - if (this.pattern && !this.pattern.test(value)) { - return context.error(`String ${JSON.stringify(value)} must match ${this.pattern}`, - `match the pattern ${this.pattern.toSource()}`); - } - - if (this.format) { - try { - r.value = this.format(r.value, context); - } catch (e) { - return context.error(String(e), `match the format "${this.format.name}"`); - } - } - - return r; - } - - checkBaseType(baseType) { - return baseType == "string"; - } - - inject(path, name, dest, context) { - if (this.enumeration) { - 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]; - } - - static parseSchema(schema, path, extraProperties = []) { - if ("functions" in schema) { - return SubModuleType.parseSchema(schema, path, extraProperties); - } - - if (!("$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, - ["unsupported", "onError", "permissions", ...extraProps]), - optional: schema.optional || false, - unsupported: schema.unsupported || false, - onError: schema.onError || null, - }; - }; - - // Parse explicit "properties" object. - let properties = Object.create(null); - for (let propName of Object.keys(schema.properties || {})) { - properties[propName] = parseProperty(schema.properties[propName], ["optional"]); - } - - // Parse regexp properties from "patternProperties" object. - let patternProperties = []; - for (let propName of Object.keys(schema.patternProperties || {})) { - let pattern; - try { - pattern = parsePattern(propName); - } catch (e) { - throw new Error(`Internal error: Invalid property pattern ${JSON.stringify(propName)}`); - } - - patternProperties.push({ - pattern, - type: parseProperty(schema.patternProperties[propName]), - }); - } - - // Parse "additionalProperties" schema. - let additionalProperties = null; - if (schema.additionalProperties) { - let type = schema.additionalProperties; - if (type === true) { - type = {"type": "any"}; - } - - additionalProperties = Schemas.parseSchema(type, path); - } - - return new this(schema, properties, additionalProperties, patternProperties, schema.isInstanceOf || null); - } - - constructor(schema, properties, additionalProperties, patternProperties, isInstanceOf) { - super(schema); - this.properties = properties; - this.additionalProperties = additionalProperties; - this.patternProperties = patternProperties; - this.isInstanceOf = isInstanceOf; - } - - extend(type) { - for (let key of Object.keys(type.properties)) { - if (key in this.properties) { - throw new Error(`InternalError: Attempt to extend an object with conflicting property "${key}"`); - } - this.properties[key] = type.properties[key]; - } - - this.patternProperties.push(...type.patternProperties); - - return this; - } - - checkBaseType(baseType) { - return baseType == "object"; - } - - /** - * Extracts the enumerable properties of the given object, including - * function properties which would normally be omitted by X-ray - * wrappers. - * - * @param {object} value - * @param {Context} context - * The current parse context. - * @returns {object} - * An object with an `error` or `value` property. - */ - extractProperties(value, context) { - // |value| should be a JS Xray wrapping an object in the - // extension compartment. This works well except when we need to - // access callable properties on |value| since JS Xrays don't - // support those. To work around the problem, we verify that - // |value| is a plain JS object (i.e., not anything scary like a - // Proxy). Then we copy the properties out of it into a normal - // object using a waiver wrapper. - - let klass = Cu.getClassName(value, true); - if (klass != "Object") { - throw context.error(`Expected a plain JavaScript object, got a ${klass}`, - `be a plain JavaScript object`); - } - - let properties = Object.create(null); - - let waived = Cu.waiveXrays(value); - for (let prop of Object.getOwnPropertyNames(waived)) { - let desc = Object.getOwnPropertyDescriptor(waived, prop); - if (desc.get || desc.set) { - throw context.error("Objects cannot have getters or setters on properties", - "contain no getter or setter properties"); - } - // Chrome ignores non-enumerable properties. - if (desc.enumerable) { - properties[prop] = Cu.unwaiveXrays(desc.value); - } - } - - return properties; - } - - checkProperty(context, prop, propType, result, properties, remainingProps) { - let {type, optional, unsupported, onError} = propType; - let error = null; - - if (unsupported) { - if (prop in properties) { - error = context.error(`Property "${prop}" is unsupported by Firefox`, - `not contain an unsupported "${prop}" property`); - } - } else if (prop in properties) { - if (optional && (properties[prop] === null || properties[prop] === undefined)) { - result[prop] = null; - } else { - let r = context.withPath(prop, () => type.normalize(properties[prop], context)); - if (r.error) { - error = r; - } else { - result[prop] = r.value; - properties[prop] = r.value; - } - } - remainingProps.delete(prop); - } else if (!optional) { - error = context.error(`Property "${prop}" is required`, - `contain the required "${prop}" property`); - } else if (optional !== "omit-key-if-missing") { - result[prop] = null; - } - - if (error) { - if (onError == "warn") { - context.logError(error.error); - } else if (onError != "ignore") { - throw error; - } - - result[prop] = null; - } - } - - normalize(value, context) { - try { - let v = this.normalizeBase("object", value, context); - if (v.error) { - return v; - } - value = v.value; - - if (this.isInstanceOf) { - if (Object.keys(this.properties).length || - this.patternProperties.length || - !(this.additionalProperties instanceof AnyType)) { - throw new Error("InternalError: isInstanceOf can only be used with objects that are otherwise unrestricted"); - } - - if (!instanceOf(value, this.isInstanceOf)) { - return context.error(`Object must be an instance of ${this.isInstanceOf}`, - `be an instance of ${this.isInstanceOf}`); - } - - // This is kind of a hack, but we can't normalize things that - // aren't JSON, so we just return them. - return this.postprocess({value}, context); - } - - let properties = this.extractProperties(value, context); - let remainingProps = new Set(Object.keys(properties)); - - let result = {}; - for (let prop of Object.keys(this.properties)) { - this.checkProperty(context, prop, this.properties[prop], result, - properties, remainingProps); - } - - for (let prop of Object.keys(properties)) { - for (let {pattern, type} of this.patternProperties) { - if (pattern.test(prop)) { - this.checkProperty(context, prop, type, result, - properties, remainingProps); - } - } - } - - if (this.additionalProperties) { - for (let prop of remainingProps) { - let type = this.additionalProperties; - let r = context.withPath(prop, () => type.normalize(properties[prop], context)); - if (r.error) { - return r; - } - result[prop] = r.value; - } - } else if (remainingProps.size == 1) { - return context.error(`Unexpected property "${[...remainingProps]}"`, - `not contain an unexpected "${[...remainingProps]}" property`); - } else if (remainingProps.size) { - let props = [...remainingProps].sort().join(", "); - return context.error(`Unexpected properties: ${props}`, - `not contain the unexpected properties [${props}]`); - } - - return this.postprocess({value: result}, context); - } catch (e) { - if (e.error) { - return e; - } - throw e; - } - } -} - -// 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 = []) { - 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.map(fun => Schemas.parseFunction(path, fun)); - - return new this(functions); - } - - constructor(functions) { - super(); - this.functions = functions; - } -}; - -class NumberType extends Type { - normalize(value, context) { - let r = this.normalizeBase("number", value, context); - if (r.error) { - return r; - } - - if (isNaN(r.value) || !Number.isFinite(r.value)) { - return context.error("NaN and infinity are not valid", - "be a finite number"); - } - - return r; - } - - checkBaseType(baseType) { - return baseType == "number" || baseType == "integer"; - } -} - -class IntegerType extends Type { - static get EXTRA_PROPERTIES() { - return ["minimum", "maximum", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(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; - this.maximum = maximum; - } - - normalize(value, context) { - let r = this.normalizeBase("integer", value, context); - if (r.error) { - return r; - } - value = r.value; - - // Ensure it's between -2**31 and 2**31-1 - if (!Number.isSafeInteger(value)) { - return context.error("Integer is out of range", - "be a valid 32 bit signed integer"); - } - - if (value < this.minimum) { - return context.error(`Integer ${value} is too small (must be at least ${this.minimum})`, - `be at least ${this.minimum}`); - } - if (value > this.maximum) { - return context.error(`Integer ${value} is too big (must be at most ${this.maximum})`, - `be no greater than ${this.maximum}`); - } - - return this.postprocess(r, context); - } - - checkBaseType(baseType) { - return baseType == "integer"; - } -} - -class BooleanType extends Type { - normalize(value, context) { - return this.normalizeBase("boolean", value, context); - } - - checkBaseType(baseType) { - return baseType == "boolean"; - } -} - -class ArrayType extends Type { - static get EXTRA_PROPERTIES() { - return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(schema, path, extraProperties = []) { - this.checkSchemaProperties(schema, path, extraProperties); - - let items = Schemas.parseSchema(schema.items, path); - - return new this(schema, items, schema.minItems || 0, schema.maxItems || Infinity); - } - - constructor(schema, itemType, minItems, maxItems) { - super(schema); - this.itemType = itemType; - this.minItems = minItems; - this.maxItems = maxItems; - } - - normalize(value, context) { - let v = this.normalizeBase("array", value, context); - if (v.error) { - return v; - } - value = v.value; - - let result = []; - for (let [i, element] of value.entries()) { - element = context.withPath(String(i), () => this.itemType.normalize(element, context)); - if (element.error) { - return element; - } - result.push(element.value); - } - - if (result.length < this.minItems) { - return context.error(`Array requires at least ${this.minItems} items; you have ${result.length}`, - `have at least ${this.minItems} items`); - } - - if (result.length > this.maxItems) { - return context.error(`Array requires at most ${this.maxItems} items; you have ${result.length}`, - `have at most ${this.maxItems} items`); - } - - return this.postprocess({value: result}, context); - } - - checkBaseType(baseType) { - return baseType == "array"; - } -} - -class FunctionType extends Type { - static get EXTRA_PROPERTIES() { - return ["parameters", "async", "returns", ...super.EXTRA_PROPERTIES]; - } - - static parseSchema(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"]), - name: param.name, - optional: param.optional == null ? isCallback : param.optional, - default: param.default == undefined ? null : param.default, - }); - } - } - if (isExpectingCallback) { - throw new Error(`Internal error: Expected a callback parameter with name ${schema.async}`); - } - - let hasAsyncCallback = false; - if (isAsync) { - hasAsyncCallback = (parameters && - parameters.length && - parameters[parameters.length - 1].name == schema.async); - - if (schema.returns) { - throw new Error("Internal error: Async functions must not have return values."); - } - if (schema.allowAmbiguousOptionalArguments && !hasAsyncCallback) { - throw new Error("Internal error: Async functions with ambiguous arguments must declare the callback as the last parameter"); - } - } - - return new this(schema, parameters, isAsync, hasAsyncCallback); - } - - constructor(schema, parameters, isAsync, hasAsyncCallback) { - super(schema); - this.parameters = parameters; - this.isAsync = isAsync; - this.hasAsyncCallback = hasAsyncCallback; - } - - normalize(value, context) { - return this.normalizeBase("function", value, context); - } - - checkBaseType(baseType) { - return baseType == "function"; - } -} - -// Represents a "property" defined in a schema namespace with a -// particular value. Essentially this is a constant. -class ValueProperty extends Entry { - constructor(schema, name, value) { - super(schema); - this.name = name; - this.value = value; - } - - inject(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) { - super(schema); - this.namespaceName = namespaceName; - this.name = name; - this.type = type; - this.writable = writable; - } - - throwError(context, msg) { - throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`); - } - - inject(path, name, dest, context) { - if (this.unsupported) { - return; - } - - let apiImpl = context.getImplementation(path.join("."), name); - - let getStub = () => { - this.checkDeprecated(context); - 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); - } - - apiImpl.setProperty(normalized.value); - }; - - desc.set = Cu.exportFunction(setStub, dest); - } - - Object.defineProperty(dest, name, desc); - } -} - -class SubModuleProperty extends Entry { - // A SubModuleProperty represents a tree of objects and properties - // to expose to an extension. Currently we support only a limited - // 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, name, namespaceName, reference, properties) { - super(schema); - this.name = name; - this.namespaceName = namespaceName; - this.reference = reference; - this.properties = properties; - } - - inject(path, name, dest, context) { - 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 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)) { - fun.inject(subpath, fun.name, obj, context); - } - } - - // 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) { - super(schema); - this.path = path; - this.name = name; - this.parameters = parameters; - this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments; - } - - throwError(context, msg) { - throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`); - } - - checkParameters(args, context) { - let fixedArgs = []; - - // First we create a new array, fixedArgs, that is the same as - // |args| but with default values in place of omitted optional parameters. - let check = (parameterIndex, argIndex) => { - if (parameterIndex == this.parameters.length) { - if (argIndex == args.length) { - return true; - } - return false; - } - - let parameter = this.parameters[parameterIndex]; - if (parameter.optional) { - // Try skipping it. - fixedArgs[parameterIndex] = parameter.default; - if (check(parameterIndex + 1, argIndex)) { - return true; - } - } - - if (argIndex == args.length) { - return false; - } - - let arg = args[argIndex]; - if (!parameter.type.checkBaseType(getValueBaseType(arg))) { - // For Chrome compatibility, use the default value if null or undefined - // is explicitly passed but is not a valid argument in this position. - if (parameter.optional && (arg === null || arg === undefined)) { - fixedArgs[parameterIndex] = Cu.cloneInto(parameter.default, global); - } else { - return false; - } - } else { - fixedArgs[parameterIndex] = arg; - } - - return check(parameterIndex + 1, argIndex + 1); - }; - - if (this.allowAmbiguousOptionalArguments) { - // When this option is set, it's up to the implementation to - // parse arguments. - // The last argument for asynchronous methods is either a function or null. - // This is specifically done for runtime.sendMessage. - if (this.hasAsyncCallback && typeof(args[args.length - 1]) != "function") { - args.push(null); - } - return args; - } - let success = check(0, 0); - if (!success) { - this.throwError(context, "Incorrect argument types"); - } - - // Now we normalize (and fully type check) all non-omitted arguments. - fixedArgs = fixedArgs.map((arg, parameterIndex) => { - if (arg === null) { - return null; - } - let parameter = this.parameters[parameterIndex]; - let r = parameter.type.normalize(arg, context); - if (r.error) { - this.throwError(context, `Type error for parameter ${parameter.name} (${r.error})`); - } - return r.value; - }); - - return fixedArgs; - } -} - -// Represents a "function" defined in a schema namespace. -class FunctionEntry extends CallEntry { - constructor(schema, path, name, type, unsupported, allowAmbiguousOptionalArguments, returns, permissions) { - super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments); - this.unsupported = unsupported; - this.returns = returns; - this.permissions = permissions; - - this.isAsync = type.isAsync; - this.hasAsyncCallback = type.hasAsyncCallback; - } - - inject(path, name, dest, context) { - if (this.unsupported) { - return; - } - - if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) { - return; - } - - exportLazyGetter(dest, name, () => { - let apiImpl = context.getImplementation(path.join("."), 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; - this.unsupported = unsupported; - this.permissions = permissions; - } - - checkListener(listener, context) { - let r = this.type.normalize(listener, context); - if (r.error) { - this.throwError(context, "Invalid listener"); - } - return r.value; - } - - inject(path, name, dest, context) { - if (this.unsupported) { - return; - } - - if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) { - return; - } - - exportLazyGetter(dest, name, () => { - let apiImpl = context.getImplementation(path.join("."), 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 hasStub = (listener) => { - listener = this.checkListener(listener, context); - return apiImpl.hasListener(listener); - }; - - let obj = Cu.createObjectIn(dest); - - 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, - integer: IntegerType, - number: NumberType, - object: ObjectType, - string: StringType, -})); - -this.Schemas = { - initialized: false, - - // Maps a schema URL to the JSON contained in that schema file. This - // is useful for sending the JSON across processes. - schemaJSON: new Map(), - - // 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); - }, - - parseSchema(schema, path, extraProperties = []) { - let allowedProperties = new Set(extraProperties); - - if ("choices" in schema) { - return ChoiceType.parseSchema(schema, path, allowedProperties); - } else if ("$ref" in schema) { - return RefType.parseSchema(schema, path, allowedProperties); - } - - if (!("type" in schema)) { - throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`); - } - - allowedProperties.add("type"); - - let type = TYPES[schema.type]; - if (!type) { - throw new Error(`Unexpected type ${schema.type}`); - } - return type.parseSchema(schema, path, allowedProperties); - }, - - parseFunction(path, fun) { - let f = new FunctionEntry(fun, path, fun.name, - this.parseSchema(fun, path, - ["name", "unsupported", "returns", - "permissions", - "allowAmbiguousOptionalArguments"]), - fun.unsupported || false, - fun.allowAmbiguousOptionalArguments || false, - fun.returns || null, - fun.permissions || null); - return f; - }, - - loadType(namespaceName, type) { - if ("$extend" in type) { - this.extendType(namespaceName, type); - } else { - this.register(namespaceName, type.id, this.parseSchema(type, [namespaceName], ["id"])); - } - }, - - extendType(namespaceName, type) { - let ns = Schemas.namespaces.get(namespaceName); - let targetType = ns && ns.get(type.$extend); - - // Only allow extending object and choices types for now. - if (targetType instanceof ObjectType) { - type.type = "object"; - } else 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 = this.parseSchema(type, [namespaceName], ["$extend"]); - if (parsed.constructor !== targetType.constructor) { - throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`); - } - - targetType.extend(parsed); - }, - - loadProperty(namespaceName, name, prop) { - if ("$ref" in prop) { - if (!prop.unsupported) { - this.register(namespaceName, name, new SubModuleProperty(prop, name, namespaceName, prop.$ref, - prop.properties || {})); - } - } else if ("value" in prop) { - this.register(namespaceName, name, new ValueProperty(prop, name, prop.value)); - } else { - // We ignore the "optional" attribute on properties since we - // don't inject anything here anyway. - let type = this.parseSchema(prop, [namespaceName], ["optional", "writable"]); - this.register(namespaceName, name, new TypeProperty(prop, namespaceName, name, type, prop.writable || false)); - } - }, - - loadFunction(namespaceName, fun) { - let f = this.parseFunction([namespaceName], fun); - this.register(namespaceName, fun.name, f); - }, - - loadEvent(namespaceName, event) { - let extras = event.extraParameters || []; - extras = extras.map(param => { - return { - type: this.parseSchema(param, [namespaceName], ["name", "optional", "default"]), - name: param.name, - optional: param.optional || false, - default: param.default == undefined ? null : param.default, - }; - }); - - // We ignore these properties for now. - /* eslint-disable no-unused-vars */ - let returns = event.returns; - let filters = event.filters; - /* eslint-enable no-unused-vars */ - - let type = this.parseSchema(event, [namespaceName], - ["name", "unsupported", "permissions", - "extraParameters", "returns", "filters"]); - - let e = new Event(event, [namespaceName], event.name, type, extras, - event.unsupported || false, - event.permissions || null); - this.register(namespaceName, event.name, e); - }, - - init() { - if (this.initialized) { - return; - } - this.initialized = true; - - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { - let data = Services.cpmm.initialProcessData; - let schemas = data["Extension:Schemas"]; - if (schemas) { - this.schemaJSON = schemas; - } - Services.cpmm.addMessageListener("Schema:Add", this); - } - - this.flushSchemas(); - }, - - receiveMessage(msg) { - switch (msg.name) { - case "Schema:Add": - this.schemaJSON.set(msg.data.url, msg.data.schema); - this.flushSchemas(); - break; - - case "Schema:Delete": - this.schemaJSON.delete(msg.data.url); - this.flushSchemas(); - break; - } - }, - - flushSchemas() { - XPCOMUtils.defineLazyGetter(this, "namespaces", - () => this.parseSchemas()); - }, - - parseSchemas() { - Object.defineProperty(this, "namespaces", { - enumerable: true, - configurable: true, - value: new Map(), - }); - - for (let json of this.schemaJSON.values()) { - try { - this.loadSchema(json); - } catch (e) { - Cu.reportError(e); - } - } - - return this.namespaces; - }, - - loadSchema(json) { - for (let namespace of json) { - let name = namespace.namespace; - - let types = namespace.types || []; - for (let type of types) { - this.loadType(name, type); - } - - let properties = namespace.properties || {}; - for (let propertyName of Object.keys(properties)) { - this.loadProperty(name, propertyName, properties[propertyName]); - } - - let functions = namespace.functions || []; - for (let fun of functions) { - this.loadFunction(name, fun); - } - - let events = namespace.events || []; - for (let event of events) { - this.loadEvent(name, event); - } - - let ns = this.namespaces.get(name); - ns.permissions = namespace.permissions || null; - ns.allowedContexts = namespace.allowedContexts || []; - ns.defaultContexts = namespace.defaultContexts || []; - } - }, - - load(url) { - if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) { - return readJSON(url).then(json => { - this.schemaJSON.set(url, json); - - let data = Services.ppmm.initialProcessData; - data["Extension:Schemas"] = this.schemaJSON; - - Services.ppmm.broadcastAsyncMessage("Schema:Add", {url, schema: json}); - - this.flushSchemas(); - }); - } - }, - - unload(url) { - this.schemaJSON.delete(url); - - let data = Services.ppmm.initialProcessData; - data["Extension:Schemas"] = this.schemaJSON; - - Services.ppmm.broadcastAsyncMessage("Schema:Delete", {url}); - - this.flushSchemas(); - }, - - /** - * 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.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); - - let createNamespace = ns => { - let obj = Cu.createObjectIn(dest); - - for (let [name, entry] of ns) { - let allowedContexts = entry.allowedContexts; - if (!allowedContexts.length) { - allowedContexts = ns.defaultContexts; - } - - if (context.shouldInject(ns.name, name, allowedContexts)) { - entry.inject([ns.name], name, obj, context); - } - } - - // Remove the namespace object if it is empty - 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 (!wrapperFuncs.shouldInject(ns.name, null, ns.allowedContexts)) { - continue; - } - - if (ns.name.includes(".")) { - let path = ns.name.split("."); - let leafName = path.pop(); - - 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 - * are reported to the given context. - * @returns {object} The normalized object. - */ - normalize(obj, typeName, context) { - let [namespaceName, prop] = typeName.split("."); - let ns = this.namespaces.get(namespaceName); - let type = ns.get(prop); - - return type.normalize(obj, new Context(context)); - }, -}; |