forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			3942 lines
		
	
	
	
		
			108 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			3942 lines
		
	
	
	
		
			108 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set sts=2 sw=2 et tw=80: */
 | |
| /* 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/. */
 | |
| 
 | |
| import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
 | |
| 
 | |
| var { DefaultMap, DefaultWeakMap } = ExtensionUtils;
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
 | |
|   NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
 | |
|   ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetter(
 | |
|   lazy,
 | |
|   "contentPolicyService",
 | |
|   "@mozilla.org/addons/content-policy;1",
 | |
|   "nsIAddonContentPolicy"
 | |
| );
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(
 | |
|   lazy,
 | |
|   "StartupCache",
 | |
|   () => lazy.ExtensionParent.StartupCache
 | |
| );
 | |
| 
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   lazy,
 | |
|   "treatWarningsAsErrors",
 | |
|   "extensions.webextensions.warnings-as-errors",
 | |
|   false
 | |
| );
 | |
| 
 | |
| const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content";
 | |
| const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged";
 | |
| 
 | |
| const MIN_MANIFEST_VERSION = 2;
 | |
| const MAX_MANIFEST_VERSION = 3;
 | |
| 
 | |
| const { DEBUG } = AppConstants;
 | |
| 
 | |
| const isParentProcess =
 | |
|   Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
 | |
| 
 | |
| function readJSON(url) {
 | |
|   return new Promise((resolve, reject) => {
 | |
|     lazy.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 = lazy.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);
 | |
|         }
 | |
|       }
 | |
|     );
 | |
|   });
 | |
| }
 | |
| 
 | |
| function stripDescriptions(json, stripThis = true) {
 | |
|   if (Array.isArray(json)) {
 | |
|     for (let i = 0; i < json.length; i++) {
 | |
|       if (typeof json[i] === "object" && json[i] !== null) {
 | |
|         json[i] = stripDescriptions(json[i]);
 | |
|       }
 | |
|     }
 | |
|     return json;
 | |
|   }
 | |
| 
 | |
|   let result = {};
 | |
| 
 | |
|   // Objects are handled much more efficiently, both in terms of memory and
 | |
|   // CPU, if they have the same shape as other objects that serve the same
 | |
|   // purpose. So, normalize the order of properties to increase the chances
 | |
|   // that the majority of schema objects wind up in large shape groups.
 | |
|   for (let key of Object.keys(json).sort()) {
 | |
|     if (stripThis && key === "description" && typeof json[key] === "string") {
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     if (typeof json[key] === "object" && json[key] !== null) {
 | |
|       result[key] = stripDescriptions(json[key], key !== "properties");
 | |
|     } else {
 | |
|       result[key] = json[key];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return result;
 | |
| }
 | |
| 
 | |
| function blobbify(json) {
 | |
|   // We don't actually use descriptions at runtime, and they make up about a
 | |
|   // third of the size of our structured clone data, so strip them before
 | |
|   // blobbifying.
 | |
|   json = stripDescriptions(json);
 | |
| 
 | |
|   return new StructuredCloneHolder("Schemas/blobbify", null, json);
 | |
| }
 | |
| 
 | |
| async function readJSONAndBlobbify(url) {
 | |
|   let json = await readJSON(url);
 | |
| 
 | |
|   return blobbify(json);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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 = ChromeUtils.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),
 | |
|   });
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Defines a lazily-instantiated property descriptor on the given
 | |
|  * object. Any security wrappers are waived on the object before the
 | |
|  * property is defined.
 | |
|  *
 | |
|  * 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
 | |
|  *        descriptor object. This will be called, and the property
 | |
|  *        descriptor installed on the object, the first time the
 | |
|  *        property is written or read. The function may return
 | |
|  *        undefined, which will cause the property to be deleted.
 | |
|  */
 | |
| function exportLazyProperty(object, prop, getter) {
 | |
|   object = ChromeUtils.waiveXrays(object);
 | |
| 
 | |
|   let redefine = obj => {
 | |
|     let desc = getter.call(obj);
 | |
|     getter = null;
 | |
| 
 | |
|     delete object[prop];
 | |
|     if (desc) {
 | |
|       let defaults = {
 | |
|         configurable: true,
 | |
|         enumerable: true,
 | |
|       };
 | |
| 
 | |
|       if (!desc.set && !desc.get) {
 | |
|         defaults.writable = true;
 | |
|       }
 | |
| 
 | |
|       Object.defineProperty(object, prop, Object.assign(defaults, desc));
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   Object.defineProperty(object, prop, {
 | |
|     enumerable: true,
 | |
|     configurable: true,
 | |
| 
 | |
|     get: Cu.exportFunction(function () {
 | |
|       redefine(this);
 | |
|       return object[prop];
 | |
|     }, object),
 | |
| 
 | |
|     set: Cu.exportFunction(function (value) {
 | |
|       redefine(this);
 | |
|       object[prop] = 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");
 | |
|   },
 | |
|   webRequestBlockingPermissionRequired(string, context) {
 | |
|     if (string === "blocking" && !context.hasPermission("webRequestBlocking")) {
 | |
|       throw new context.cloneScope.Error(
 | |
|         "Using webRequest.addListener with the " +
 | |
|           "blocking option requires the 'webRequestBlocking' permission."
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return string;
 | |
|   },
 | |
|   requireBackgroundServiceWorkerEnabled(value, context) {
 | |
|     if (WebExtensionPolicy.backgroundServiceWorkerEnabled) {
 | |
|       return value;
 | |
|     }
 | |
| 
 | |
|     // Add an error to the manifest validations and throw the
 | |
|     // same error.
 | |
|     const msg = "background.service_worker is currently disabled";
 | |
|     context.logError(context.makeError(msg));
 | |
|     throw new Error(msg);
 | |
|   },
 | |
| 
 | |
|   manifestVersionCheck(value, context) {
 | |
|     if (
 | |
|       value == 2 ||
 | |
|       (value == 3 &&
 | |
|         Services.prefs.getBoolPref("extensions.manifestV3.enabled", false))
 | |
|     ) {
 | |
|       return value;
 | |
|     }
 | |
|     const msg = `Unsupported manifest version: ${value}`;
 | |
|     context.logError(context.makeError(msg));
 | |
|     throw new Error(msg);
 | |
|   },
 | |
| 
 | |
|   webAccessibleMatching(value, context) {
 | |
|     // Ensure each object has at least one of matches or extension_ids array.
 | |
|     for (let obj of value) {
 | |
|       if (!obj.matches && !obj.extension_ids) {
 | |
|         const msg = `web_accessible_resources requires one of "matches" or "extension_ids"`;
 | |
|         context.logError(context.makeError(msg));
 | |
|         throw new Error(msg);
 | |
|       }
 | |
|     }
 | |
|     return value;
 | |
|   },
 | |
| };
 | |
| 
 | |
| // 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 type = typeof value;
 | |
|   switch (type) {
 | |
|     case "object":
 | |
|       if (value === null) {
 | |
|         return "null";
 | |
|       }
 | |
|       if (Array.isArray(value)) {
 | |
|         return "array";
 | |
|       }
 | |
|       break;
 | |
| 
 | |
|     case "number":
 | |
|       if (value % 1 === 0) {
 | |
|         return "integer";
 | |
|       }
 | |
|   }
 | |
|   return type;
 | |
| }
 | |
| 
 | |
| // 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,
 | |
|   "getImplementation",
 | |
|   "isPermissionRevokable",
 | |
|   "shouldInject",
 | |
| ];
 | |
| 
 | |
| // If the message is a function, call it and return the result.
 | |
| // Otherwise, assume it's a string.
 | |
| function forceString(msg) {
 | |
|   if (typeof msg === "function") {
 | |
|     return msg();
 | |
|   }
 | |
|   return msg;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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;
 | |
| 
 | |
|     if (typeof params.manifestVersion !== "number") {
 | |
|       throw new Error(
 | |
|         `Unexpected params.manifestVersion value: ${params.manifestVersion}`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this.path = [];
 | |
|     this.preprocessors = {
 | |
|       localize(value, context) {
 | |
|         return value;
 | |
|       },
 | |
|       ...params.preprocessors,
 | |
|     };
 | |
| 
 | |
|     this.postprocessors = POSTPROCESSORS;
 | |
|     this.isChromeCompat = params.isChromeCompat ?? false;
 | |
|     this.manifestVersion = params.manifestVersion;
 | |
| 
 | |
|     this.currentChoices = new Set();
 | |
|     this.choicePathIndex = 0;
 | |
| 
 | |
|     for (let method of overridableMethods) {
 | |
|       if (method in params) {
 | |
|         this[method] = params[method].bind(params);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   get choicePath() {
 | |
|     let path = this.path.slice(this.choicePathIndex);
 | |
|     return path.join(".");
 | |
|   }
 | |
| 
 | |
|   get cloneScope() {
 | |
|     return this.params.cloneScope || undefined;
 | |
|   }
 | |
| 
 | |
|   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.checkLoadURIWithPrincipal(
 | |
|         this.principal,
 | |
|         Services.io.newURI(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;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Checks whether the given permission can be dynamically revoked or
 | |
|    * granted.
 | |
|    *
 | |
|    * @param {string} permission
 | |
|    *        The name of the permission to check.
 | |
|    *
 | |
|    * @returns {boolean} True if the given permission is revokable.
 | |
|    */
 | |
|   isPermissionRevokable(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 | Function} errorMessage
 | |
|    *        The error message which will be displayed when this is the
 | |
|    *        only possible matching schema. If a function is passed, it
 | |
|    *        will be evaluated when the error string is first needed, and
 | |
|    *        must return a string.
 | |
|    * @param {string | Function} 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.
 | |
|    * @param {boolean} [warning = false]
 | |
|    *        If true, make message prefixed `Warning`. If false, make message
 | |
|    *        prefixed `Error`
 | |
|    * @returns {object}
 | |
|    */
 | |
|   error(errorMessage, choicesMessage = undefined, warning = false) {
 | |
|     if (choicesMessage !== null) {
 | |
|       let { choicePath } = this;
 | |
|       if (choicePath) {
 | |
|         choicesMessage = `.${choicePath} must ${choicesMessage}`;
 | |
|       }
 | |
| 
 | |
|       this.currentChoices.add(choicesMessage);
 | |
|     }
 | |
| 
 | |
|     if (this.currentTarget) {
 | |
|       let { currentTarget } = this;
 | |
|       return {
 | |
|         error: () =>
 | |
|           `${
 | |
|             warning ? "Warning" : "Error"
 | |
|           } processing ${currentTarget}: ${forceString(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
 | |
|    * @param {object} [options]
 | |
|    * @param {boolean} [options.warning = false]
 | |
|    * @returns {Error}
 | |
|    */
 | |
|   makeError(message, { warning = false } = {}) {
 | |
|     let error = forceString(this.error(message, null, warning).error);
 | |
|     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) {
 | |
|     if (this.cloneScope) {
 | |
|       Cu.reportError(
 | |
|         // Error objects logged using Cu.reportError are not associated
 | |
|         // to the related innerWindowID. This results in a leaked docshell
 | |
|         // since consoleService cannot release the error object when the
 | |
|         // extension global is destroyed.
 | |
|         typeof error == "string" ? error : String(error),
 | |
|         // Report the error with the appropriate stack trace when the
 | |
|         // is related to an actual extension global (instead of being
 | |
|         // related to a manifest validation).
 | |
|         this.principal && ChromeUtils.getCallerLocation(this.principal)
 | |
|       );
 | |
|     } else {
 | |
|       Cu.reportError(error);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Logs a warning. An error might be thrown when we treat warnings as errors.
 | |
|    *
 | |
|    * @param {string} warningMessage
 | |
|    */
 | |
|   logWarning(warningMessage) {
 | |
|     let error = this.makeError(warningMessage, { warning: true });
 | |
|     this.logError(error);
 | |
| 
 | |
|     if (lazy.treatWarningsAsErrors) {
 | |
|       // This pref is false by default, and true by default in tests to
 | |
|       // discourage the use of deprecated APIs in our unit tests.
 | |
|       // If a warning is an expected part of a test, temporarily set the pref
 | |
|       // to false, e.g. with the ExtensionTestUtils.failOnSchemaWarnings helper.
 | |
|       Services.console.logStringMessage(
 | |
|         "Treating warning as error because the preference " +
 | |
|           "extensions.webextensions.warnings-as-errors is set to true"
 | |
|       );
 | |
|       if (typeof error === "string") {
 | |
|         error = new Error(error);
 | |
|       }
 | |
|       throw 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 };
 | |
|     } finally {
 | |
|       this.currentChoices = currentChoices;
 | |
|       this.choicePathIndex = choicePathIndex;
 | |
| 
 | |
|       if (choices.size == 1) {
 | |
|         for (let choice of choices) {
 | |
|           currentChoices.add(choice);
 | |
|         }
 | |
|       } else if (choices.size) {
 | |
|         this.error(null, () => {
 | |
|           let array = Array.from(choices, forceString);
 | |
|           let n = array.length - 1;
 | |
|           array[n] = `or ${array[n]}`;
 | |
| 
 | |
|           return `must either [${array.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();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   matchManifestVersion(entry) {
 | |
|     let { manifestVersion } = this;
 | |
|     return (
 | |
|       manifestVersion >= entry.min_manifest_version &&
 | |
|       manifestVersion <= entry.max_manifest_version
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Represents a schema entry to be injected into an object. Handles the
 | |
|  * injection, revocation, and permissions of said entry.
 | |
|  *
 | |
|  * @param {InjectionContext} context
 | |
|  *        The injection context for the entry.
 | |
|  * @param {Entry} entry
 | |
|  *        The entry to inject.
 | |
|  * @param {object} parentObject
 | |
|  *        The object into which to inject this entry.
 | |
|  * @param {string} name
 | |
|  *        The property name at which to inject this entry.
 | |
|  * @param {Array<string>} path
 | |
|  *        The full path from the root entry to this entry.
 | |
|  * @param {Entry} parentEntry
 | |
|  *        The parent entry for the injected entry.
 | |
|  */
 | |
| class InjectionEntry {
 | |
|   constructor(context, entry, parentObj, name, path, parentEntry) {
 | |
|     this.context = context;
 | |
|     this.entry = entry;
 | |
|     this.parentObj = parentObj;
 | |
|     this.name = name;
 | |
|     this.path = path;
 | |
|     this.parentEntry = parentEntry;
 | |
| 
 | |
|     this.injected = null;
 | |
|     this.lazyInjected = null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @property {Array<string>} allowedContexts
 | |
|    *        The list of allowed contexts into which the entry may be
 | |
|    *        injected.
 | |
|    */
 | |
|   get allowedContexts() {
 | |
|     let { allowedContexts } = this.entry;
 | |
|     if (allowedContexts.length) {
 | |
|       return allowedContexts;
 | |
|     }
 | |
|     return this.parentEntry.defaultContexts;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @property {boolean} isRevokable
 | |
|    *        Returns true if this entry may be dynamically injected or
 | |
|    *        revoked based on its permissions.
 | |
|    */
 | |
|   get isRevokable() {
 | |
|     return (
 | |
|       this.entry.permissions &&
 | |
|       this.entry.permissions.some(perm =>
 | |
|         this.context.isPermissionRevokable(perm)
 | |
|       )
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @property {boolean} hasPermission
 | |
|    *        Returns true if the injection context currently has the
 | |
|    *        appropriate permissions to access this entry.
 | |
|    */
 | |
|   get hasPermission() {
 | |
|     return (
 | |
|       !this.entry.permissions ||
 | |
|       this.entry.permissions.some(perm => this.context.hasPermission(perm))
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @property {boolean} shouldInject
 | |
|    *        Returns true if this entry should be injected in the given
 | |
|    *        context, without respect to permissions.
 | |
|    */
 | |
|   get shouldInject() {
 | |
|     return (
 | |
|       this.context.matchManifestVersion(this.entry) &&
 | |
|       this.context.shouldInject(
 | |
|         this.path.join("."),
 | |
|         this.name,
 | |
|         this.allowedContexts
 | |
|       )
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Revokes this entry, removing its property from its parent object,
 | |
|    * and invalidating its wrappers.
 | |
|    */
 | |
|   revoke() {
 | |
|     if (this.lazyInjected) {
 | |
|       this.lazyInjected = false;
 | |
|     } else if (this.injected) {
 | |
|       if (this.injected.revoke) {
 | |
|         this.injected.revoke();
 | |
|       }
 | |
| 
 | |
|       try {
 | |
|         let unwrapped = ChromeUtils.waiveXrays(this.parentObj);
 | |
|         delete unwrapped[this.name];
 | |
|       } catch (e) {
 | |
|         Cu.reportError(e);
 | |
|       }
 | |
| 
 | |
|       let { value } = this.injected.descriptor;
 | |
|       if (value) {
 | |
|         this.context.revokeChildren(value);
 | |
|       }
 | |
| 
 | |
|       this.injected = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns a property descriptor object for this entry, if it should
 | |
|    * be injected, or undefined if it should not.
 | |
|    *
 | |
|    * @returns {object?}
 | |
|    *        A property descriptor object, or undefined if the property
 | |
|    *        should be removed.
 | |
|    */
 | |
|   getDescriptor() {
 | |
|     this.lazyInjected = false;
 | |
| 
 | |
|     if (this.injected) {
 | |
|       let path = [...this.path, this.name];
 | |
|       throw new Error(
 | |
|         `Attempting to re-inject already injected entry: ${path.join(".")}`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (!this.shouldInject) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (this.isRevokable) {
 | |
|       this.context.pendingEntries.add(this);
 | |
|     }
 | |
| 
 | |
|     if (!this.hasPermission) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.injected = this.entry.getDescriptor(this.path, this.context);
 | |
|     if (!this.injected) {
 | |
|       return undefined;
 | |
|     }
 | |
| 
 | |
|     return this.injected.descriptor;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Injects a lazy property descriptor into the parent object which
 | |
|    * checks permissions and eligibility for injection the first time it
 | |
|    * is accessed.
 | |
|    */
 | |
|   lazyInject() {
 | |
|     if (this.lazyInjected || this.injected) {
 | |
|       let path = [...this.path, this.name];
 | |
|       throw new Error(
 | |
|         `Attempting to re-lazy-inject already injected entry: ${path.join(".")}`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this.lazyInjected = true;
 | |
|     exportLazyProperty(this.parentObj, this.name, () => {
 | |
|       if (this.lazyInjected) {
 | |
|         return this.getDescriptor();
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Injects or revokes this entry if its current state does not match
 | |
|    * the context's current permissions.
 | |
|    */
 | |
|   permissionsChanged() {
 | |
|     if (this.injected) {
 | |
|       this.maybeRevoke();
 | |
|     } else {
 | |
|       this.maybeInject();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   maybeInject() {
 | |
|     if (!this.injected && !this.lazyInjected) {
 | |
|       this.lazyInject();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   maybeRevoke() {
 | |
|     if (this.injected && !this.hasPermission) {
 | |
|       this.revoke();
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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, schemaRoot) {
 | |
|     super(params, CONTEXT_FOR_INJECTION);
 | |
| 
 | |
|     this.schemaRoot = schemaRoot;
 | |
| 
 | |
|     this.pendingEntries = new Set();
 | |
|     this.children = new DefaultWeakMap(() => new Map());
 | |
| 
 | |
|     this.injectedRoots = new Set();
 | |
| 
 | |
|     if (params.setPermissionsChangedCallback) {
 | |
|       params.setPermissionsChangedCallback(this.permissionsChanged.bind(this));
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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");
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Updates all injection entries which may need to be updated after a
 | |
|    * permission change, revoking or re-injecting them as necessary.
 | |
|    */
 | |
|   permissionsChanged() {
 | |
|     for (let entry of this.pendingEntries) {
 | |
|       try {
 | |
|         entry.permissionsChanged();
 | |
|       } catch (e) {
 | |
|         Cu.reportError(e);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Recursively revokes all child injection entries of the given
 | |
|    * object.
 | |
|    *
 | |
|    * @param {object} object
 | |
|    *        The object for which to invoke children.
 | |
|    */
 | |
|   revokeChildren(object) {
 | |
|     if (!this.children.has(object)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let children = this.children.get(object);
 | |
|     for (let [name, entry] of children.entries()) {
 | |
|       try {
 | |
|         entry.revoke();
 | |
|       } catch (e) {
 | |
|         Cu.reportError(e);
 | |
|       }
 | |
|       children.delete(name);
 | |
| 
 | |
|       // When we revoke children for an object, we consider that object
 | |
|       // dead. If the entry is ever reified again, a new object is
 | |
|       // created, with new child entries.
 | |
|       this.pendingEntries.delete(entry);
 | |
|     }
 | |
|     this.children.delete(object);
 | |
|   }
 | |
| 
 | |
|   _getInjectionEntry(entry, dest, name, path, parentEntry) {
 | |
|     let injection = new InjectionEntry(
 | |
|       this,
 | |
|       entry,
 | |
|       dest,
 | |
|       name,
 | |
|       path,
 | |
|       parentEntry
 | |
|     );
 | |
| 
 | |
|     this.children.get(dest).set(name, injection);
 | |
| 
 | |
|     return injection;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the property descriptor for the given entry.
 | |
|    *
 | |
|    * @param {Entry} entry
 | |
|    *        The entry instance to return a descriptor for.
 | |
|    * @param {object} dest
 | |
|    *        The object into which this entry is being injected.
 | |
|    * @param {string} name
 | |
|    *        The property name on the destination object where the entry
 | |
|    *        will be injected.
 | |
|    * @param {Array<string>} path
 | |
|    *        The full path from the root injection object to this entry.
 | |
|    * @param {Partial<Entry>} parentEntry
 | |
|    *        The parent entry for this entry.
 | |
|    *
 | |
|    * @returns {object?}
 | |
|    *        A property descriptor object, or null if the entry should
 | |
|    *        not be injected.
 | |
|    */
 | |
|   getDescriptor(entry, dest, name, path, parentEntry) {
 | |
|     let injection = this._getInjectionEntry(
 | |
|       entry,
 | |
|       dest,
 | |
|       name,
 | |
|       path,
 | |
|       parentEntry
 | |
|     );
 | |
| 
 | |
|     return injection.getDescriptor();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Lazily injects the given entry into the given object.
 | |
|    *
 | |
|    * @param {Entry} entry
 | |
|    *        The entry instance to lazily inject.
 | |
|    * @param {object} dest
 | |
|    *        The object into which to inject this entry.
 | |
|    * @param {string} name
 | |
|    *        The property name at which to inject the entry.
 | |
|    * @param {Array<string>} path
 | |
|    *        The full path from the root injection object to this entry.
 | |
|    * @param {Entry} parentEntry
 | |
|    *        The parent entry for this entry.
 | |
|    */
 | |
|   injectInto(entry, dest, name, path, parentEntry) {
 | |
|     let injection = this._getInjectionEntry(
 | |
|       entry,
 | |
|       dest,
 | |
|       name,
 | |
|       path,
 | |
|       parentEntry
 | |
|     );
 | |
| 
 | |
|     injection.lazyInject();
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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 = {
 | |
|   hostname(string, context) {
 | |
|     // TODO bug 1797376: Despite the name, this format is NOT a "hostname",
 | |
|     // but hostname + port and may fail with IPv6. Use canonicalDomain instead.
 | |
|     let valid = true;
 | |
| 
 | |
|     try {
 | |
|       valid = new URL(`http://${string}`).host === string;
 | |
|     } catch (e) {
 | |
|       valid = false;
 | |
|     }
 | |
| 
 | |
|     if (!valid) {
 | |
|       throw new Error(`Invalid hostname ${string}`);
 | |
|     }
 | |
| 
 | |
|     return string;
 | |
|   },
 | |
| 
 | |
|   canonicalDomain(string, context) {
 | |
|     let valid;
 | |
| 
 | |
|     try {
 | |
|       valid = new URL(`http://${string}`).hostname === string;
 | |
|     } catch (e) {
 | |
|       valid = false;
 | |
|     }
 | |
| 
 | |
|     if (!valid) {
 | |
|       // Require the input to be a canonical domain.
 | |
|       // Rejects obvious non-domains such as URLs,
 | |
|       // but also catches non-IDN (punycode) domains.
 | |
|       throw new Error(`Invalid domain ${string}`);
 | |
|     }
 | |
| 
 | |
|     return string;
 | |
|   },
 | |
| 
 | |
|   url(string, context) {
 | |
|     let url = new URL(string).href;
 | |
| 
 | |
|     if (!context.checkLoadURL(url)) {
 | |
|       throw new Error(`Access denied for URL ${url}`);
 | |
|     }
 | |
|     return url;
 | |
|   },
 | |
| 
 | |
|   origin(string, context) {
 | |
|     let url;
 | |
|     try {
 | |
|       url = new URL(string);
 | |
|     } catch (e) {
 | |
|       throw new Error(`Invalid origin: ${string}`);
 | |
|     }
 | |
|     if (!/^https?:/.test(url.protocol)) {
 | |
|       throw new Error(`Invalid origin must be http or https for URL ${string}`);
 | |
|     }
 | |
|     // url.origin is punycode so a direct check against string wont work.
 | |
|     // url.href appends a slash even if not in the original string, we we
 | |
|     // additionally check that string does not end in slash.
 | |
|     if (string.endsWith("/") || url.href != new URL(url.origin).href) {
 | |
|       throw new Error(
 | |
|         `Invalid origin for URL ${string}, replace with origin ${url.origin}`
 | |
|       );
 | |
|     }
 | |
|     if (!context.checkLoadURL(url.origin)) {
 | |
|       throw new Error(`Access denied for URL ${url}`);
 | |
|     }
 | |
|     return url.origin;
 | |
|   },
 | |
| 
 | |
|   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) {
 | |
|     void FORMATS.unresolvedRelativeUrl(string, context);
 | |
|     return FORMATS.relativeUrl(string, context);
 | |
|   },
 | |
| 
 | |
|   unresolvedRelativeUrl(string, context) {
 | |
|     if (!string.startsWith("//")) {
 | |
|       try {
 | |
|         new URL(string);
 | |
|       } catch (e) {
 | |
|         return string;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     throw new SyntaxError(
 | |
|       `String ${JSON.stringify(string)} must be a relative URL`
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   homepageUrl(string, context) {
 | |
|     // Pipes are used for separating homepages, but we only allow extensions to
 | |
|     // set a single homepage. Encoding any pipes makes it one URL.
 | |
|     return FORMATS.relativeUrl(
 | |
|       string.replace(new RegExp("\\|", "g"), "%7C"),
 | |
|       context
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   imageDataOrStrictRelativeUrl(string, context) {
 | |
|     // Do not accept a string which resolves as an absolute URL, or any
 | |
|     // protocol-relative URL, except PNG or JPG data URLs
 | |
|     if (
 | |
|       !string.startsWith("data:image/png;base64,") &&
 | |
|       !string.startsWith("data:image/jpeg;base64,")
 | |
|     ) {
 | |
|       try {
 | |
|         return FORMATS.strictRelativeUrl(string, context);
 | |
|       } catch (e) {
 | |
|         throw new SyntaxError(
 | |
|           `String ${JSON.stringify(
 | |
|             string
 | |
|           )} must be a relative or PNG or JPG data:image URL`
 | |
|         );
 | |
|       }
 | |
|     }
 | |
|     return string;
 | |
|   },
 | |
| 
 | |
|   contentSecurityPolicy(string, context) {
 | |
|     // Manifest V3 extension_pages allows WASM.  When sandbox is
 | |
|     // implemented, or any other V3 or later directive, the flags
 | |
|     // logic will need to be updated.
 | |
|     let flags =
 | |
|       context.manifestVersion < 3
 | |
|         ? Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY
 | |
|         : Ci.nsIAddonContentPolicy.CSP_ALLOW_WASM;
 | |
|     let error = lazy.contentPolicyService.validateAddonCSP(string, flags);
 | |
|     if (error != null) {
 | |
|       // The CSP validation error is not reported as part of the "choices" error message,
 | |
|       // we log the CSP validation error explicitly here to make it easier for the addon developers
 | |
|       // to see and fix the extension CSP.
 | |
|       context.logError(`Error processing ${context.currentTarget}: ${error}`);
 | |
|       return null;
 | |
|     }
 | |
|     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(Date.parse(string))) {
 | |
|       throw new Error(`Invalid date string ${string}`);
 | |
|     }
 | |
|     return string;
 | |
|   },
 | |
| 
 | |
|   manifestShortcutKey(string, context) {
 | |
|     if (lazy.ShortcutUtils.validate(string) == lazy.ShortcutUtils.IS_VALID) {
 | |
|       return string;
 | |
|     }
 | |
|     let errorMessage =
 | |
|       `Value "${string}" must consist of ` +
 | |
|       `either a combination of one or two modifiers, including ` +
 | |
|       `a mandatory primary modifier and a key, separated by '+', ` +
 | |
|       `or a media key. For details see: ` +
 | |
|       `https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`;
 | |
|     throw new Error(errorMessage);
 | |
|   },
 | |
| 
 | |
|   manifestShortcutKeyOrEmpty(string, context) {
 | |
|     return string === "" ? "" : FORMATS.manifestShortcutKey(string, context);
 | |
|   },
 | |
| 
 | |
|   versionString(string, context) {
 | |
|     const parts = string.split(".");
 | |
| 
 | |
|     if (
 | |
|       // We accept up to 4 numbers.
 | |
|       parts.length > 4 ||
 | |
|       // Non-zero values cannot start with 0 and we allow numbers up to 9 digits.
 | |
|       parts.some(part => !/^(0|[1-9][0-9]{0,8})$/.test(part))
 | |
|     ) {
 | |
|       context.logWarning(
 | |
|         `version must be a version string consisting of at most 4 integers ` +
 | |
|           `of at most 9 digits without leading zeros, and separated with dots`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     // The idea is to only emit a warning when the version string does not
 | |
|     // match the simple format we want to encourage developers to use. Given
 | |
|     // the version is required, we always accept the value as is.
 | |
|     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 || [];
 | |
| 
 | |
|     this.min_manifest_version =
 | |
|       schema.min_manifest_version ?? MIN_MANIFEST_VERSION;
 | |
|     this.max_manifest_version =
 | |
|       schema.max_manifest_version ?? MAX_MANIFEST_VERSION;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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 {any} [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.logWarning(message);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Checks whether the entry is deprecated and, if so, logs a
 | |
|    * deprecation message.
 | |
|    *
 | |
|    * @param {Context} context
 | |
|    * @param {any} [value]
 | |
|    */
 | |
|   checkDeprecated(context, value = null) {
 | |
|     if (this.deprecated) {
 | |
|       this.logDeprecation(context, value);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns an object containing property descriptor for use when
 | |
|    * injecting this entry into an API object.
 | |
|    *
 | |
|    * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
 | |
|    * @param {InjectionContext} context
 | |
|    *
 | |
|    * @returns {object?}
 | |
|    *        An object containing a `descriptor` property, specifying the
 | |
|    *        entry's property descriptor, and an optional `revoke`
 | |
|    *        method, to be called when the entry is being revoked.
 | |
|    */
 | |
|   getDescriptor(path, context) {
 | |
|     return undefined;
 | |
|   }
 | |
| }
 | |
| 
 | |
| // 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",
 | |
|       "privileged",
 | |
|       "allowedContexts",
 | |
|       "min_manifest_version",
 | |
|       "max_manifest_version",
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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(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.
 | |
|    *
 | |
|    * @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 {Iterable<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 = []) {
 | |
|     if (DEBUG) {
 | |
|       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".includes(type[0])) {
 | |
|       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];
 | |
|   }
 | |
| 
 | |
|   /** @type {(root, schema, path, extraProperties?: Iterable) => ChoiceType} */
 | |
|   static parseSchema(root, schema, path, extraProperties = []) {
 | |
|     this.checkSchemaProperties(schema, path, extraProperties);
 | |
| 
 | |
|     let choices = schema.choices.map(t => root.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) {
 | |
|         // Ignore a possible choice if it is not supported by
 | |
|         // the manifest version we are normalizing.
 | |
|         if (!context.matchManifestVersion(choice)) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         let r = choice.normalize(value, context);
 | |
|         if (!r.error) {
 | |
|           return r;
 | |
|         }
 | |
| 
 | |
|         error = r;
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     if (result) {
 | |
|       return result;
 | |
|     }
 | |
|     if (choices.size <= 1) {
 | |
|       return error;
 | |
|     }
 | |
| 
 | |
|     choices = Array.from(choices, forceString);
 | |
|     let n = choices.length - 1;
 | |
|     choices[n] = `or ${choices[n]}`;
 | |
| 
 | |
|     let message;
 | |
|     if (typeof value === "object") {
 | |
|       message = () => `Value must either: ${choices.join(", ")}`;
 | |
|     } else {
 | |
|       message = () =>
 | |
|         `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`;
 | |
|     }
 | |
| 
 | |
|     return context.error(message, null);
 | |
|   }
 | |
| 
 | |
|   checkBaseType(baseType) {
 | |
|     return this.choices.some(t => t.checkBaseType(baseType));
 | |
|   }
 | |
| 
 | |
|   getDescriptor(path, context) {
 | |
|     // In StringType.getDescriptor, unlike any other Type, a descriptor is returned if
 | |
|     // it is an enumeration.  Since we need versioned choices in some cases, here we
 | |
|     // build a list of valid enumerations that will work for a given manifest version.
 | |
|     if (
 | |
|       !this.choices.length ||
 | |
|       !this.choices.every(t => t.checkBaseType("string") && t.enumeration)
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let obj = Cu.createObjectIn(context.cloneScope);
 | |
|     let descriptor = { value: obj };
 | |
|     for (let choice of this.choices) {
 | |
|       // Ignore a possible choice if it is not supported by
 | |
|       // the manifest version we are normalizing.
 | |
|       if (!context.matchManifestVersion(choice)) {
 | |
|         continue;
 | |
|       }
 | |
|       let d = choice.getDescriptor(path, context);
 | |
|       if (d) {
 | |
|         Object.assign(obj, d.descriptor.value);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return { descriptor };
 | |
|   }
 | |
| }
 | |
| 
 | |
| // This is a reference to another type--essentially a typedef.
 | |
| class RefType extends Type {
 | |
|   static get EXTRA_PROPERTIES() {
 | |
|     return ["$ref", ...super.EXTRA_PROPERTIES];
 | |
|   }
 | |
| 
 | |
|   /** @type {(root, schema, path, extraProperties?: Iterable) => RefType} */
 | |
|   static parseSchema(root, schema, path, extraProperties = []) {
 | |
|     this.checkSchemaProperties(schema, path, extraProperties);
 | |
| 
 | |
|     let ref = schema.$ref;
 | |
|     let ns = path.join(".");
 | |
|     if (ref.includes(".")) {
 | |
|       [, ns, ref] = /^(.*)\.(.*?)$/.exec(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(root, schema, namespaceName, reference) {
 | |
|     super(schema);
 | |
|     this.root = root;
 | |
|     this.namespaceName = namespaceName;
 | |
|     this.reference = reference;
 | |
|   }
 | |
| 
 | |
|   get targetType() {
 | |
|     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) {
 | |
|     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(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 => {
 | |
|         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,
 | |
|       schema.id || undefined,
 | |
|       enumeration,
 | |
|       schema.minLength || 0,
 | |
|       schema.maxLength || Infinity,
 | |
|       pattern,
 | |
|       format
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   constructor(
 | |
|     schema,
 | |
|     name,
 | |
|     enumeration,
 | |
|     minLength,
 | |
|     maxLength,
 | |
|     pattern,
 | |
|     format
 | |
|   ) {
 | |
|     super(schema);
 | |
|     this.name = name;
 | |
|     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";
 | |
|   }
 | |
| 
 | |
|   getDescriptor(path, context) {
 | |
|     if (this.enumeration) {
 | |
|       let obj = Cu.createObjectIn(context.cloneScope);
 | |
| 
 | |
|       for (let e of this.enumeration) {
 | |
|         obj[e.toUpperCase()] = e;
 | |
|       }
 | |
| 
 | |
|       return {
 | |
|         descriptor: { value: obj },
 | |
|       };
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| class NullType extends Type {
 | |
|   normalize(value, context) {
 | |
|     return this.normalizeBase("null", value, context);
 | |
|   }
 | |
| 
 | |
|   checkBaseType(baseType) {
 | |
|     return baseType == "null";
 | |
|   }
 | |
| }
 | |
| 
 | |
| let FunctionEntry;
 | |
| let Event;
 | |
| let SubModuleType;
 | |
| 
 | |
| class ObjectType extends Type {
 | |
|   static get EXTRA_PROPERTIES() {
 | |
|     return [
 | |
|       "properties",
 | |
|       "patternProperties",
 | |
|       "$import",
 | |
|       ...super.EXTRA_PROPERTIES,
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   static parseSchema(root, schema, path, extraProperties = []) {
 | |
|     if ("functions" in schema) {
 | |
|       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 imported = null;
 | |
|     if ("$import" in schema) {
 | |
|       let importPath = schema.$import;
 | |
|       let idx = importPath.indexOf(".");
 | |
|       if (idx === -1) {
 | |
|         imported = [path[0], importPath];
 | |
|       } else {
 | |
|         imported = [importPath.slice(0, idx), importPath.slice(idx + 1)];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let parseProperty = (schema, extraProps = []) => {
 | |
|       return {
 | |
|         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.
 | |
|     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 = root.parseSchema(type, path);
 | |
|     }
 | |
| 
 | |
|     return new this(
 | |
|       schema,
 | |
|       properties,
 | |
|       additionalProperties,
 | |
|       patternProperties,
 | |
|       schema.isInstanceOf || null,
 | |
|       imported
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   constructor(
 | |
|     schema,
 | |
|     properties,
 | |
|     additionalProperties,
 | |
|     patternProperties,
 | |
|     isInstanceOf,
 | |
|     imported
 | |
|   ) {
 | |
|     super(schema);
 | |
|     this.properties = properties;
 | |
|     this.additionalProperties = additionalProperties;
 | |
|     this.patternProperties = patternProperties;
 | |
|     this.isInstanceOf = isInstanceOf;
 | |
| 
 | |
|     if (imported) {
 | |
|       let [ns, path] = imported;
 | |
|       ns = Schemas.getNamespace(ns);
 | |
|       let importedType = ns.get(path);
 | |
|       if (!importedType) {
 | |
|         throw new Error(`Internal error: imported type ${path} not found`);
 | |
|       }
 | |
| 
 | |
|       if (DEBUG && !(importedType instanceof ObjectType)) {
 | |
|         throw new Error(
 | |
|           `Internal error: cannot import non-object type ${path}`
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       this.properties = Object.assign(
 | |
|         {},
 | |
|         importedType.properties,
 | |
|         this.properties
 | |
|       );
 | |
|       this.patternProperties = [
 | |
|         ...importedType.patternProperties,
 | |
|         ...this.patternProperties,
 | |
|       ];
 | |
|       this.additionalProperties =
 | |
|         importedType.additionalProperties || this.additionalProperties;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   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 = ChromeUtils.getClassName(value, true);
 | |
|     if (klass != "Object") {
 | |
|       throw context.error(
 | |
|         `Expected a plain JavaScript object, got a ${klass}`,
 | |
|         `be a plain JavaScript object`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return ChromeUtils.shallowClone(value);
 | |
|   }
 | |
| 
 | |
|   checkProperty(context, prop, propType, result, properties, remainingProps) {
 | |
|     let { type, optional, unsupported, onError } = propType;
 | |
|     let error = null;
 | |
| 
 | |
|     if (!context.matchManifestVersion(type)) {
 | |
|       if (prop in properties) {
 | |
|         error = context.error(
 | |
|           `Property "${prop}" is unsupported in Manifest Version ${context.manifestVersion}`,
 | |
|           `not contain an unsupported "${prop}" property`
 | |
|         );
 | |
| 
 | |
|         context.logWarning(forceString(error.error));
 | |
|         if (this.additionalProperties) {
 | |
|           // When `additionalProperties` is set to UnrecognizedProperty, the
 | |
|           // caller (i.e. ObjectType's normalize method) assigns the original
 | |
|           // value to `result[prop]`. Erase the property now to prevent
 | |
|           // `result[prop]` from becoming anything other than `undefined.
 | |
|           //
 | |
|           // A warning was already logged above, so we do not need to also log
 | |
|           // "An unexpected property was found in the WebExtension manifest."
 | |
|           remainingProps.delete(prop);
 | |
|         }
 | |
|         // When `additionalProperties` is not set, ObjectType's normalize method
 | |
|         // will return an error because prop is still in remainingProps.
 | |
|         return;
 | |
|       }
 | |
|     } else 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] = propType.default;
 | |
|       } 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] = propType.default;
 | |
|     }
 | |
| 
 | |
|     if (error) {
 | |
|       if (onError == "warn") {
 | |
|         context.logWarning(forceString(error.error));
 | |
|       } else if (onError != "ignore") {
 | |
|         throw error;
 | |
|       }
 | |
| 
 | |
|       result[prop] = propType.default;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   normalize(value, context) {
 | |
|     try {
 | |
|       let v = this.normalizeBase("object", value, context);
 | |
|       if (v.error) {
 | |
|         return v;
 | |
|       }
 | |
|       value = v.value;
 | |
| 
 | |
|       if (this.isInstanceOf) {
 | |
|         if (DEBUG) {
 | |
|           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 (
 | |
|           ChromeUtils.getClassName(value) !== this.isInstanceOf &&
 | |
|           (this.isInstanceOf !== "Element" || value.nodeType !== 1)
 | |
|         ) {
 | |
|           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 r = context.withPath(prop, () =>
 | |
|             this.additionalProperties.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(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(root, fun, path));
 | |
| 
 | |
|     let events = [];
 | |
| 
 | |
|     if (schema.events) {
 | |
|       events = schema.events
 | |
|         .filter(event => !event.unsupported)
 | |
|         .map(event => Event.parseSchema(root, event, path));
 | |
|     }
 | |
| 
 | |
|     return new this(schema, functions, events);
 | |
|   }
 | |
| 
 | |
|   constructor(schema, functions, events) {
 | |
|     // schema contains properties such as min/max_manifest_version needed
 | |
|     // in the base class so that the Context class can version compare
 | |
|     // any entries against the manifest version.
 | |
|     super(schema);
 | |
|     this.functions = functions;
 | |
|     this.events = events;
 | |
|   }
 | |
| };
 | |
| 
 | |
| 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(root, schema, path, extraProperties = []) {
 | |
|     this.checkSchemaProperties(schema, path, extraProperties);
 | |
| 
 | |
|     let { minimum = -Infinity, maximum = Infinity } = schema;
 | |
|     return new this(schema, minimum, maximum);
 | |
|   }
 | |
| 
 | |
|   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 {
 | |
|   static get EXTRA_PROPERTIES() {
 | |
|     return ["enum", ...super.EXTRA_PROPERTIES];
 | |
|   }
 | |
| 
 | |
|   static parseSchema(root, schema, path, extraProperties = []) {
 | |
|     this.checkSchemaProperties(schema, path, extraProperties);
 | |
|     let enumeration = schema.enum || null;
 | |
|     return new this(schema, enumeration);
 | |
|   }
 | |
| 
 | |
|   constructor(schema, enumeration) {
 | |
|     super(schema);
 | |
|     this.enumeration = enumeration;
 | |
|   }
 | |
| 
 | |
|   normalize(value, context) {
 | |
|     if (!this.checkBaseType(getValueBaseType(value))) {
 | |
|       return context.error(
 | |
|         () => `Expected boolean instead of ${JSON.stringify(value)}`,
 | |
|         `be a boolean`
 | |
|       );
 | |
|     }
 | |
|     value = this.preprocess(value, context);
 | |
|     if (this.enumeration && !this.enumeration.includes(value)) {
 | |
|       return context.error(
 | |
|         () => `Invalid value ${JSON.stringify(value)}`,
 | |
|         `be ${this.enumeration}`
 | |
|       );
 | |
|     }
 | |
|     this.checkDeprecated(context, value);
 | |
|     return { value };
 | |
|   }
 | |
| 
 | |
|   checkBaseType(baseType) {
 | |
|     return baseType == "boolean";
 | |
|   }
 | |
| }
 | |
| 
 | |
| class ArrayType extends Type {
 | |
|   static get EXTRA_PROPERTIES() {
 | |
|     return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
 | |
|   }
 | |
| 
 | |
|   static parseSchema(root, schema, path, extraProperties = []) {
 | |
|     this.checkSchemaProperties(schema, path, extraProperties);
 | |
| 
 | |
|     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;
 | |
|     this.maxItems = maxItems;
 | |
|     this.onError = schema.items.onError || null;
 | |
|   }
 | |
| 
 | |
|   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) {
 | |
|         if (this.onError == "warn") {
 | |
|           context.logWarning(forceString(element.error));
 | |
|         } else if (this.onError != "ignore") {
 | |
|           return element;
 | |
|         }
 | |
|         continue;
 | |
|       }
 | |
|       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",
 | |
|       "requireUserInput",
 | |
|       ...super.EXTRA_PROPERTIES,
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   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: 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) {
 | |
|       hasAsyncCallback =
 | |
|         parameters &&
 | |
|         parameters.length &&
 | |
|         parameters[parameters.length - 1].name == schema.async;
 | |
|     }
 | |
| 
 | |
|     if (DEBUG) {
 | |
|       if (isExpectingCallback) {
 | |
|         throw new Error(
 | |
|           `Internal error: Expected a callback parameter ` +
 | |
|             `with name ${schema.async}`
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       if (isAsync && schema.returns) {
 | |
|         throw new Error(
 | |
|           "Internal error: Async functions must not have return values."
 | |
|         );
 | |
|       }
 | |
|       if (
 | |
|         isAsync &&
 | |
|         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,
 | |
|       !!schema.requireUserInput
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   constructor(schema, parameters, isAsync, hasAsyncCallback, requireUserInput) {
 | |
|     super(schema);
 | |
|     this.parameters = parameters;
 | |
|     this.isAsync = isAsync;
 | |
|     this.hasAsyncCallback = hasAsyncCallback;
 | |
|     this.requireUserInput = requireUserInput;
 | |
|   }
 | |
| 
 | |
|   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;
 | |
|   }
 | |
| 
 | |
|   getDescriptor(path, context) {
 | |
|     // Prevent injection if not a supported version.
 | |
|     if (!context.matchManifestVersion(this)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       descriptor: { value: this.value },
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Represents a "property" defined in a schema namespace that is not a
 | |
| // constant.
 | |
| class TypeProperty extends Entry {
 | |
|   unsupported = false;
 | |
| 
 | |
|   constructor(schema, path, name, type, writable, permissions) {
 | |
|     super(schema);
 | |
|     this.path = path;
 | |
|     this.name = name;
 | |
|     this.type = type;
 | |
|     this.writable = writable;
 | |
|     this.permissions = permissions;
 | |
|   }
 | |
| 
 | |
|   throwError(context, msg) {
 | |
|     throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
 | |
|   }
 | |
| 
 | |
|   getDescriptor(path, context) {
 | |
|     if (this.unsupported || !context.matchManifestVersion(this)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let apiImpl = context.getImplementation(path.join("."), this.name);
 | |
| 
 | |
|     let getStub = () => {
 | |
|       this.checkDeprecated(context);
 | |
|       return apiImpl.getProperty();
 | |
|     };
 | |
| 
 | |
|     let descriptor = {
 | |
|       get: Cu.exportFunction(getStub, context.cloneScope),
 | |
|     };
 | |
| 
 | |
|     if (this.writable) {
 | |
|       let setStub = value => {
 | |
|         let normalized = this.type.normalize(value, context);
 | |
|         if (normalized.error) {
 | |
|           this.throwError(context, forceString(normalized.error));
 | |
|         }
 | |
| 
 | |
|         apiImpl.setProperty(normalized.value);
 | |
|       };
 | |
| 
 | |
|       descriptor.set = Cu.exportFunction(setStub, context.cloneScope);
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       descriptor,
 | |
|       revoke() {
 | |
|         apiImpl.revoke();
 | |
|         apiImpl = null;
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| 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(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;
 | |
|   }
 | |
| 
 | |
|   get targetType() {
 | |
|     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 = this.root.getNamespace(namespaceName);
 | |
|       type = ns.get(ref);
 | |
|     }
 | |
|     return type;
 | |
|   }
 | |
| 
 | |
|   getDescriptor(path, context) {
 | |
|     let obj = Cu.createObjectIn(context.cloneScope);
 | |
| 
 | |
|     let ns = this.root.getNamespace(this.namespaceName);
 | |
|     let type = this.targetType;
 | |
| 
 | |
|     // Prevent injection if not a supported version.
 | |
|     if (!context.matchManifestVersion(type)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (DEBUG) {
 | |
|       if (!type || !(type instanceof SubModuleType)) {
 | |
|         throw new Error(
 | |
|           `Internal error: ${this.namespaceName}.${this.reference} ` +
 | |
|             `is not a sub-module`
 | |
|         );
 | |
|       }
 | |
|     }
 | |
|     let subpath = [...path, this.name];
 | |
| 
 | |
|     let functions = type.functions;
 | |
|     for (let fun of functions) {
 | |
|       context.injectInto(fun, obj, fun.name, subpath, ns);
 | |
|     }
 | |
| 
 | |
|     let events = type.events;
 | |
|     for (let event of events) {
 | |
|       context.injectInto(event, obj, event.name, subpath, ns);
 | |
|     }
 | |
| 
 | |
|     // TODO: Inject this.properties.
 | |
| 
 | |
|     return {
 | |
|       descriptor: { value: obj },
 | |
|       revoke() {
 | |
|         let unwrapped = ChromeUtils.waiveXrays(obj);
 | |
|         for (let fun of functions) {
 | |
|           try {
 | |
|             delete unwrapped[fun.name];
 | |
|           } catch (e) {
 | |
|             Cu.reportError(e);
 | |
|           }
 | |
|         }
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| // 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 {
 | |
|   hasAsyncCallback = false;
 | |
| 
 | |
|   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, {});
 | |
|         } 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} (${forceString(r.error)})`
 | |
|         );
 | |
|       }
 | |
|       return r.value;
 | |
|     });
 | |
| 
 | |
|     return fixedArgs;
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Represents a "function" defined in a schema namespace.
 | |
| FunctionEntry = class FunctionEntry extends CallEntry {
 | |
|   static parseSchema(root, schema, path) {
 | |
|     // When not in DEBUG mode, we just need to know *if* this returns.
 | |
|     /** @type {boolean|object} */
 | |
|     let returns = !!schema.returns;
 | |
|     if (DEBUG && "returns" in schema) {
 | |
|       returns = {
 | |
|         type: root.parseSchema(schema.returns, path, ["optional", "name"]),
 | |
|         optional: schema.returns.optional || false,
 | |
|         name: "result",
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     return new this(
 | |
|       schema,
 | |
|       path,
 | |
|       schema.name,
 | |
|       root.parseSchema(schema, path, [
 | |
|         "name",
 | |
|         "unsupported",
 | |
|         "returns",
 | |
|         "permissions",
 | |
|         "allowAmbiguousOptionalArguments",
 | |
|         "allowCrossOriginArguments",
 | |
|       ]),
 | |
|       schema.unsupported || false,
 | |
|       schema.allowAmbiguousOptionalArguments || false,
 | |
|       schema.allowCrossOriginArguments || false,
 | |
|       returns,
 | |
|       schema.permissions || null
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   constructor(
 | |
|     schema,
 | |
|     path,
 | |
|     name,
 | |
|     type,
 | |
|     unsupported,
 | |
|     allowAmbiguousOptionalArguments,
 | |
|     allowCrossOriginArguments,
 | |
|     returns,
 | |
|     permissions
 | |
|   ) {
 | |
|     super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
 | |
|     this.unsupported = unsupported;
 | |
|     this.returns = returns;
 | |
|     this.permissions = permissions;
 | |
|     this.allowCrossOriginArguments = allowCrossOriginArguments;
 | |
| 
 | |
|     this.isAsync = type.isAsync;
 | |
|     this.hasAsyncCallback = type.hasAsyncCallback;
 | |
|     this.requireUserInput = type.requireUserInput;
 | |
|   }
 | |
| 
 | |
|   checkValue({ type, optional, name }, value, context) {
 | |
|     if (optional && value == null) {
 | |
|       return;
 | |
|     }
 | |
|     if (
 | |
|       type.reference === "ExtensionPanel" ||
 | |
|       type.reference === "ExtensionSidebarPane" ||
 | |
|       type.reference === "Port"
 | |
|     ) {
 | |
|       // TODO: We currently treat objects with functions as SubModuleType,
 | |
|       // which is just wrong, and a bigger yak.  Skipping for now.
 | |
|       return;
 | |
|     }
 | |
|     const { error } = type.normalize(value, context);
 | |
|     if (error) {
 | |
|       this.throwError(
 | |
|         context,
 | |
|         `Type error for ${name} value (${forceString(error)})`
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   checkCallback(args, context) {
 | |
|     const callback = this.parameters[this.parameters.length - 1];
 | |
|     for (const [i, param] of callback.type.parameters.entries()) {
 | |
|       this.checkValue(param, args[i], context);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   getDescriptor(path, context) {
 | |
|     let apiImpl = context.getImplementation(path.join("."), this.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 = () => {};
 | |
|         }
 | |
|         if (DEBUG && this.hasAsyncCallback && callback) {
 | |
|           let original = callback;
 | |
|           callback = (...args) => {
 | |
|             this.checkCallback(args, context);
 | |
|             original(...args);
 | |
|           };
 | |
|         }
 | |
|         let result = apiImpl.callAsyncFunction(
 | |
|           actuals,
 | |
|           callback,
 | |
|           this.requireUserInput
 | |
|         );
 | |
|         if (DEBUG && this.hasAsyncCallback && !callback) {
 | |
|           return result.then(result => {
 | |
|             this.checkCallback([result], context);
 | |
|             return result;
 | |
|           });
 | |
|         }
 | |
|         return result;
 | |
|       };
 | |
|     } 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);
 | |
|         let result = apiImpl.callFunction(actuals);
 | |
|         if (DEBUG && this.returns) {
 | |
|           this.checkValue(this.returns, result, context);
 | |
|         }
 | |
|         return result;
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       descriptor: {
 | |
|         value: Cu.exportFunction(stub, context.cloneScope, {
 | |
|           allowCrossOriginArguments: this.allowCrossOriginArguments,
 | |
|         }),
 | |
|       },
 | |
|       revoke() {
 | |
|         apiImpl.revoke();
 | |
|         apiImpl = null;
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| };
 | |
| 
 | |
| // 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.
 | |
| // eslint-disable-next-line no-global-assign
 | |
| Event = class Event extends CallEntry {
 | |
|   static parseSchema(root, event, path) {
 | |
|     let extraParameters = Array.from(event.extraParameters || [], param => ({
 | |
|       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,
 | |
|       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;
 | |
|     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;
 | |
|   }
 | |
| 
 | |
|   getDescriptor(path, context) {
 | |
|     let apiImpl = context.getImplementation(path.join("."), this.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(context.cloneScope);
 | |
| 
 | |
|     Cu.exportFunction(addStub, obj, { defineAs: "addListener" });
 | |
|     Cu.exportFunction(removeStub, obj, { defineAs: "removeListener" });
 | |
|     Cu.exportFunction(hasStub, obj, { defineAs: "hasListener" });
 | |
| 
 | |
|     return {
 | |
|       descriptor: { value: obj },
 | |
|       revoke() {
 | |
|         apiImpl.revoke();
 | |
|         apiImpl = null;
 | |
| 
 | |
|         let unwrapped = ChromeUtils.waiveXrays(obj);
 | |
|         delete unwrapped.addListener;
 | |
|         delete unwrapped.removeListener;
 | |
|         delete unwrapped.hasListener;
 | |
|       },
 | |
|     };
 | |
|   }
 | |
| };
 | |
| 
 | |
| const TYPES = Object.freeze(
 | |
|   Object.assign(Object.create(null), {
 | |
|     any: AnyType,
 | |
|     array: ArrayType,
 | |
|     boolean: BooleanType,
 | |
|     function: FunctionType,
 | |
|     integer: IntegerType,
 | |
|     null: NullType,
 | |
|     number: NumberType,
 | |
|     object: ObjectType,
 | |
|     string: StringType,
 | |
|   })
 | |
| );
 | |
| 
 | |
| const LOADERS = {
 | |
|   events: "loadEvent",
 | |
|   functions: "loadFunction",
 | |
|   properties: "loadProperty",
 | |
|   types: "loadType",
 | |
| };
 | |
| 
 | |
| class Namespace extends Map {
 | |
|   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;
 | |
| 
 | |
|     this.min_manifest_version = MIN_MANIFEST_VERSION;
 | |
|     this.max_manifest_version = MAX_MANIFEST_VERSION;
 | |
| 
 | |
|     this.permissions = null;
 | |
|     this.allowedContexts = [];
 | |
|     this.defaultContexts = [];
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Adds a JSON Schema object to the set of schemas that represent this
 | |
|    * namespace.
 | |
|    *
 | |
|    * @param {object} schema
 | |
|    *        A JSON schema object which partially describes this
 | |
|    *        namespace.
 | |
|    */
 | |
|   addSchema(schema) {
 | |
|     this._lazySchemas.push(schema);
 | |
| 
 | |
|     for (let prop of [
 | |
|       "permissions",
 | |
|       "allowedContexts",
 | |
|       "defaultContexts",
 | |
|       "min_manifest_version",
 | |
|       "max_manifest_version",
 | |
|     ]) {
 | |
|       if (schema[prop]) {
 | |
|         this[prop] = schema[prop];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (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() {
 | |
|     if (this.initialized) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (this.superNamespace) {
 | |
|       this._lazySchemas.unshift(...this.superNamespace._lazySchemas);
 | |
|     }
 | |
| 
 | |
|     // Keep in sync with LOADERS above.
 | |
|     this.types = new DefaultMap(() => []);
 | |
|     this.properties = new DefaultMap(() => []);
 | |
|     this.functions = new DefaultMap(() => []);
 | |
|     this.events = new DefaultMap(() => []);
 | |
| 
 | |
|     for (let schema of this._lazySchemas) {
 | |
|       for (let type of schema.types || []) {
 | |
|         if (!type.unsupported) {
 | |
|           this.types.get(type.$extend || type.id).push(type);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       for (let [name, prop] of Object.entries(schema.properties || {})) {
 | |
|         if (!prop.unsupported) {
 | |
|           this.properties.get(name).push(prop);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       for (let fun of schema.functions || []) {
 | |
|         if (!fun.unsupported) {
 | |
|           this.functions.get(fun.name).push(fun);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       for (let event of schema.events || []) {
 | |
|         if (!event.unsupported) {
 | |
|           this.events.get(event.name).push(event);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // For each type of top-level property in the schema object, iterate
 | |
|     // over all properties of that type, and create a temporary key for
 | |
|     // each property pointing to its type. Those temporary properties
 | |
|     // are later used to instantiate an Entry object based on the actual
 | |
|     // schema object.
 | |
|     for (let type of Object.keys(LOADERS)) {
 | |
|       for (let key of this[type].keys()) {
 | |
|         this.set(key, type);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.initialized = true;
 | |
| 
 | |
|     if (DEBUG) {
 | |
|       for (let key of this.keys()) {
 | |
|         this.get(key);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Initializes the value of a given key, by parsing the schema object
 | |
|    * associated with it and replacing its temporary value with an `Entry`
 | |
|    * instance.
 | |
|    *
 | |
|    * @param {string} key
 | |
|    *        The name of the property to initialize.
 | |
|    * @param {string} type
 | |
|    *        The type of property the key represents. Must have a
 | |
|    *        corresponding entry in the `LOADERS` object, pointing to the
 | |
|    *        initialization method for that type.
 | |
|    *
 | |
|    * @returns {Entry}
 | |
|    */
 | |
|   initKey(key, type) {
 | |
|     let loader = LOADERS[type];
 | |
| 
 | |
|     for (let schema of this[type].get(key)) {
 | |
|       this.set(key, this[loader](key, schema));
 | |
|     }
 | |
| 
 | |
|     return this.get(key);
 | |
|   }
 | |
| 
 | |
|   loadType(name, type) {
 | |
|     if ("$extend" in type) {
 | |
|       return this.extendType(type);
 | |
|     }
 | |
|     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 nonexistent 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.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(
 | |
|           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 = 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(this.root, fun, this.path);
 | |
|   }
 | |
| 
 | |
|   loadEvent(name, event) {
 | |
|     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
 | |
|    *        The injection context with which to inject the properties.
 | |
|    */
 | |
|   injectInto(dest, context) {
 | |
|     for (let name of this.keys()) {
 | |
|       // If the entry does not match the manifest version do not
 | |
|       // inject the property.  This prevents the item from being
 | |
|       // enumerable in the namespace object.  We cannot accomplish
 | |
|       // this inside exportLazyProperty, it specifically injects
 | |
|       // an enumerable object.
 | |
|       let entry = this.get(name);
 | |
|       if (!context.matchManifestVersion(entry)) {
 | |
|         continue;
 | |
|       }
 | |
|       exportLazyProperty(dest, name, () => {
 | |
|         let entry = this.get(name);
 | |
| 
 | |
|         return context.getDescriptor(entry, dest, name, this.path, this);
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   getDescriptor(path, context) {
 | |
|     let obj = Cu.createObjectIn(context.cloneScope);
 | |
| 
 | |
|     let ns = context.schemaRoot.getNamespace(this.path.join("."));
 | |
|     ns.injectInto(obj, context);
 | |
| 
 | |
|     // Only inject the namespace object if it isn't empty.
 | |
|     if (Object.keys(obj).length) {
 | |
|       return {
 | |
|         descriptor: { value: obj },
 | |
|       };
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   keys() {
 | |
|     this.init();
 | |
|     return super.keys();
 | |
|   }
 | |
| 
 | |
|   /** @returns {Generator<[string, Entry]>} */
 | |
|   *entries() {
 | |
|     for (let key of this.keys()) {
 | |
|       yield [key, this.get(key)];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   get(key) {
 | |
|     this.init();
 | |
|     let value = super.get(key);
 | |
| 
 | |
|     // The initial values of lazily-initialized schema properties are
 | |
|     // strings, pointing to the type of property, corresponding to one
 | |
|     // of the entries in the `LOADERS` object.
 | |
|     if (typeof value === "string") {
 | |
|       value = this.initKey(key, value);
 | |
|     }
 | |
| 
 | |
|     return value;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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, 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) {
 | |
|       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);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * A namespace which combines the children of an arbitrary number of
 | |
|  * sub-namespaces.
 | |
|  */
 | |
| class Namespaces extends Namespace {
 | |
|   constructor(root, name, path, namespaces) {
 | |
|     super(root, name, path);
 | |
| 
 | |
|     this.namespaces = namespaces;
 | |
|   }
 | |
| 
 | |
|   injectInto(obj, context) {
 | |
|     for (let ns of this.namespaces) {
 | |
|       ns.injectInto(obj, context);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * A root schema which combines the contents of an arbitrary number of base
 | |
|  * schema roots.
 | |
|  */
 | |
| class SchemaRoots extends Namespaces {
 | |
|   constructor(root, bases) {
 | |
|     bases = bases.map(base => base.rootSchema || base);
 | |
| 
 | |
|     super(null, "", [], bases);
 | |
| 
 | |
|     this.root = root;
 | |
|     this.bases = bases;
 | |
|     this._namespaces = new Map();
 | |
|   }
 | |
| 
 | |
|   _getNamespace(name, create) {
 | |
|     let results = [];
 | |
|     for (let root of this.bases) {
 | |
|       let ns = root.getNamespace(name, create);
 | |
|       if (ns) {
 | |
|         results.push(ns);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (results.length == 1) {
 | |
|       return results[0];
 | |
|     }
 | |
| 
 | |
|     if (results.length) {
 | |
|       return new Namespaces(this.root, name, name.split("."), results);
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   getNamespace(name, create) {
 | |
|     let ns = this._namespaces.get(name);
 | |
|     if (!ns) {
 | |
|       ns = this._getNamespace(name, create);
 | |
|       if (ns) {
 | |
|         this._namespaces.set(name, ns);
 | |
|       }
 | |
|     }
 | |
|     return ns;
 | |
|   }
 | |
| 
 | |
|   *getNamespaces(name) {
 | |
|     for (let root of this.bases) {
 | |
|       yield* root.getNamespaces(name);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * A root schema namespace containing schema data which is isolated from data in
 | |
|  * other schema roots. May extend a base namespace, in which case schemas in
 | |
|  * this root may refer to types in a base, but not vice versa.
 | |
|  *
 | |
|  * @param {SchemaRoot|Array<SchemaRoot>|null} base
 | |
|  *        A base schema root (or roots) from which to derive, or null.
 | |
|  * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON
 | |
|  *        A map of schema URLs and corresponding JSON blobs from which to
 | |
|  *        populate this root namespace.
 | |
|  */
 | |
| export class SchemaRoot extends Namespace {
 | |
|   constructor(base, schemaJSON) {
 | |
|     super(null, "", []);
 | |
| 
 | |
|     if (Array.isArray(base)) {
 | |
|       base = new SchemaRoots(this, base);
 | |
|     }
 | |
| 
 | |
|     this.root = this;
 | |
|     this.base = base;
 | |
|     this.schemaJSON = schemaJSON;
 | |
|   }
 | |
| 
 | |
|   *getNamespaces(path) {
 | |
|     let name = path.join(".");
 | |
| 
 | |
|     let ns = this.getNamespace(name, false);
 | |
|     if (ns) {
 | |
|       yield ns;
 | |
|     }
 | |
| 
 | |
|     if (this.base) {
 | |
|       yield* this.base.getNamespaces(name);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns the sub-namespace with the given name. If the given namespace
 | |
|    * doesn't already exist, attempts to find it in the base SchemaRoot before
 | |
|    * creating a new empty namespace.
 | |
|    *
 | |
|    * @param {string} name
 | |
|    *        The namespace to retrieve.
 | |
|    * @param {boolean} [create = true]
 | |
|    *        If true, an empty namespace should be created if one does not
 | |
|    *        already exist.
 | |
|    * @returns {Namespace|null}
 | |
|    */
 | |
|   getNamespace(name, create = true) {
 | |
|     let ns = super.getNamespace(name, false);
 | |
|     if (ns) {
 | |
|       return ns;
 | |
|     }
 | |
| 
 | |
|     ns = this.base && this.base.getNamespace(name, false);
 | |
|     if (ns) {
 | |
|       return ns;
 | |
|     }
 | |
|     return create && super.getNamespace(name, create);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Like getNamespace, but does not take the base SchemaRoot into account.
 | |
|    *
 | |
|    * @param {string} name
 | |
|    *        The namespace to retrieve.
 | |
|    * @returns {Namespace}
 | |
|    */
 | |
|   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(globalThis, isParentProcess);
 | |
| 
 | |
|           // 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, this);
 | |
| 
 | |
|     this.injectInto(dest, context);
 | |
|   }
 | |
| 
 | |
|   injectInto(dest, context) {
 | |
|     // For schema graphs where multiple schema roots have the same base, don't
 | |
|     // inject it more than once.
 | |
| 
 | |
|     if (!context.injectedRoots.has(this)) {
 | |
|       context.injectedRoots.add(this);
 | |
|       if (this.base) {
 | |
|         this.base.injectInto(dest, context);
 | |
|       }
 | |
|       super.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;
 | |
|   }
 | |
| }
 | |
| 
 | |
| export var 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 map of schema JSON which should be available in all content processes.
 | |
|   contentSchemaJSON: new Map(),
 | |
| 
 | |
|   // A map of schema JSON which should only be available to extension processes.
 | |
|   privilegedSchemaJSON: new Map(),
 | |
| 
 | |
|   _rootSchema: null,
 | |
| 
 | |
|   // A weakmap for the validation Context class instances given an extension
 | |
|   // context (keyed by the extensin context instance).
 | |
|   // This is used instead of the InjectionContext for webIDL API validation
 | |
|   // and normalization (see Schemas.checkParameters).
 | |
|   paramsValidationContexts: new DefaultWeakMap(
 | |
|     extContext => new Context(extContext)
 | |
|   ),
 | |
| 
 | |
|   get rootSchema() {
 | |
|     if (!this.initialized) {
 | |
|       this.init();
 | |
|     }
 | |
|     if (!this._rootSchema) {
 | |
|       this._rootSchema = new SchemaRoot(null, this.schemaJSON);
 | |
|       this._rootSchema.parseSchemas();
 | |
|     }
 | |
|     return this._rootSchema;
 | |
|   },
 | |
| 
 | |
|   getNamespace(name) {
 | |
|     return this.rootSchema.getNamespace(name);
 | |
|   },
 | |
| 
 | |
|   init() {
 | |
|     if (this.initialized) {
 | |
|       return;
 | |
|     }
 | |
|     this.initialized = true;
 | |
| 
 | |
|     if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
 | |
|       let addSchemas = schemas => {
 | |
|         for (let [key, value] of schemas.entries()) {
 | |
|           this.schemaJSON.set(key, value);
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       if (WebExtensionPolicy.isExtensionProcess || DEBUG) {
 | |
|         addSchemas(Services.cpmm.sharedData.get(KEY_PRIVILEGED_SCHEMAS));
 | |
|       }
 | |
| 
 | |
|       let schemas = Services.cpmm.sharedData.get(KEY_CONTENT_SCHEMAS);
 | |
|       if (schemas) {
 | |
|         addSchemas(schemas);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _loadCachedSchemasPromise: null,
 | |
|   loadCachedSchemas() {
 | |
|     if (!this._loadCachedSchemasPromise) {
 | |
|       this._loadCachedSchemasPromise = lazy.StartupCache.schemas
 | |
|         .getAll()
 | |
|         .then(results => {
 | |
|           return results;
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     return this._loadCachedSchemasPromise;
 | |
|   },
 | |
| 
 | |
|   addSchema(url, schema, content = false) {
 | |
|     this.schemaJSON.set(url, schema);
 | |
| 
 | |
|     if (content) {
 | |
|       this.contentSchemaJSON.set(url, schema);
 | |
|     } else {
 | |
|       this.privilegedSchemaJSON.set(url, schema);
 | |
|     }
 | |
| 
 | |
|     if (this._rootSchema) {
 | |
|       throw new Error("Schema loaded after root schema populated");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   updateSharedSchemas() {
 | |
|     let { sharedData } = Services.ppmm;
 | |
| 
 | |
|     sharedData.set(KEY_CONTENT_SCHEMAS, this.contentSchemaJSON);
 | |
|     sharedData.set(KEY_PRIVILEGED_SCHEMAS, this.privilegedSchemaJSON);
 | |
|   },
 | |
| 
 | |
|   fetch(url) {
 | |
|     return readJSONAndBlobbify(url);
 | |
|   },
 | |
| 
 | |
|   processSchema(json) {
 | |
|     return blobbify(json);
 | |
|   },
 | |
| 
 | |
|   async load(url, content = false) {
 | |
|     if (!isParentProcess) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const startTime = Cu.now();
 | |
|     let schemaCache = await this.loadCachedSchemas();
 | |
|     const fromCache = schemaCache.has(url);
 | |
| 
 | |
|     let blob =
 | |
|       schemaCache.get(url) ||
 | |
|       (await lazy.StartupCache.schemas.get(url, readJSONAndBlobbify));
 | |
| 
 | |
|     if (!this.schemaJSON.has(url)) {
 | |
|       this.addSchema(url, blob, content);
 | |
|     }
 | |
| 
 | |
|     ChromeUtils.addProfilerMarker(
 | |
|       "ExtensionSchemas",
 | |
|       { startTime },
 | |
|       `load ${url}, from cache: ${fromCache}`
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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) {
 | |
|     return this.rootSchema.checkPermissions(namespace, wrapperFuncs);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Returns a sorted array of permission names for the given permission types.
 | |
|    *
 | |
|    * @param {Array} types An array of permission types, defaults to all permissions.
 | |
|    * @returns {Array} sorted array of permission names
 | |
|    */
 | |
|   getPermissionNames(
 | |
|     types = [
 | |
|       "Permission",
 | |
|       "OptionalPermission",
 | |
|       "PermissionNoPrompt",
 | |
|       "OptionalPermissionNoPrompt",
 | |
|       "PermissionPrivileged",
 | |
|     ]
 | |
|   ) {
 | |
|     const ns = this.getNamespace("manifest");
 | |
|     let names = [];
 | |
|     for (let typeName of types) {
 | |
|       for (let choice of ns
 | |
|         .get(typeName)
 | |
|         .choices.filter(choice => choice.enumeration)) {
 | |
|         names = names.concat(choice.enumeration);
 | |
|       }
 | |
|     }
 | |
|     return names.sort();
 | |
|   },
 | |
| 
 | |
|   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) {
 | |
|     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) {
 | |
|     return this.rootSchema.normalize(obj, typeName, context);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Validate and normalize the arguments for an API request originated
 | |
|    * from the webIDL API bindings.
 | |
|    *
 | |
|    * This provides for calls originating through WebIDL the parameters
 | |
|    * validation and normalization guarantees that the ext-APINAMESPACE.js
 | |
|    * scripts expects (what InjectionContext does for the regular bindings).
 | |
|    *
 | |
|    * @param {object}                   extContext
 | |
|    * @param {mozIExtensionAPIRequest } apiRequest
 | |
|    *
 | |
|    * @returns {Array<any>} Normalized arguments array.
 | |
|    */
 | |
|   checkWebIDLRequestParameters(extContext, apiRequest) {
 | |
|     const getSchemaForProperty = (schemaObj, propName, schemaPath) => {
 | |
|       if (schemaObj instanceof Namespace) {
 | |
|         return schemaObj?.get(propName);
 | |
|       } else if (schemaObj instanceof SubModuleProperty) {
 | |
|         for (const fun of schemaObj.targetType.functions) {
 | |
|           if (fun.name === propName) {
 | |
|             return fun;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         for (const fun of schemaObj.targetType.events) {
 | |
|           if (fun.name === propName) {
 | |
|             return fun;
 | |
|           }
 | |
|         }
 | |
|       } else if (schemaObj instanceof Event) {
 | |
|         return schemaObj;
 | |
|       }
 | |
| 
 | |
|       const schemaPathType = schemaObj?.constructor.name;
 | |
|       throw new Error(
 | |
|         `API Schema for "${propName}" not found in ${schemaPath} (${schemaPath} type is ${schemaPathType})`
 | |
|       );
 | |
|     };
 | |
|     const { requestType, apiNamespace, apiName } = apiRequest;
 | |
| 
 | |
|     let [ns, ...rest] = (
 | |
|       ["addListener", "removeListener"].includes(requestType)
 | |
|         ? `${apiNamespace}.${apiName}.${requestType}`
 | |
|         : `${apiNamespace}.${apiName}`
 | |
|     ).split(".");
 | |
|     let apiSchema = this.getNamespace(ns);
 | |
| 
 | |
|     // Keep track of the current schema path, populated while navigating the nested API schema
 | |
|     // data and then used to include the full path to the API schema that is hitting unexpected
 | |
|     // errors due to schema data not found or an unexpected schema type.
 | |
|     let schemaPath = [ns];
 | |
| 
 | |
|     while (rest.length) {
 | |
|       // Nested property as namespace (e.g. used for proxy.settings requests).
 | |
|       if (!apiSchema) {
 | |
|         throw new Error(`API Schema not found for ${schemaPath.join(".")}`);
 | |
|       }
 | |
| 
 | |
|       let [propName, ...newRest] = rest;
 | |
|       rest = newRest;
 | |
| 
 | |
|       apiSchema = getSchemaForProperty(
 | |
|         apiSchema,
 | |
|         propName,
 | |
|         schemaPath.join(".")
 | |
|       );
 | |
|       schemaPath.push(propName);
 | |
|     }
 | |
| 
 | |
|     if (!apiSchema) {
 | |
|       throw new Error(`API Schema not found for ${schemaPath.join(".")}`);
 | |
|     }
 | |
| 
 | |
|     if (!apiSchema.checkParameters) {
 | |
|       throw new Error(
 | |
|         `Unexpected API Schema type for ${schemaPath.join(
 | |
|           "."
 | |
|         )} (${schemaPath.join(".")} type is ${apiSchema.constructor.name})`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return apiSchema.checkParameters(
 | |
|       apiRequest.args,
 | |
|       this.paramsValidationContexts.get(extContext)
 | |
|     );
 | |
|   },
 | |
| };
 | 
