1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
10 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
12 var { DefaultMap, DefaultWeakMap } = ExtensionUtils;
17 ChromeUtils.defineESModuleGetters(lazy, {
18 ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
19 NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
20 ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
23 XPCOMUtils.defineLazyServiceGetter(
25 "contentPolicyService",
26 "@mozilla.org/addons/content-policy;1",
27 "nsIAddonContentPolicy"
30 ChromeUtils.defineLazyGetter(
33 () => lazy.ExtensionParent.StartupCache
36 XPCOMUtils.defineLazyPreferenceGetter(
38 "treatWarningsAsErrors",
39 "extensions.webextensions.warnings-as-errors",
43 const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content";
44 const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged";
46 const MIN_MANIFEST_VERSION = 2;
47 const MAX_MANIFEST_VERSION = 3;
49 const { DEBUG } = AppConstants;
51 const isParentProcess =
52 Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
54 function readJSON(url) {
55 return new Promise((resolve, reject) => {
56 lazy.NetUtil.asyncFetch(
57 { uri: url, loadUsingSystemPrincipal: true },
58 (inputStream, status) => {
59 if (!Components.isSuccessCode(status)) {
60 // Convert status code to a string
61 let e = Components.Exception("", status);
62 reject(new Error(`Error while loading '${url}' (${e.name})`));
66 let text = lazy.NetUtil.readInputStreamToString(
68 inputStream.available()
71 // Chrome JSON files include a license comment that we need to
72 // strip off for this to be valid JSON. As a hack, we just
73 // look for the first '[' character, which signals the start
74 // of the JSON content.
75 let index = text.indexOf("[");
76 text = text.slice(index);
78 resolve(JSON.parse(text));
87 function stripDescriptions(json, stripThis = true) {
88 if (Array.isArray(json)) {
89 for (let i = 0; i < json.length; i++) {
90 if (typeof json[i] === "object" && json[i] !== null) {
91 json[i] = stripDescriptions(json[i]);
99 // Objects are handled much more efficiently, both in terms of memory and
100 // CPU, if they have the same shape as other objects that serve the same
101 // purpose. So, normalize the order of properties to increase the chances
102 // that the majority of schema objects wind up in large shape groups.
103 for (let key of Object.keys(json).sort()) {
104 if (stripThis && key === "description" && typeof json[key] === "string") {
108 if (typeof json[key] === "object" && json[key] !== null) {
109 result[key] = stripDescriptions(json[key], key !== "properties");
111 result[key] = json[key];
118 function blobbify(json) {
119 // We don't actually use descriptions at runtime, and they make up about a
120 // third of the size of our structured clone data, so strip them before
122 json = stripDescriptions(json);
124 return new StructuredCloneHolder("Schemas/blobbify", null, json);
127 async function readJSONAndBlobbify(url) {
128 let json = await readJSON(url);
130 return blobbify(json);
134 * Defines a lazy getter for the given property on the given object. Any
135 * security wrappers are waived on the object before the property is
136 * defined, and the getter and setter methods are wrapped for the target
139 * The given getter function is guaranteed to be called only once, even
140 * if the target scope retrieves the wrapped getter from the property
141 * descriptor and calls it directly.
143 * @param {object} object
144 * The object on which to define the getter.
145 * @param {string | symbol} prop
146 * The property name for which to define the getter.
147 * @param {Function} getter
148 * The function to call in order to generate the final property
151 function exportLazyGetter(object, prop, getter) {
152 object = ChromeUtils.waiveXrays(object);
154 let redefine = value => {
155 if (value === undefined) {
158 Object.defineProperty(object, prop, {
171 Object.defineProperty(object, prop, {
175 get: Cu.exportFunction(function () {
176 return redefine(getter.call(this));
179 set: Cu.exportFunction(value => {
186 * Defines a lazily-instantiated property descriptor on the given
187 * object. Any security wrappers are waived on the object before the
188 * property is defined.
190 * The given getter function is guaranteed to be called only once, even
191 * if the target scope retrieves the wrapped getter from the property
192 * descriptor and calls it directly.
194 * @param {object} object
195 * The object on which to define the getter.
196 * @param {string | symbol} prop
197 * The property name for which to define the getter.
198 * @param {Function} getter
199 * The function to call in order to generate the final property
200 * descriptor object. This will be called, and the property
201 * descriptor installed on the object, the first time the
202 * property is written or read. The function may return
203 * undefined, which will cause the property to be deleted.
205 function exportLazyProperty(object, prop, getter) {
206 object = ChromeUtils.waiveXrays(object);
208 let redefine = obj => {
209 let desc = getter.call(obj);
219 if (!desc.set && !desc.get) {
220 defaults.writable = true;
223 Object.defineProperty(object, prop, Object.assign(defaults, desc));
227 Object.defineProperty(object, prop, {
231 get: Cu.exportFunction(function () {
236 set: Cu.exportFunction(function (value) {
238 object[prop] = value;
243 const POSTPROCESSORS = {
244 convertImageDataToURL(imageData, context) {
245 let document = context.cloneScope.document;
246 let canvas = document.createElementNS(
247 "http://www.w3.org/1999/xhtml",
250 canvas.width = imageData.width;
251 canvas.height = imageData.height;
252 canvas.getContext("2d").putImageData(imageData, 0, 0);
254 return canvas.toDataURL("image/png");
256 mutuallyExclusiveBlockingOrAsyncBlocking(value, context) {
257 if (!Array.isArray(value)) {
260 if (value.includes("blocking") && value.includes("asyncBlocking")) {
261 throw new context.cloneScope.Error(
262 "'blocking' and 'asyncBlocking' are mutually exclusive"
267 webRequestBlockingPermissionRequired(string, context) {
268 if (string === "blocking" && !context.hasPermission("webRequestBlocking")) {
269 throw new context.cloneScope.Error(
270 "Using webRequest.addListener with the " +
271 "blocking option requires the 'webRequestBlocking' permission."
277 webRequestBlockingOrAuthProviderPermissionRequired(string, context) {
279 string === "blocking" &&
281 context.hasPermission("webRequestBlocking") ||
282 context.hasPermission("webRequestAuthProvider")
285 throw new context.cloneScope.Error(
286 "Using webRequest.onAuthRequired.addListener with the " +
287 "blocking option requires either the 'webRequestBlocking' " +
288 "or 'webRequestAuthProvider' permission."
294 requireBackgroundServiceWorkerEnabled(value, context) {
295 if (WebExtensionPolicy.backgroundServiceWorkerEnabled) {
299 // Add an error to the manifest validations and throw the
301 const msg = "background.service_worker is currently disabled";
302 context.logError(context.makeError(msg));
303 throw new Error(msg);
306 manifestVersionCheck(value, context) {
310 Services.prefs.getBoolPref("extensions.manifestV3.enabled", false))
314 const msg = `Unsupported manifest version: ${value}`;
315 context.logError(context.makeError(msg));
316 throw new Error(msg);
319 webAccessibleMatching(value, context) {
320 // Ensure each object has at least one of matches or extension_ids array.
321 for (let obj of value) {
322 if (!obj.matches && !obj.extension_ids) {
323 const msg = `web_accessible_resources requires one of "matches" or "extension_ids"`;
324 context.logError(context.makeError(msg));
325 throw new Error(msg);
331 incognitoSplitUnsupportedAndFallback(value, context) {
332 if (value === "split") {
333 // incognito:split has not been implemented (bug 1380812). There are two
334 // alternatives: "spanning" and "not_allowed".
336 // "incognito":"split" is required by Chrome when extensions want to load
337 // any extension page in a tab in Chrome. In Firefox that is not required,
338 // so extensions could replace "split" with "spanning".
339 // Another (poorly documented) effect of "incognito":"split" is separation
340 // of some state between some extension APIs. Because this can in theory
341 // result in unwanted mixing of state between private and non-private
342 // browsing, we fall back to "not_allowed", which prevents the user from
343 // enabling the extension in private browsing windows.
344 value = "not_allowed";
346 `incognito "split" is unsupported. Falling back to incognito "${value}".`
353 // Parses a regular expression, with support for the Python extended
354 // syntax that allows setting flags by including the string (?im)
355 function parsePattern(pattern) {
357 let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
359 [, flags, pattern] = match;
361 return new RegExp(pattern, flags);
364 function getValueBaseType(value) {
365 let type = typeof value;
368 if (value === null) {
371 if (Array.isArray(value)) {
377 if (value % 1 === 0) {
384 // Methods of Context that are used by Schemas.normalize. These methods can be
385 // overridden at the construction of Context.
386 const CONTEXT_FOR_VALIDATION = ["checkLoadURL", "hasPermission", "logError"];
388 // Methods of Context that are used by Schemas.inject.
389 // Callers of Schemas.inject should implement all of these methods.
390 const CONTEXT_FOR_INJECTION = [
391 ...CONTEXT_FOR_VALIDATION,
393 "isPermissionRevokable",
397 // If the message is a function, call it and return the result.
398 // Otherwise, assume it's a string.
399 function forceString(msg) {
400 if (typeof msg === "function") {
407 * A context for schema validation and error reporting. This class is only used
408 * internally within Schemas.
412 * @param {object} params Provides the implementation of this class.
413 * @param {Array<string>} overridableMethods
415 constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) {
416 this.params = params;
418 if (typeof params.manifestVersion !== "number") {
420 `Unexpected params.manifestVersion value: ${params.manifestVersion}`
425 this.preprocessors = {
429 ...params.preprocessors,
432 this.postprocessors = POSTPROCESSORS;
433 this.isChromeCompat = params.isChromeCompat ?? false;
434 this.manifestVersion = params.manifestVersion;
436 this.currentChoices = new Set();
437 this.choicePathIndex = 0;
439 for (let method of overridableMethods) {
440 if (method in params) {
441 this[method] = params[method].bind(params);
447 let path = this.path.slice(this.choicePathIndex);
448 return path.join(".");
452 return this.params.cloneScope || undefined;
456 return this.params.url;
459 get ignoreUnrecognizedProperties() {
460 return !!this.params.ignoreUnrecognizedProperties;
465 this.params.principal ||
466 Services.scriptSecurityManager.createNullPrincipal({})
471 * Checks whether `url` may be loaded by the extension in this context.
473 * @param {string} url The URL that the extension wished to load.
474 * @returns {boolean} Whether the context may load `url`.
477 let ssm = Services.scriptSecurityManager;
479 ssm.checkLoadURIWithPrincipal(
481 Services.io.newURI(url),
482 ssm.DISALLOW_INHERIT_PRINCIPAL
491 * Checks whether this context has the given permission.
493 * @param {string} _permission
494 * The name of the permission to check.
496 * @returns {boolean} True if the context has the given permission.
498 hasPermission(_permission) {
503 * Checks whether the given permission can be dynamically revoked or
506 * @param {string} _permission
507 * The name of the permission to check.
509 * @returns {boolean} True if the given permission is revokable.
511 isPermissionRevokable(_permission) {
516 * Returns an error result object with the given message, for return
517 * by Type normalization functions.
519 * If the context has a `currentTarget` value, this is prepended to
520 * the message to indicate the location of the error.
522 * @param {string | Function} errorMessage
523 * The error message which will be displayed when this is the
524 * only possible matching schema. If a function is passed, it
525 * will be evaluated when the error string is first needed, and
526 * must return a string.
527 * @param {string | Function} choicesMessage
528 * The message describing the valid what constitutes a valid
529 * value for this schema, which will be displayed when multiple
530 * schema choices are available and none match.
532 * A caller may pass `null` to prevent a choice from being
533 * added, but this should *only* be done from code processing a
535 * @param {boolean} [warning = false]
536 * If true, make message prefixed `Warning`. If false, make message
540 error(errorMessage, choicesMessage = undefined, warning = false) {
541 if (choicesMessage !== null) {
542 let { choicePath } = this;
544 choicesMessage = `.${choicePath} must ${choicesMessage}`;
547 this.currentChoices.add(choicesMessage);
550 if (this.currentTarget) {
551 let { currentTarget } = this;
555 warning ? "Warning" : "Error"
556 } processing ${currentTarget}: ${forceString(errorMessage)}`,
559 return { error: errorMessage };
563 * Creates an `Error` object belonging to the current unprivileged
564 * scope. If there is no unprivileged scope associated with this
565 * context, the message is returned as a string.
567 * If the context has a `currentTarget` value, this is prepended to
568 * the message, in the same way as for the `error` method.
570 * @param {string} message
571 * @param {object} [options]
572 * @param {boolean} [options.warning = false]
575 makeError(message, { warning = false } = {}) {
576 let error = forceString(this.error(message, null, warning).error);
577 if (this.cloneScope) {
578 return new this.cloneScope.Error(error);
584 * Logs the given error to the console. May be overridden to enable
587 * @param {Error|string} error
590 if (this.cloneScope) {
592 // Error objects logged using Cu.reportError are not associated
593 // to the related innerWindowID. This results in a leaked docshell
594 // since consoleService cannot release the error object when the
595 // extension global is destroyed.
596 typeof error == "string" ? error : String(error),
597 // Report the error with the appropriate stack trace when the
598 // is related to an actual extension global (instead of being
599 // related to a manifest validation).
600 this.principal && ChromeUtils.getCallerLocation(this.principal)
603 Cu.reportError(error);
608 * Logs a warning. An error might be thrown when we treat warnings as errors.
610 * @param {string} warningMessage
612 logWarning(warningMessage) {
613 let error = this.makeError(warningMessage, { warning: true });
614 this.logError(error);
616 if (lazy.treatWarningsAsErrors) {
617 // This pref is false by default, and true by default in tests to
618 // discourage the use of deprecated APIs in our unit tests.
619 // If a warning is an expected part of a test, temporarily set the pref
620 // to false, e.g. with the ExtensionTestUtils.failOnSchemaWarnings helper.
621 Services.console.logStringMessage(
622 "Treating warning as error because the preference " +
623 "extensions.webextensions.warnings-as-errors is set to true"
625 if (typeof error === "string") {
626 error = new Error(error);
633 * Returns the name of the value currently being normalized. For a
634 * nested object, this is usually approximately equivalent to the
635 * JavaScript property accessor for that property. Given:
637 * { foo: { bar: [{ baz: x }] } }
639 * When processing the value for `x`, the currentTarget is
642 get currentTarget() {
643 return this.path.join(".");
647 * Executes the given callback, and returns an array of choice strings
648 * passed to {@see #error} during its execution.
650 * @param {Function} callback
652 * An object with a `result` property containing the return
653 * value of the callback, and a `choice` property containing
654 * an array of choices.
656 withChoices(callback) {
657 let { currentChoices, choicePathIndex } = this;
659 let choices = new Set();
660 this.currentChoices = choices;
661 this.choicePathIndex = this.path.length;
664 let result = callback();
666 return { result, choices };
668 this.currentChoices = currentChoices;
669 this.choicePathIndex = choicePathIndex;
671 if (choices.size == 1) {
672 for (let choice of choices) {
673 currentChoices.add(choice);
675 } else if (choices.size) {
676 this.error(null, () => {
677 let array = Array.from(choices, forceString);
678 let n = array.length - 1;
679 array[n] = `or ${array[n]}`;
681 return `must either [${array.join(", ")}]`;
688 * Appends the given component to the `currentTarget` path to indicate
689 * that it is being processed, calls the given callback function, and
690 * then restores the original path.
692 * This is used to identify the path of the property being processed
693 * when reporting type errors.
695 * @param {string} component
696 * @param {Function} callback
699 withPath(component, callback) {
700 this.path.push(component);
708 matchManifestVersion(entry) {
709 let { manifestVersion } = this;
711 manifestVersion >= entry.min_manifest_version &&
712 manifestVersion <= entry.max_manifest_version
718 * Represents a schema entry to be injected into an object. Handles the
719 * injection, revocation, and permissions of said entry.
721 * @param {InjectionContext} context
722 * The injection context for the entry.
723 * @param {Entry} entry
724 * The entry to inject.
725 * @param {object} parentObject
726 * The object into which to inject this entry.
727 * @param {string} name
728 * The property name at which to inject this entry.
729 * @param {Array<string>} path
730 * The full path from the root entry to this entry.
731 * @param {Entry} parentEntry
732 * The parent entry for the injected entry.
734 class InjectionEntry {
735 constructor(context, entry, parentObj, name, path, parentEntry) {
736 this.context = context;
738 this.parentObj = parentObj;
741 this.parentEntry = parentEntry;
743 this.injected = null;
744 this.lazyInjected = null;
748 * @property {Array<string>} allowedContexts
749 * The list of allowed contexts into which the entry may be
752 get allowedContexts() {
753 let { allowedContexts } = this.entry;
754 if (allowedContexts.length) {
755 return allowedContexts;
757 return this.parentEntry.defaultContexts;
761 * @property {boolean} isRevokable
762 * Returns true if this entry may be dynamically injected or
763 * revoked based on its permissions.
767 this.entry.permissions &&
768 this.entry.permissions.some(perm =>
769 this.context.isPermissionRevokable(perm)
775 * @property {boolean} hasPermission
776 * Returns true if the injection context currently has the
777 * appropriate permissions to access this entry.
779 get hasPermission() {
781 !this.entry.permissions ||
782 this.entry.permissions.some(perm => this.context.hasPermission(perm))
787 * @property {boolean} shouldInject
788 * Returns true if this entry should be injected in the given
789 * context, without respect to permissions.
793 this.context.matchManifestVersion(this.entry) &&
794 this.context.shouldInject(
803 * Revokes this entry, removing its property from its parent object,
804 * and invalidating its wrappers.
807 if (this.lazyInjected) {
808 this.lazyInjected = false;
809 } else if (this.injected) {
810 if (this.injected.revoke) {
811 this.injected.revoke();
815 let unwrapped = ChromeUtils.waiveXrays(this.parentObj);
816 delete unwrapped[this.name];
821 let { value } = this.injected.descriptor;
823 this.context.revokeChildren(value);
826 this.injected = null;
831 * Returns a property descriptor object for this entry, if it should
832 * be injected, or undefined if it should not.
835 * A property descriptor object, or undefined if the property
839 this.lazyInjected = false;
842 let path = [...this.path, this.name];
844 `Attempting to re-inject already injected entry: ${path.join(".")}`
848 if (!this.shouldInject) {
852 if (this.isRevokable) {
853 this.context.pendingEntries.add(this);
856 if (!this.hasPermission) {
860 this.injected = this.entry.getDescriptor(this.path, this.context);
861 if (!this.injected) {
865 return this.injected.descriptor;
869 * Injects a lazy property descriptor into the parent object which
870 * checks permissions and eligibility for injection the first time it
874 if (this.lazyInjected || this.injected) {
875 let path = [...this.path, this.name];
877 `Attempting to re-lazy-inject already injected entry: ${path.join(".")}`
881 this.lazyInjected = true;
882 exportLazyProperty(this.parentObj, this.name, () => {
883 if (this.lazyInjected) {
884 return this.getDescriptor();
890 * Injects or revokes this entry if its current state does not match
891 * the context's current permissions.
893 permissionsChanged() {
902 if (!this.injected && !this.lazyInjected) {
908 if (this.injected && !this.hasPermission) {
915 * Holds methods that run the actual implementation of the extension APIs. These
916 * methods are only called if the extension API invocation matches the signature
917 * as defined in the schema. Otherwise an error is reported to the context.
919 class InjectionContext extends Context {
920 constructor(params, schemaRoot) {
921 super(params, CONTEXT_FOR_INJECTION);
923 this.schemaRoot = schemaRoot;
925 this.pendingEntries = new Set();
926 this.children = new DefaultWeakMap(() => new Map());
928 this.injectedRoots = new Set();
930 if (params.setPermissionsChangedCallback) {
931 params.setPermissionsChangedCallback(this.permissionsChanged.bind(this));
936 * Check whether the API should be injected.
939 * @param {string} _namespace The namespace of the API. This may contain dots,
940 * e.g. in the case of "devtools.inspectedWindow".
941 * @param {string?} _name The name of the property in the namespace.
942 * `null` if we are checking whether the namespace should be injected.
943 * @param {Array<string>} _allowedContexts A list of additional contexts in
944 * which this API should be available. May include any of:
945 * "main" - The main chrome browser process.
946 * "addon" - An addon process.
947 * "content" - A content process.
948 * @returns {boolean} Whether the API should be injected.
950 shouldInject(_namespace, _name, _allowedContexts) {
951 throw new Error("Not implemented");
955 * Generate the implementation for `namespace`.`name`.
958 * @param {string} _namespace The full path to the namespace of the API, minus
959 * the name of the method or property. E.g. "storage.local".
960 * @param {string} _name The name of the method, property or event.
961 * @returns {import("ExtensionCommon.sys.mjs").SchemaAPIInterface}
962 * The implementation of the API.
964 getImplementation(_namespace, _name) {
965 throw new Error("Not implemented");
969 * Updates all injection entries which may need to be updated after a
970 * permission change, revoking or re-injecting them as necessary.
972 permissionsChanged() {
973 for (let entry of this.pendingEntries) {
975 entry.permissionsChanged();
983 * Recursively revokes all child injection entries of the given
986 * @param {object} object
987 * The object for which to invoke children.
989 revokeChildren(object) {
990 if (!this.children.has(object)) {
994 let children = this.children.get(object);
995 for (let [name, entry] of children.entries()) {
1001 children.delete(name);
1003 // When we revoke children for an object, we consider that object
1004 // dead. If the entry is ever reified again, a new object is
1005 // created, with new child entries.
1006 this.pendingEntries.delete(entry);
1008 this.children.delete(object);
1011 _getInjectionEntry(entry, dest, name, path, parentEntry) {
1012 let injection = new InjectionEntry(
1021 this.children.get(dest).set(name, injection);
1027 * Returns the property descriptor for the given entry.
1029 * @param {Entry} entry
1030 * The entry instance to return a descriptor for.
1031 * @param {object} dest
1032 * The object into which this entry is being injected.
1033 * @param {string} name
1034 * The property name on the destination object where the entry
1036 * @param {Array<string>} path
1037 * The full path from the root injection object to this entry.
1038 * @param {Partial<Entry>} parentEntry
1039 * The parent entry for this entry.
1041 * @returns {object?}
1042 * A property descriptor object, or null if the entry should
1045 getDescriptor(entry, dest, name, path, parentEntry) {
1046 let injection = this._getInjectionEntry(
1054 return injection.getDescriptor();
1058 * Lazily injects the given entry into the given object.
1060 * @param {Entry} entry
1061 * The entry instance to lazily inject.
1062 * @param {object} dest
1063 * The object into which to inject this entry.
1064 * @param {string} name
1065 * The property name at which to inject the entry.
1066 * @param {Array<string>} path
1067 * The full path from the root injection object to this entry.
1068 * @param {Entry} parentEntry
1069 * The parent entry for this entry.
1071 injectInto(entry, dest, name, path, parentEntry) {
1072 let injection = this._getInjectionEntry(
1080 injection.lazyInject();
1085 * The methods in this singleton represent the "format" specifier for
1086 * JSON Schema string types.
1088 * Each method either returns a normalized version of the original
1089 * value, or throws an error if the value is not valid for the given
1094 // TODO bug 1797376: Despite the name, this format is NOT a "hostname",
1095 // but hostname + port and may fail with IPv6. Use canonicalDomain instead.
1099 valid = new URL(`http://${string}`).host === string;
1105 throw new Error(`Invalid hostname ${string}`);
1111 canonicalDomain(string) {
1115 valid = new URL(`http://${string}`).hostname === string;
1121 // Require the input to be a canonical domain.
1122 // Rejects obvious non-domains such as URLs,
1123 // but also catches non-IDN (punycode) domains.
1124 throw new Error(`Invalid domain ${string}`);
1130 url(string, context) {
1131 let url = new URL(string).href;
1133 if (!context.checkLoadURL(url)) {
1134 throw new Error(`Access denied for URL ${url}`);
1139 origin(string, context) {
1142 url = new URL(string);
1144 throw new Error(`Invalid origin: ${string}`);
1146 if (!/^https?:/.test(url.protocol)) {
1147 throw new Error(`Invalid origin must be http or https for URL ${string}`);
1149 // url.origin is punycode so a direct check against string wont work.
1150 // url.href appends a slash even if not in the original string, we we
1151 // additionally check that string does not end in slash.
1152 if (string.endsWith("/") || url.href != new URL(url.origin).href) {
1154 `Invalid origin for URL ${string}, replace with origin ${url.origin}`
1157 if (!context.checkLoadURL(url.origin)) {
1158 throw new Error(`Access denied for URL ${url}`);
1163 relativeUrl(string, context) {
1165 // If there's no context URL, return relative URLs unresolved, and
1166 // skip security checks for them.
1174 let url = new URL(string, context.url).href;
1176 if (!context.checkLoadURL(url)) {
1177 throw new Error(`Access denied for URL ${url}`);
1182 strictRelativeUrl(string, context) {
1183 void FORMATS.unresolvedRelativeUrl(string);
1184 return FORMATS.relativeUrl(string, context);
1187 unresolvedRelativeUrl(string) {
1188 if (!string.startsWith("//")) {
1196 throw new SyntaxError(
1197 `String ${JSON.stringify(string)} must be a relative URL`
1201 homepageUrl(string, context) {
1202 // Pipes are used for separating homepages, but we only allow extensions to
1203 // set a single homepage. Encoding any pipes makes it one URL.
1204 return FORMATS.relativeUrl(
1205 string.replace(new RegExp("\\|", "g"), "%7C"),
1210 imageDataOrStrictRelativeUrl(string, context) {
1211 // Do not accept a string which resolves as an absolute URL, or any
1212 // protocol-relative URL, except PNG or JPG data URLs
1214 !string.startsWith("data:image/png;base64,") &&
1215 !string.startsWith("data:image/jpeg;base64,")
1218 return FORMATS.strictRelativeUrl(string, context);
1220 throw new SyntaxError(
1221 `String ${JSON.stringify(
1223 )} must be a relative or PNG or JPG data:image URL`
1230 contentSecurityPolicy(string, context) {
1231 // Manifest V3 extension_pages allows WASM. When sandbox is
1232 // implemented, or any other V3 or later directive, the flags
1233 // logic will need to be updated.
1235 context.manifestVersion < 3
1236 ? Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY
1237 : Ci.nsIAddonContentPolicy.CSP_ALLOW_WASM;
1238 let error = lazy.contentPolicyService.validateAddonCSP(string, flags);
1239 if (error != null) {
1240 // The CSP validation error is not reported as part of the "choices" error message,
1241 // we log the CSP validation error explicitly here to make it easier for the addon developers
1242 // to see and fix the extension CSP.
1243 context.logError(`Error processing ${context.currentTarget}: ${error}`);
1250 // A valid ISO 8601 timestamp.
1252 /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/;
1253 if (!PATTERN.test(string)) {
1254 throw new Error(`Invalid date string ${string}`);
1256 // Our pattern just checks the format, we could still have invalid
1257 // values (e.g., month=99 or month=02 and day=31). Let the Date
1258 // constructor do the dirty work of validating.
1259 if (isNaN(Date.parse(string))) {
1260 throw new Error(`Invalid date string ${string}`);
1265 manifestShortcutKey(string) {
1266 if (lazy.ShortcutUtils.validate(string) == lazy.ShortcutUtils.IS_VALID) {
1270 `Value "${string}" must consist of ` +
1271 `either a combination of one or two modifiers, including ` +
1272 `a mandatory primary modifier and a key, separated by '+', ` +
1273 `or a media key. For details see: ` +
1274 `https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`;
1275 throw new Error(errorMessage);
1278 manifestShortcutKeyOrEmpty(string) {
1279 return string === "" ? "" : FORMATS.manifestShortcutKey(string);
1282 versionString(string, context) {
1283 const parts = string.split(".");
1286 // We accept up to 4 numbers.
1288 // Non-zero values cannot start with 0 and we allow numbers up to 9 digits.
1289 parts.some(part => !/^(0|[1-9][0-9]{0,8})$/.test(part))
1292 `version must be a version string consisting of at most 4 integers ` +
1293 `of at most 9 digits without leading zeros, and separated with dots`
1297 // The idea is to only emit a warning when the version string does not
1298 // match the simple format we want to encourage developers to use. Given
1299 // the version is required, we always accept the value as is.
1304 // Schema files contain namespaces, and each namespace contains types,
1305 // properties, functions, and events. An Entry is a base class for
1306 // types, properties, functions, and events.
1308 constructor(schema = {}) {
1310 * If set to any value which evaluates as true, this entry is
1311 * deprecated, and any access to it will result in a deprecation
1312 * warning being logged to the browser console.
1314 * If the value is a string, it will be appended to the deprecation
1315 * message. If it contains the substring "${value}", it will be
1316 * replaced with a string representation of the value being
1319 * If the value is any other truthy value, a generic deprecation
1320 * message will be emitted.
1322 this.deprecated = false;
1323 if ("deprecated" in schema) {
1324 this.deprecated = schema.deprecated;
1328 * @property {string} [preprocessor]
1329 * If set to a string value, and a preprocessor of the same is
1330 * defined in the validation context, it will be applied to this
1331 * value prior to any normalization.
1333 this.preprocessor = schema.preprocess || null;
1336 * @property {string} [postprocessor]
1337 * If set to a string value, and a postprocessor of the same is
1338 * defined in the validation context, it will be applied to this
1339 * value after any normalization.
1341 this.postprocessor = schema.postprocess || null;
1344 * @property {Array<string>} allowedContexts A list of allowed contexts
1345 * to consider before generating the API.
1346 * These are not parsed by the schema, but passed to `shouldInject`.
1348 this.allowedContexts = schema.allowedContexts || [];
1350 this.min_manifest_version =
1351 schema.min_manifest_version ?? MIN_MANIFEST_VERSION;
1352 this.max_manifest_version =
1353 schema.max_manifest_version ?? MAX_MANIFEST_VERSION;
1357 * Preprocess the given value with the preprocessor declared in
1361 * @param {Context} context
1364 preprocess(value, context) {
1365 if (this.preprocessor) {
1366 return context.preprocessors[this.preprocessor](value, context);
1372 * Postprocess the given result with the postprocessor declared in
1375 * @param {object} result
1376 * @param {Context} context
1379 postprocess(result, context) {
1380 if (result.error || !this.postprocessor) {
1384 let value = context.postprocessors[this.postprocessor](
1392 * Logs a deprecation warning for this entry, based on the value of
1393 * its `deprecated` property.
1395 * @param {Context} context
1396 * @param {any} [value]
1398 logDeprecation(context, value = null) {
1399 let message = "This property is deprecated";
1400 if (typeof this.deprecated == "string") {
1401 message = this.deprecated;
1402 if (message.includes("${value}")) {
1404 value = JSON.stringify(value);
1406 value = String(value);
1408 message = message.replace(/\$\{value\}/g, () => value);
1412 context.logWarning(message);
1416 * Checks whether the entry is deprecated and, if so, logs a
1417 * deprecation message.
1419 * @param {Context} context
1420 * @param {any} [value]
1422 checkDeprecated(context, value = null) {
1423 if (this.deprecated) {
1424 this.logDeprecation(context, value);
1429 * Returns an object containing property descriptor for use when
1430 * injecting this entry into an API object.
1432 * @param {Array<string>} _path The API path, e.g. `["storage", "local"]`.
1433 * @param {InjectionContext} _context
1435 * @returns {object?}
1436 * An object containing a `descriptor` property, specifying the
1437 * entry's property descriptor, and an optional `revoke`
1438 * method, to be called when the entry is being revoked.
1440 getDescriptor(_path, _context) {
1445 // Corresponds either to a type declared in the "types" section of the
1446 // schema or else to any type object used throughout the schema.
1447 class Type extends Entry {
1449 * @property {Array<string>} EXTRA_PROPERTIES
1450 * An array of extra properties which may be present for
1451 * schemas of this type.
1453 static get EXTRA_PROPERTIES() {
1461 "min_manifest_version",
1462 "max_manifest_version",
1467 * Parses the given schema object and returns an instance of this
1468 * class which corresponds to its properties.
1470 * @param {SchemaRoot} root
1471 * The root schema for this type.
1472 * @param {object} schema
1473 * A JSON schema object which corresponds to a definition of
1475 * @param {Array<string>} path
1476 * The path to this schema object from the root schema,
1477 * corresponding to the property names and array indices
1478 * traversed during parsing in order to arrive at this schema
1480 * @param {Array<string>} [extraProperties]
1481 * An array of extra property names which are valid for this
1482 * schema in the current context.
1484 * An instance of this type which corresponds to the given
1488 static parseSchema(root, schema, path, extraProperties = []) {
1489 this.checkSchemaProperties(schema, path, extraProperties);
1491 return new this(schema);
1495 * Checks that all of the properties present in the given schema
1496 * object are valid properties for this type, and throws if invalid.
1498 * @param {object} schema
1499 * A JSON schema object.
1500 * @param {Array<string>} path
1501 * The path to this schema object from the root schema,
1502 * corresponding to the property names and array indices
1503 * traversed during parsing in order to arrive at this schema
1505 * @param {Iterable<string>} [extra]
1506 * An array of extra property names which are valid for this
1507 * schema in the current context.
1509 * An error describing the first invalid property found in the
1512 static checkSchemaProperties(schema, path, extra = []) {
1514 let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]);
1516 for (let prop of Object.keys(schema)) {
1517 if (!allowedSet.has(prop)) {
1519 `Internal error: Namespace ${path.join(".")} has ` +
1520 `invalid type property "${prop}" ` +
1521 `in type "${schema.id || JSON.stringify(schema)}"`
1529 * Takes a value, checks that it has the correct type, and returns a
1530 * "normalized" version of the value. The normalized version will
1531 * include "nulls" in place of omitted optional properties. The
1532 * result of this function is either {error: "Some type error"} or
1533 * {value: <normalized-value>}.
1535 normalize(value, context) {
1536 return context.error("invalid type");
1540 * Unlike normalize, this function does a shallow check to see if
1541 * |baseType| (one of the possible getValueBaseType results) is
1542 * valid for this type. It returns true or false. It's used to fill
1543 * in optional arguments to functions before actually type checking
1545 * @param {string} _baseType
1547 checkBaseType(_baseType) {
1552 * Helper method that simply relies on checkBaseType to implement
1553 * normalize. Subclasses can choose to use it or not.
1555 normalizeBase(type, value, context) {
1556 if (this.checkBaseType(getValueBaseType(value))) {
1557 this.checkDeprecated(context, value);
1558 return { value: this.preprocess(value, context) };
1562 if ("aeiou".includes(type[0])) {
1563 choice = `be an ${type} value`;
1565 choice = `be a ${type} value`;
1568 return context.error(
1569 () => `Expected ${type} instead of ${JSON.stringify(value)}`,
1575 // Type that allows any value.
1576 class AnyType extends Type {
1577 normalize(value, context) {
1578 this.checkDeprecated(context, value);
1579 return this.postprocess({ value }, context);
1587 // An untagged union type.
1588 class ChoiceType extends Type {
1589 static get EXTRA_PROPERTIES() {
1590 return ["choices", ...super.EXTRA_PROPERTIES];
1593 /** @type {(root, schema, path, extraProperties?: Iterable) => ChoiceType} */
1594 static parseSchema(root, schema, path, extraProperties = []) {
1595 this.checkSchemaProperties(schema, path, extraProperties);
1597 let choices = schema.choices.map(t => root.parseSchema(t, path));
1598 return new this(schema, choices);
1601 constructor(schema, choices) {
1603 this.choices = choices;
1607 this.choices.push(...type.choices);
1612 normalize(value, context) {
1613 this.checkDeprecated(context, value);
1616 let { choices, result } = context.withChoices(() => {
1617 for (let choice of this.choices) {
1618 // Ignore a possible choice if it is not supported by
1619 // the manifest version we are normalizing.
1620 if (!context.matchManifestVersion(choice)) {
1624 let r = choice.normalize(value, context);
1636 if (choices.size <= 1) {
1640 choices = Array.from(choices, forceString);
1641 let n = choices.length - 1;
1642 choices[n] = `or ${choices[n]}`;
1645 if (typeof value === "object") {
1646 message = () => `Value must either: ${choices.join(", ")}`;
1649 `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`;
1652 return context.error(message, null);
1655 checkBaseType(baseType) {
1656 return this.choices.some(t => t.checkBaseType(baseType));
1659 getDescriptor(path, context) {
1660 // In StringType.getDescriptor, unlike any other Type, a descriptor is returned if
1661 // it is an enumeration. Since we need versioned choices in some cases, here we
1662 // build a list of valid enumerations that will work for a given manifest version.
1664 !this.choices.length ||
1665 !this.choices.every(t => t.checkBaseType("string") && t.enumeration)
1670 let obj = Cu.createObjectIn(context.cloneScope);
1671 let descriptor = { value: obj };
1672 for (let choice of this.choices) {
1673 // Ignore a possible choice if it is not supported by
1674 // the manifest version we are normalizing.
1675 if (!context.matchManifestVersion(choice)) {
1678 let d = choice.getDescriptor(path, context);
1680 Object.assign(obj, d.descriptor.value);
1684 return { descriptor };
1688 // This is a reference to another type--essentially a typedef.
1689 class RefType extends Type {
1690 static get EXTRA_PROPERTIES() {
1691 return ["$ref", ...super.EXTRA_PROPERTIES];
1694 /** @type {(root, schema, path, extraProperties?: Iterable) => RefType} */
1695 static parseSchema(root, schema, path, extraProperties = []) {
1696 this.checkSchemaProperties(schema, path, extraProperties);
1698 let ref = schema.$ref;
1699 let ns = path.join(".");
1700 if (ref.includes(".")) {
1701 [, ns, ref] = /^(.*)\.(.*?)$/.exec(ref);
1703 return new this(root, schema, ns, ref);
1706 // For a reference to a type named T declared in namespace NS,
1707 // namespaceName will be NS and reference will be T.
1708 constructor(root, schema, namespaceName, reference) {
1711 this.namespaceName = namespaceName;
1712 this.reference = reference;
1716 let ns = this.root.getNamespace(this.namespaceName);
1717 let type = ns.get(this.reference);
1719 throw new Error(`Internal error: Type ${this.reference} not found`);
1724 normalize(value, context) {
1725 this.checkDeprecated(context, value);
1726 return this.targetType.normalize(value, context);
1729 checkBaseType(baseType) {
1730 return this.targetType.checkBaseType(baseType);
1734 class StringType extends Type {
1735 static get EXTRA_PROPERTIES() {
1742 ...super.EXTRA_PROPERTIES,
1746 static parseSchema(root, schema, path, extraProperties = []) {
1747 this.checkSchemaProperties(schema, path, extraProperties);
1749 let enumeration = schema.enum || null;
1751 // The "enum" property is either a list of strings that are
1752 // valid values or else a list of {name, description} objects,
1753 // where the .name values are the valid values.
1754 enumeration = enumeration.map(e => {
1755 if (typeof e == "object") {
1763 if (schema.pattern) {
1765 pattern = parsePattern(schema.pattern);
1768 `Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}`
1774 if (schema.format) {
1775 if (!(schema.format in FORMATS)) {
1777 `Internal error: Invalid string format ${schema.format}`
1780 format = FORMATS[schema.format];
1784 schema.id || undefined,
1786 schema.minLength || 0,
1787 schema.maxLength || Infinity,
1804 this.enumeration = enumeration;
1805 this.minLength = minLength;
1806 this.maxLength = maxLength;
1807 this.pattern = pattern;
1808 this.format = format;
1811 normalize(value, context) {
1812 let r = this.normalizeBase("string", value, context);
1818 if (this.enumeration) {
1819 if (this.enumeration.includes(value)) {
1820 return this.postprocess({ value }, context);
1823 let choices = this.enumeration.map(JSON.stringify).join(", ");
1825 return context.error(
1826 () => `Invalid enumeration value ${JSON.stringify(value)}`,
1827 `be one of [${choices}]`
1831 if (value.length < this.minLength) {
1832 return context.error(
1834 `String ${JSON.stringify(value)} is too short (must be ${
1837 `be longer than ${this.minLength}`
1840 if (value.length > this.maxLength) {
1841 return context.error(
1843 `String ${JSON.stringify(value)} is too long (must be ${
1846 `be shorter than ${this.maxLength}`
1850 if (this.pattern && !this.pattern.test(value)) {
1851 return context.error(
1852 () => `String ${JSON.stringify(value)} must match ${this.pattern}`,
1853 `match the pattern ${this.pattern.toSource()}`
1859 r.value = this.format(r.value, context);
1861 return context.error(
1863 `match the format "${this.format.name}"`
1871 checkBaseType(baseType) {
1872 return baseType == "string";
1875 getDescriptor(path, context) {
1876 if (this.enumeration) {
1877 let obj = Cu.createObjectIn(context.cloneScope);
1879 for (let e of this.enumeration) {
1880 obj[e.toUpperCase()] = e;
1884 descriptor: { value: obj },
1890 class NullType extends Type {
1891 normalize(value, context) {
1892 return this.normalizeBase("null", value, context);
1895 checkBaseType(baseType) {
1896 return baseType == "null";
1904 class ObjectType extends Type {
1905 static get EXTRA_PROPERTIES() {
1908 "patternProperties",
1910 ...super.EXTRA_PROPERTIES,
1914 static parseSchema(root, schema, path, extraProperties = []) {
1915 if ("functions" in schema) {
1916 return SubModuleType.parseSchema(root, schema, path, extraProperties);
1919 if (DEBUG && !("$extend" in schema)) {
1920 // Only allow extending "properties" and "patternProperties".
1922 "additionalProperties",
1927 this.checkSchemaProperties(schema, path, extraProperties);
1929 let imported = null;
1930 if ("$import" in schema) {
1931 let importPath = schema.$import;
1932 let idx = importPath.indexOf(".");
1934 imported = [path[0], importPath];
1936 imported = [importPath.slice(0, idx), importPath.slice(idx + 1)];
1940 let parseProperty = (schema, extraProps = []) => {
1942 type: root.parseSchema(
1953 optional: schema.optional || false,
1954 unsupported: schema.unsupported || false,
1955 onError: schema.onError || null,
1956 default: schema.default === undefined ? null : schema.default,
1960 // Parse explicit "properties" object.
1961 let properties = Object.create(null);
1962 for (let propName of Object.keys(schema.properties || {})) {
1963 properties[propName] = parseProperty(schema.properties[propName], [
1968 // Parse regexp properties from "patternProperties" object.
1969 let patternProperties = [];
1970 for (let propName of Object.keys(schema.patternProperties || {})) {
1973 pattern = parsePattern(propName);
1976 `Internal error: Invalid property pattern ${JSON.stringify(propName)}`
1980 patternProperties.push({
1982 type: parseProperty(schema.patternProperties[propName]),
1986 // Parse "additionalProperties" schema.
1987 let additionalProperties = null;
1988 if (schema.additionalProperties) {
1989 let type = schema.additionalProperties;
1990 if (type === true) {
1991 type = { type: "any" };
1994 additionalProperties = root.parseSchema(type, path);
2000 additionalProperties,
2002 schema.isInstanceOf || null,
2010 additionalProperties,
2016 this.properties = properties;
2017 this.additionalProperties = additionalProperties;
2018 this.patternProperties = patternProperties;
2019 this.isInstanceOf = isInstanceOf;
2022 let [ns, path] = imported;
2023 ns = Schemas.getNamespace(ns);
2024 let importedType = ns.get(path);
2025 if (!importedType) {
2026 throw new Error(`Internal error: imported type ${path} not found`);
2029 if (DEBUG && !(importedType instanceof ObjectType)) {
2031 `Internal error: cannot import non-object type ${path}`
2035 this.properties = Object.assign(
2037 importedType.properties,
2040 this.patternProperties = [
2041 ...importedType.patternProperties,
2042 ...this.patternProperties,
2044 this.additionalProperties =
2045 importedType.additionalProperties || this.additionalProperties;
2050 for (let key of Object.keys(type.properties)) {
2051 if (key in this.properties) {
2053 `InternalError: Attempt to extend an object with conflicting property "${key}"`
2056 this.properties[key] = type.properties[key];
2059 this.patternProperties.push(...type.patternProperties);
2064 checkBaseType(baseType) {
2065 return baseType == "object";
2069 * Extracts the enumerable properties of the given object, including
2070 * function properties which would normally be omitted by X-ray
2073 * @param {object} value
2074 * @param {Context} context
2075 * The current parse context.
2077 * An object with an `error` or `value` property.
2079 extractProperties(value, context) {
2080 // |value| should be a JS Xray wrapping an object in the
2081 // extension compartment. This works well except when we need to
2082 // access callable properties on |value| since JS Xrays don't
2083 // support those. To work around the problem, we verify that
2084 // |value| is a plain JS object (i.e., not anything scary like a
2085 // Proxy). Then we copy the properties out of it into a normal
2086 // object using a waiver wrapper.
2088 let klass = ChromeUtils.getClassName(value, true);
2089 if (klass != "Object") {
2090 throw context.error(
2091 `Expected a plain JavaScript object, got a ${klass}`,
2092 `be a plain JavaScript object`
2096 return ChromeUtils.shallowClone(value);
2099 checkProperty(context, prop, propType, result, properties, remainingProps) {
2100 let { type, optional, unsupported, onError } = propType;
2103 if (!context.matchManifestVersion(type)) {
2104 if (prop in properties) {
2105 error = context.error(
2106 `Property "${prop}" is unsupported in Manifest Version ${context.manifestVersion}`,
2107 `not contain an unsupported "${prop}" property`
2110 context.logWarning(forceString(error.error));
2111 if (this.additionalProperties) {
2112 // When `additionalProperties` is set to UnrecognizedProperty, the
2113 // caller (i.e. ObjectType's normalize method) assigns the original
2114 // value to `result[prop]`. Erase the property now to prevent
2115 // `result[prop]` from becoming anything other than `undefined.
2117 // A warning was already logged above, so we do not need to also log
2118 // "An unexpected property was found in the WebExtension manifest."
2119 remainingProps.delete(prop);
2121 // When `additionalProperties` is not set, ObjectType's normalize method
2122 // will return an error because prop is still in remainingProps.
2125 } else if (unsupported) {
2126 if (prop in properties) {
2127 error = context.error(
2128 `Property "${prop}" is unsupported by Firefox`,
2129 `not contain an unsupported "${prop}" property`
2132 } else if (prop in properties) {
2135 (properties[prop] === null || properties[prop] === undefined)
2137 result[prop] = propType.default;
2139 let r = context.withPath(prop, () =>
2140 type.normalize(properties[prop], context)
2145 result[prop] = r.value;
2146 properties[prop] = r.value;
2149 remainingProps.delete(prop);
2150 } else if (!optional) {
2151 error = context.error(
2152 `Property "${prop}" is required`,
2153 `contain the required "${prop}" property`
2155 } else if (optional !== "omit-key-if-missing") {
2156 result[prop] = propType.default;
2160 if (onError == "warn") {
2161 context.logWarning(forceString(error.error));
2162 } else if (onError != "ignore") {
2166 result[prop] = propType.default;
2170 normalize(value, context) {
2172 let v = this.normalizeBase("object", value, context);
2178 if (this.isInstanceOf) {
2181 Object.keys(this.properties).length ||
2182 this.patternProperties.length ||
2183 !(this.additionalProperties instanceof AnyType)
2186 "InternalError: isInstanceOf can only be used " +
2187 "with objects that are otherwise unrestricted"
2193 ChromeUtils.getClassName(value) !== this.isInstanceOf &&
2194 (this.isInstanceOf !== "Element" || value.nodeType !== 1)
2196 return context.error(
2197 `Object must be an instance of ${this.isInstanceOf}`,
2198 `be an instance of ${this.isInstanceOf}`
2202 // This is kind of a hack, but we can't normalize things that
2203 // aren't JSON, so we just return them.
2204 return this.postprocess({ value }, context);
2207 let properties = this.extractProperties(value, context);
2208 let remainingProps = new Set(Object.keys(properties));
2211 for (let prop of Object.keys(this.properties)) {
2215 this.properties[prop],
2222 for (let prop of Object.keys(properties)) {
2223 for (let { pattern, type } of this.patternProperties) {
2224 if (pattern.test(prop)) {
2237 if (this.additionalProperties) {
2238 for (let prop of remainingProps) {
2239 let r = context.withPath(prop, () =>
2240 this.additionalProperties.normalize(properties[prop], context)
2245 result[prop] = r.value;
2247 } else if (remainingProps.size && !context.ignoreUnrecognizedProperties) {
2248 if (remainingProps.size == 1) {
2249 return context.error(
2250 `Unexpected property "${[...remainingProps]}"`,
2251 `not contain an unexpected "${[...remainingProps]}" property`
2253 } else if (remainingProps.size) {
2254 let props = [...remainingProps].sort().join(", ");
2255 return context.error(
2256 `Unexpected properties: ${props}`,
2257 `not contain the unexpected properties [${props}]`
2262 return this.postprocess({ value: result }, context);
2272 // This type is just a placeholder to be referred to by
2273 // SubModuleProperty. No value is ever expected to have this type.
2274 SubModuleType = class SubModuleType extends Type {
2275 static get EXTRA_PROPERTIES() {
2276 return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
2279 static parseSchema(root, schema, path, extraProperties = []) {
2280 this.checkSchemaProperties(schema, path, extraProperties);
2282 // The path we pass in here is only used for error messages.
2283 path = [...path, schema.id];
2284 let functions = schema.functions
2285 .filter(fun => !fun.unsupported)
2286 .map(fun => FunctionEntry.parseSchema(root, fun, path));
2290 if (schema.events) {
2291 events = schema.events
2292 .filter(event => !event.unsupported)
2293 .map(event => Event.parseSchema(root, event, path));
2296 return new this(schema, functions, events);
2299 constructor(schema, functions, events) {
2300 // schema contains properties such as min/max_manifest_version needed
2301 // in the base class so that the Context class can version compare
2302 // any entries against the manifest version.
2304 this.functions = functions;
2305 this.events = events;
2309 class NumberType extends Type {
2310 normalize(value, context) {
2311 let r = this.normalizeBase("number", value, context);
2316 if (isNaN(r.value) || !Number.isFinite(r.value)) {
2317 return context.error(
2318 "NaN and infinity are not valid",
2319 "be a finite number"
2326 checkBaseType(baseType) {
2327 return baseType == "number" || baseType == "integer";
2331 class IntegerType extends Type {
2332 static get EXTRA_PROPERTIES() {
2333 return ["minimum", "maximum", ...super.EXTRA_PROPERTIES];
2336 static parseSchema(root, schema, path, extraProperties = []) {
2337 this.checkSchemaProperties(schema, path, extraProperties);
2339 let { minimum = -Infinity, maximum = Infinity } = schema;
2340 return new this(schema, minimum, maximum);
2343 constructor(schema, minimum, maximum) {
2345 this.minimum = minimum;
2346 this.maximum = maximum;
2349 normalize(value, context) {
2350 let r = this.normalizeBase("integer", value, context);
2356 // Ensure it's between -2**31 and 2**31-1
2357 if (!Number.isSafeInteger(value)) {
2358 return context.error(
2359 "Integer is out of range",
2360 "be a valid 32 bit signed integer"
2364 if (value < this.minimum) {
2365 return context.error(
2366 `Integer ${value} is too small (must be at least ${this.minimum})`,
2367 `be at least ${this.minimum}`
2370 if (value > this.maximum) {
2371 return context.error(
2372 `Integer ${value} is too big (must be at most ${this.maximum})`,
2373 `be no greater than ${this.maximum}`
2377 return this.postprocess(r, context);
2380 checkBaseType(baseType) {
2381 return baseType == "integer";
2385 class BooleanType extends Type {
2386 static get EXTRA_PROPERTIES() {
2387 return ["enum", ...super.EXTRA_PROPERTIES];
2390 static parseSchema(root, schema, path, extraProperties = []) {
2391 this.checkSchemaProperties(schema, path, extraProperties);
2392 let enumeration = schema.enum || null;
2393 return new this(schema, enumeration);
2396 constructor(schema, enumeration) {
2398 this.enumeration = enumeration;
2401 normalize(value, context) {
2402 if (!this.checkBaseType(getValueBaseType(value))) {
2403 return context.error(
2404 () => `Expected boolean instead of ${JSON.stringify(value)}`,
2408 value = this.preprocess(value, context);
2409 if (this.enumeration && !this.enumeration.includes(value)) {
2410 return context.error(
2411 () => `Invalid value ${JSON.stringify(value)}`,
2412 `be ${this.enumeration}`
2415 this.checkDeprecated(context, value);
2419 checkBaseType(baseType) {
2420 return baseType == "boolean";
2424 class ArrayType extends Type {
2425 static get EXTRA_PROPERTIES() {
2426 return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
2429 static parseSchema(root, schema, path, extraProperties = []) {
2430 this.checkSchemaProperties(schema, path, extraProperties);
2432 let items = root.parseSchema(schema.items, path, ["onError"]);
2437 schema.minItems || 0,
2438 schema.maxItems || Infinity
2442 constructor(schema, itemType, minItems, maxItems) {
2444 this.itemType = itemType;
2445 this.minItems = minItems;
2446 this.maxItems = maxItems;
2447 this.onError = schema.items.onError || null;
2450 normalize(value, context) {
2451 let v = this.normalizeBase("array", value, context);
2458 for (let [i, element] of value.entries()) {
2459 element = context.withPath(String(i), () =>
2460 this.itemType.normalize(element, context)
2462 if (element.error) {
2463 if (this.onError == "warn") {
2464 context.logWarning(forceString(element.error));
2465 } else if (this.onError != "ignore") {
2470 result.push(element.value);
2473 if (result.length < this.minItems) {
2474 return context.error(
2475 `Array requires at least ${this.minItems} items; you have ${result.length}`,
2476 `have at least ${this.minItems} items`
2480 if (result.length > this.maxItems) {
2481 return context.error(
2482 `Array requires at most ${this.maxItems} items; you have ${result.length}`,
2483 `have at most ${this.maxItems} items`
2487 return this.postprocess({ value: result }, context);
2490 checkBaseType(baseType) {
2491 return baseType == "array";
2495 class FunctionType extends Type {
2496 static get EXTRA_PROPERTIES() {
2502 ...super.EXTRA_PROPERTIES,
2506 static parseSchema(root, schema, path, extraProperties = []) {
2507 this.checkSchemaProperties(schema, path, extraProperties);
2509 let isAsync = !!schema.async;
2510 let isExpectingCallback = typeof schema.async === "string";
2511 let parameters = null;
2512 if ("parameters" in schema) {
2514 for (let param of schema.parameters) {
2515 // Callbacks default to optional for now, because of promise
2517 let isCallback = isAsync && param.name == schema.async;
2519 isExpectingCallback = false;
2523 type: root.parseSchema(param, path, ["name", "optional", "default"]),
2525 optional: param.optional == null ? isCallback : param.optional,
2526 default: param.default == undefined ? null : param.default,
2530 let hasAsyncCallback = false;
2534 parameters.length &&
2535 parameters[parameters.length - 1].name == schema.async;
2539 if (isExpectingCallback) {
2541 `Internal error: Expected a callback parameter ` +
2542 `with name ${schema.async}`
2546 if (isAsync && schema.returns) {
2548 "Internal error: Async functions must not have return values."
2553 schema.allowAmbiguousOptionalArguments &&
2557 "Internal error: Async functions with ambiguous " +
2558 "arguments must declare the callback as the last parameter"
2568 !!schema.requireUserInput
2572 constructor(schema, parameters, isAsync, hasAsyncCallback, requireUserInput) {
2574 this.parameters = parameters;
2575 this.isAsync = isAsync;
2576 this.hasAsyncCallback = hasAsyncCallback;
2577 this.requireUserInput = requireUserInput;
2580 normalize(value, context) {
2581 return this.normalizeBase("function", value, context);
2584 checkBaseType(baseType) {
2585 return baseType == "function";
2589 // Represents a "property" defined in a schema namespace with a
2590 // particular value. Essentially this is a constant.
2591 class ValueProperty extends Entry {
2592 constructor(schema, name, value) {
2598 getDescriptor(path, context) {
2599 // Prevent injection if not a supported version.
2600 if (!context.matchManifestVersion(this)) {
2605 descriptor: { value: this.value },
2610 // Represents a "property" defined in a schema namespace that is not a
2612 class TypeProperty extends Entry {
2613 unsupported = false;
2615 constructor(schema, path, name, type, writable, permissions) {
2620 this.writable = writable;
2621 this.permissions = permissions;
2624 throwError(context, msg) {
2625 throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2628 getDescriptor(path, context) {
2629 if (this.unsupported || !context.matchManifestVersion(this)) {
2633 let apiImpl = context.getImplementation(path.join("."), this.name);
2635 let getStub = () => {
2636 this.checkDeprecated(context);
2637 return apiImpl.getProperty();
2641 get: Cu.exportFunction(getStub, context.cloneScope),
2644 if (this.writable) {
2645 let setStub = value => {
2646 let normalized = this.type.normalize(value, context);
2647 if (normalized.error) {
2648 this.throwError(context, forceString(normalized.error));
2651 apiImpl.setProperty(normalized.value);
2654 descriptor.set = Cu.exportFunction(setStub, context.cloneScope);
2667 class SubModuleProperty extends Entry {
2668 // A SubModuleProperty represents a tree of objects and properties
2669 // to expose to an extension. Currently we support only a limited
2670 // form of sub-module properties, where "$ref" points to a
2671 // SubModuleType containing a list of functions and "properties" is
2672 // a list of additional simple properties.
2674 // name: Name of the property stuff is being added to.
2675 // namespaceName: Namespace in which the property lives.
2676 // reference: Name of the type defining the functions to add to the property.
2677 // properties: Additional properties to add to the module (unsupported).
2678 constructor(root, schema, path, name, reference, properties, permissions) {
2683 this.namespaceName = path.join(".");
2684 this.reference = reference;
2685 this.properties = properties;
2686 this.permissions = permissions;
2690 let ns = this.root.getNamespace(this.namespaceName);
2691 let type = ns.get(this.reference);
2692 if (!type && this.reference.includes(".")) {
2693 let [namespaceName, ref] = this.reference.split(".");
2694 ns = this.root.getNamespace(namespaceName);
2700 getDescriptor(path, context) {
2701 let obj = Cu.createObjectIn(context.cloneScope);
2703 let ns = this.root.getNamespace(this.namespaceName);
2704 let type = this.targetType;
2706 // Prevent injection if not a supported version.
2707 if (!context.matchManifestVersion(type)) {
2712 if (!type || !(type instanceof SubModuleType)) {
2714 `Internal error: ${this.namespaceName}.${this.reference} ` +
2715 `is not a sub-module`
2719 let subpath = [...path, this.name];
2721 let functions = type.functions;
2722 for (let fun of functions) {
2723 context.injectInto(fun, obj, fun.name, subpath, ns);
2726 let events = type.events;
2727 for (let event of events) {
2728 context.injectInto(event, obj, event.name, subpath, ns);
2731 // TODO: Inject this.properties.
2734 descriptor: { value: obj },
2736 let unwrapped = ChromeUtils.waiveXrays(obj);
2737 for (let fun of functions) {
2739 delete unwrapped[fun.name];
2749 // This class is a base class for FunctionEntrys and Events. It takes
2750 // care of validating parameter lists (i.e., handling of optional
2751 // parameters and parameter type checking).
2752 class CallEntry extends Entry {
2753 hasAsyncCallback = false;
2755 constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) {
2759 this.parameters = parameters;
2760 this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments;
2763 throwError(context, msg) {
2764 throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2767 checkParameters(args, context) {
2770 // First we create a new array, fixedArgs, that is the same as
2771 // |args| but with default values in place of omitted optional parameters.
2772 let check = (parameterIndex, argIndex) => {
2773 if (parameterIndex == this.parameters.length) {
2774 if (argIndex == args.length) {
2780 let parameter = this.parameters[parameterIndex];
2781 if (parameter.optional) {
2783 fixedArgs[parameterIndex] = parameter.default;
2784 if (check(parameterIndex + 1, argIndex)) {
2789 if (argIndex == args.length) {
2793 let arg = args[argIndex];
2794 if (!parameter.type.checkBaseType(getValueBaseType(arg))) {
2795 // For Chrome compatibility, use the default value if null or undefined
2796 // is explicitly passed but is not a valid argument in this position.
2797 if (parameter.optional && (arg === null || arg === undefined)) {
2798 fixedArgs[parameterIndex] = Cu.cloneInto(parameter.default, {});
2803 fixedArgs[parameterIndex] = arg;
2806 return check(parameterIndex + 1, argIndex + 1);
2809 if (this.allowAmbiguousOptionalArguments) {
2810 // When this option is set, it's up to the implementation to
2812 // The last argument for asynchronous methods is either a function or null.
2813 // This is specifically done for runtime.sendMessage.
2814 if (this.hasAsyncCallback && typeof args[args.length - 1] != "function") {
2819 let success = check(0, 0);
2821 this.throwError(context, "Incorrect argument types");
2824 // Now we normalize (and fully type check) all non-omitted arguments.
2825 fixedArgs = fixedArgs.map((arg, parameterIndex) => {
2829 let parameter = this.parameters[parameterIndex];
2830 let r = parameter.type.normalize(arg, context);
2834 `Type error for parameter ${parameter.name} (${forceString(r.error)})`
2844 // Represents a "function" defined in a schema namespace.
2845 FunctionEntry = class FunctionEntry extends CallEntry {
2846 static parseSchema(root, schema, path) {
2847 // When not in DEBUG mode, we just need to know *if* this returns.
2848 /** @type {boolean|object} */
2849 let returns = !!schema.returns;
2850 if (DEBUG && "returns" in schema) {
2852 type: root.parseSchema(schema.returns, path, ["optional", "name"]),
2853 optional: schema.returns.optional || false,
2862 root.parseSchema(schema, path, [
2867 "allowAmbiguousOptionalArguments",
2868 "allowCrossOriginArguments",
2870 schema.unsupported || false,
2871 schema.allowAmbiguousOptionalArguments || false,
2872 schema.allowCrossOriginArguments || false,
2874 schema.permissions || null
2884 allowAmbiguousOptionalArguments,
2885 allowCrossOriginArguments,
2889 super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
2890 this.unsupported = unsupported;
2891 this.returns = returns;
2892 this.permissions = permissions;
2893 this.allowCrossOriginArguments = allowCrossOriginArguments;
2895 this.isAsync = type.isAsync;
2896 this.hasAsyncCallback = type.hasAsyncCallback;
2897 this.requireUserInput = type.requireUserInput;
2900 checkValue({ type, optional, name }, value, context) {
2901 if (optional && value == null) {
2905 type.reference === "ExtensionPanel" ||
2906 type.reference === "ExtensionSidebarPane" ||
2907 type.reference === "Port"
2909 // TODO: We currently treat objects with functions as SubModuleType,
2910 // which is just wrong, and a bigger yak. Skipping for now.
2913 const { error } = type.normalize(value, context);
2917 `Type error for ${name} value (${forceString(error)})`
2922 checkCallback(args, context) {
2923 const callback = this.parameters[this.parameters.length - 1];
2924 for (const [i, param] of callback.type.parameters.entries()) {
2925 this.checkValue(param, args[i], context);
2929 getDescriptor(path, context) {
2930 let apiImpl = context.getImplementation(path.join("."), this.name);
2934 stub = (...args) => {
2935 this.checkDeprecated(context);
2936 let actuals = this.checkParameters(args, context);
2937 let callback = null;
2938 if (this.hasAsyncCallback) {
2939 callback = actuals.pop();
2941 if (callback === null && context.isChromeCompat) {
2942 // We pass an empty stub function as a default callback for
2943 // the `chrome` API, so promise objects are not returned,
2944 // and lastError values are reported immediately.
2945 callback = () => {};
2947 if (DEBUG && this.hasAsyncCallback && callback) {
2948 let original = callback;
2949 callback = (...args) => {
2950 this.checkCallback(args, context);
2954 let result = apiImpl.callAsyncFunction(
2957 this.requireUserInput
2959 if (DEBUG && this.hasAsyncCallback && !callback) {
2960 return result.then(result => {
2961 this.checkCallback([result], context);
2967 } else if (!this.returns) {
2968 stub = (...args) => {
2969 this.checkDeprecated(context);
2970 let actuals = this.checkParameters(args, context);
2971 return apiImpl.callFunctionNoReturn(actuals);
2974 stub = (...args) => {
2975 this.checkDeprecated(context);
2976 let actuals = this.checkParameters(args, context);
2977 let result = apiImpl.callFunction(actuals);
2978 if (DEBUG && this.returns) {
2979 this.checkValue(this.returns, result, context);
2987 value: Cu.exportFunction(stub, context.cloneScope, {
2988 allowCrossOriginArguments: this.allowCrossOriginArguments,
2999 // Represents an "event" defined in a schema namespace.
3001 // TODO Bug 1369722: we should be able to remove the eslint-disable-line that follows
3002 // once Bug 1369722 has been fixed.
3003 // eslint-disable-next-line no-global-assign
3004 Event = class Event extends CallEntry {
3005 static parseSchema(root, event, path) {
3006 let extraParameters = Array.from(event.extraParameters || [], param => ({
3007 type: root.parseSchema(param, path, ["name", "optional", "default"]),
3009 optional: param.optional || false,
3010 default: param.default == undefined ? null : param.default,
3013 let extraProperties = [
3018 // We ignore these properties for now.
3027 root.parseSchema(event, path, extraProperties),
3029 event.unsupported || false,
3030 event.permissions || null
3043 super(schema, path, name, extraParameters);
3045 this.unsupported = unsupported;
3046 this.permissions = permissions;
3049 checkListener(listener, context) {
3050 let r = this.type.normalize(listener, context);
3052 this.throwError(context, "Invalid listener");
3057 getDescriptor(path, context) {
3058 let apiImpl = context.getImplementation(path.join("."), this.name);
3060 let addStub = (listener, ...args) => {
3061 listener = this.checkListener(listener, context);
3062 let actuals = this.checkParameters(args, context);
3063 apiImpl.addListener(listener, actuals);
3066 let removeStub = listener => {
3067 listener = this.checkListener(listener, context);
3068 apiImpl.removeListener(listener);
3071 let hasStub = listener => {
3072 listener = this.checkListener(listener, context);
3073 return apiImpl.hasListener(listener);
3076 let obj = Cu.createObjectIn(context.cloneScope);
3078 Cu.exportFunction(addStub, obj, { defineAs: "addListener" });
3079 Cu.exportFunction(removeStub, obj, { defineAs: "removeListener" });
3080 Cu.exportFunction(hasStub, obj, { defineAs: "hasListener" });
3083 descriptor: { value: obj },
3088 let unwrapped = ChromeUtils.waiveXrays(obj);
3089 delete unwrapped.addListener;
3090 delete unwrapped.removeListener;
3091 delete unwrapped.hasListener;
3097 const TYPES = Object.freeze(
3098 Object.assign(Object.create(null), {
3101 boolean: BooleanType,
3102 function: FunctionType,
3103 integer: IntegerType,
3112 events: "loadEvent",
3113 functions: "loadFunction",
3114 properties: "loadProperty",
3118 class Namespace extends Map {
3119 constructor(root, name, path) {
3124 this._lazySchemas = [];
3125 this.initialized = false;
3128 this.path = name ? [...path, name] : [...path];
3130 this.superNamespace = null;
3132 this.min_manifest_version = MIN_MANIFEST_VERSION;
3133 this.max_manifest_version = MAX_MANIFEST_VERSION;
3135 this.permissions = null;
3136 this.allowedContexts = [];
3137 this.defaultContexts = [];
3141 * Adds a JSON Schema object to the set of schemas that represent this
3144 * @param {object} schema
3145 * A JSON schema object which partially describes this
3149 this._lazySchemas.push(schema);
3155 "min_manifest_version",
3156 "max_manifest_version",
3159 this[prop] = schema[prop];
3163 if (schema.$import) {
3164 this.superNamespace = this.root.getNamespace(schema.$import);
3169 * Initializes the keys of this namespace based on the schema objects
3170 * added via previous `addSchema` calls.
3173 if (this.initialized) {
3177 if (this.superNamespace) {
3178 this._lazySchemas.unshift(...this.superNamespace._lazySchemas);
3181 // Keep in sync with LOADERS above.
3182 this.types = new DefaultMap(() => []);
3183 this.properties = new DefaultMap(() => []);
3184 this.functions = new DefaultMap(() => []);
3185 this.events = new DefaultMap(() => []);
3187 for (let schema of this._lazySchemas) {
3188 for (let type of schema.types || []) {
3189 if (!type.unsupported) {
3190 this.types.get(type.$extend || type.id).push(type);
3194 for (let [name, prop] of Object.entries(schema.properties || {})) {
3195 if (!prop.unsupported) {
3196 this.properties.get(name).push(prop);
3200 for (let fun of schema.functions || []) {
3201 if (!fun.unsupported) {
3202 this.functions.get(fun.name).push(fun);
3206 for (let event of schema.events || []) {
3207 if (!event.unsupported) {
3208 this.events.get(event.name).push(event);
3213 // For each type of top-level property in the schema object, iterate
3214 // over all properties of that type, and create a temporary key for
3215 // each property pointing to its type. Those temporary properties
3216 // are later used to instantiate an Entry object based on the actual
3218 for (let type of Object.keys(LOADERS)) {
3219 for (let key of this[type].keys()) {
3220 this.set(key, type);
3224 this.initialized = true;
3227 for (let key of this.keys()) {
3228 // Force initialization of all lazy keys to catch unexpected errors.
3231 this.#verifyFallbackEntries();
3236 * Verify that multiple definitions via fallback entries (currently only
3237 * supported for functions and events) are defined for mutually exclusive
3238 * manifest versions.
3240 #verifyFallbackEntries() {
3242 let manifestVersion = MIN_MANIFEST_VERSION;
3243 manifestVersion <= MAX_MANIFEST_VERSION;
3246 for (let key of this.keys()) {
3247 let hasMatch = false;
3248 let entry = this.get(key);
3251 manifestVersion >= entry.min_manifest_version &&
3252 manifestVersion <= entry.max_manifest_version;
3253 if (isMatch && hasMatch) {
3255 `Namespace ${this.path.join(".")} has ` +
3256 `multiple definitions for ${key} ` +
3257 `for manifest version ${manifestVersion}`
3260 hasMatch ||= isMatch;
3261 entry = entry.fallbackEntry;
3268 * Returns the definition of the provided Entry or Namespace which is valid for
3269 * the manifest version of the provided context, or none.
3271 * @param {Entry|Namespace} entryOrNs
3272 * @param {Context} context
3274 * @returns {Entry|Namespace?}
3276 #getMatchingDefinitionForContext(entryOrNs, context) {
3278 if (context.matchManifestVersion(entryOrNs)) {
3279 // Common case at first iteration.
3282 entryOrNs = entryOrNs.fallbackEntry;
3283 } while (entryOrNs);
3287 * Initializes the value of a given key, by parsing the schema object
3288 * associated with it and replacing its temporary value with an `Entry`
3291 * @param {string} key
3292 * The name of the property to initialize.
3293 * @param {string} type
3294 * The type of property the key represents. Must have a
3295 * corresponding entry in the `LOADERS` object, pointing to the
3296 * initialization method for that type.
3300 initKey(key, type) {
3301 let loader = LOADERS[type];
3304 for (let schema of this[type].get(key)) {
3305 // Note: The 3rd parameter is currently only supported by loadEvent() and
3306 // loadFunction(). It stores the entry from the last iteration as a
3307 // fallbackEntry (different definitions for different manifest versions).
3308 entry = this[loader](key, schema, entry);
3309 // entry is always an Entry past the first iteration.
3310 this.set(key, entry);
3313 return this.get(key);
3316 loadType(name, type) {
3317 if ("$extend" in type) {
3318 return this.extendType(type);
3320 return this.root.parseSchema(type, this.path, ["id"]);
3324 let targetType = this.get(type.$extend);
3326 // Only allow extending object and choices types for now.
3327 if (targetType instanceof ObjectType) {
3328 type.type = "object";
3332 `Internal error: Attempt to extend a nonexistent type ${type.$extend}`
3334 } else if (!(targetType instanceof ChoiceType)) {
3336 `Internal error: Attempt to extend a non-extensible type ${type.$extend}`
3341 let parsed = this.root.parseSchema(type, this.path, ["$extend"]);
3343 if (DEBUG && parsed.constructor !== targetType.constructor) {
3344 throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
3347 targetType.extend(parsed);
3352 loadProperty(name, prop) {
3353 if ("$ref" in prop) {
3354 if (!prop.unsupported) {
3355 return new SubModuleProperty(
3361 prop.properties || {},
3362 prop.permissions || null
3365 } else if ("value" in prop) {
3366 return new ValueProperty(prop, name, prop.value);
3368 // We ignore the "optional" attribute on properties since we
3369 // don't inject anything here anyway.
3370 let type = this.root.parseSchema(
3373 ["optional", "permissions", "writable"]
3375 return new TypeProperty(
3380 prop.writable || false,
3381 prop.permissions || null
3386 loadFunction(name, fun, fallbackEntry) {
3387 const parsed = FunctionEntry.parseSchema(this.root, fun, this.path);
3388 // If there is already a valid entry, use it as a fallback for the current
3389 // one. Used for multiple definitions for different manifest versions.
3390 if (fallbackEntry) {
3391 parsed.fallbackEntry = fallbackEntry;
3396 loadEvent(name, event, fallbackEntry) {
3397 const parsed = Event.parseSchema(this.root, event, this.path);
3398 // If there is already a valid entry, use it as a fallback for the current
3399 // one. Used for multiple definitions for different manifest versions.
3400 if (fallbackEntry) {
3401 parsed.fallbackEntry = fallbackEntry;
3407 * Injects the properties of this namespace into the given object.
3409 * @param {object} dest
3410 * The object into which to inject the namespace properties.
3411 * @param {InjectionContext} context
3412 * The injection context with which to inject the properties.
3414 injectInto(dest, context) {
3415 for (let name of this.keys()) {
3416 // TODO bug 1896081: we should not call this.get() unconditionally, but
3417 // only for entries that have min_manifest_version or
3418 // max_manifest_version set.
3419 let entry = this.#getMatchingDefinitionForContext(
3423 // If no definition matches the manifest version, do not inject the property.
3424 // This prevents the item from being enumerable in the namespace object.
3425 // We cannot accomplish this inside exportLazyProperty, it specifically
3426 // injects an enumerable object.
3431 exportLazyProperty(dest, name, () => {
3433 // entry ??= this.get(name);
3434 return context.getDescriptor(entry, dest, name, this.path, this);
3439 getDescriptor(path, context) {
3440 let obj = Cu.createObjectIn(context.cloneScope);
3442 let ns = context.schemaRoot.getNamespace(this.path.join("."));
3443 ns.injectInto(obj, context);
3445 // Only inject the namespace object if it isn't empty.
3446 if (Object.keys(obj).length) {
3448 descriptor: { value: obj },
3455 return super.keys();
3458 /** @returns {Generator<[string, Entry]>} */
3460 for (let key of this.keys()) {
3461 yield [key, this.get(key)];
3467 let value = super.get(key);
3469 // The initial values of lazily-initialized schema properties are
3470 // strings, pointing to the type of property, corresponding to one
3471 // of the entries in the `LOADERS` object.
3472 if (typeof value === "string") {
3473 value = this.initKey(key, value);
3480 * Returns a Namespace object for the given namespace name. If a
3481 * namespace object with this name does not already exist, it is
3482 * created. If the name contains any '.' characters, namespaces are
3483 * recursively created, for each dot-separated component.
3485 * @param {string} name
3486 * The name of the sub-namespace to retrieve.
3487 * @param {boolean} [create = true]
3488 * If true, create any intermediate namespaces which don't
3491 * @returns {Namespace}
3493 getNamespace(name, create = true) {
3496 let idx = name.indexOf(".");
3498 subName = name.slice(idx + 1);
3499 name = name.slice(0, idx);
3502 let ns = super.get(name);
3507 ns = new Namespace(this.root, name, this.path);
3512 return ns.getNamespace(subName);
3517 getOwnNamespace(name) {
3518 return this.getNamespace(name);
3523 return super.has(key);
3528 * A namespace which combines the children of an arbitrary number of
3531 class Namespaces extends Namespace {
3532 constructor(root, name, path, namespaces) {
3533 super(root, name, path);
3535 this.namespaces = namespaces;
3538 injectInto(obj, context) {
3539 for (let ns of this.namespaces) {
3540 ns.injectInto(obj, context);
3546 * A root schema which combines the contents of an arbitrary number of base
3549 class SchemaRoots extends Namespaces {
3550 constructor(root, bases) {
3551 bases = bases.map(base => base.rootSchema || base);
3553 super(null, "", [], bases);
3557 this._namespaces = new Map();
3560 _getNamespace(name, create) {
3562 for (let root of this.bases) {
3563 let ns = root.getNamespace(name, create);
3569 if (results.length == 1) {
3573 if (results.length) {
3574 return new Namespaces(this.root, name, name.split("."), results);
3579 getNamespace(name, create) {
3580 let ns = this._namespaces.get(name);
3582 ns = this._getNamespace(name, create);
3584 this._namespaces.set(name, ns);
3590 *getNamespaces(name) {
3591 for (let root of this.bases) {
3592 yield* root.getNamespaces(name);
3598 * A root schema namespace containing schema data which is isolated from data in
3599 * other schema roots. May extend a base namespace, in which case schemas in
3600 * this root may refer to types in a base, but not vice versa.
3602 * @implements {SchemaInject}
3604 export class SchemaRoot extends Namespace {
3606 * @param {SchemaRoot|SchemaRoot[]} base
3607 * A base schema root (or roots) from which to derive, or null.
3608 * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON
3609 * A map of schema URLs and corresponding JSON blobs from which to
3610 * populate this root namespace.
3612 constructor(base, schemaJSON) {
3613 super(null, "", []);
3615 if (Array.isArray(base)) {
3616 this.base = new SchemaRoots(this, base);
3622 this.schemaJSON = schemaJSON;
3625 *getNamespaces(path) {
3626 let name = path.join(".");
3628 let ns = this.getNamespace(name, false);
3634 yield* this.base.getNamespaces(name);
3639 * Returns the sub-namespace with the given name. If the given namespace
3640 * doesn't already exist, attempts to find it in the base SchemaRoot before
3641 * creating a new empty namespace.
3643 * @param {string} name
3644 * The namespace to retrieve.
3645 * @param {boolean} [create = true]
3646 * If true, an empty namespace should be created if one does not
3648 * @returns {Namespace|null}
3650 getNamespace(name, create = true) {
3651 let ns = super.getNamespace(name, false);
3656 ns = this.base && this.base.getNamespace(name, false);
3660 return create && super.getNamespace(name, create);
3664 * Like getNamespace, but does not take the base SchemaRoot into account.
3666 * @param {string} name
3667 * The namespace to retrieve.
3668 * @returns {Namespace}
3670 getOwnNamespace(name) {
3671 return super.getNamespace(name);
3674 parseSchema(schema, path, extraProperties = []) {
3675 let allowedProperties = DEBUG && new Set(extraProperties);
3677 if ("choices" in schema) {
3678 return ChoiceType.parseSchema(this, schema, path, allowedProperties);
3679 } else if ("$ref" in schema) {
3680 return RefType.parseSchema(this, schema, path, allowedProperties);
3683 let type = TYPES[schema.type];
3686 allowedProperties.add("type");
3688 if (!("type" in schema)) {
3689 throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
3693 throw new Error(`Unexpected type ${schema.type}`);
3697 return type.parseSchema(this, schema, path, allowedProperties);
3701 for (let [key, schema] of this.schemaJSON.entries()) {
3703 if (StructuredCloneHolder.isInstance(schema)) {
3704 schema = schema.deserialize(globalThis, isParentProcess);
3706 // If we're in the parent process, we need to keep the
3707 // StructuredCloneHolder blob around in order to send to future child
3708 // processes. If we're in a child, we have no further use for it, so
3709 // just store the deserialized schema data in its place.
3710 if (!isParentProcess) {
3711 this.schemaJSON.set(key, schema);
3715 this.loadSchema(schema);
3723 for (let namespace of json) {
3724 this.getOwnNamespace(namespace.namespace).addSchema(namespace);
3729 * Checks whether a given object has the necessary permissions to
3730 * expose the given namespace.
3732 * @param {string} namespace
3733 * The top-level namespace to check permissions for.
3734 * @param {object} wrapperFuncs
3735 * Wrapper functions for the given context.
3736 * @param {Function} wrapperFuncs.hasPermission
3737 * A function which, when given a string argument, returns true
3738 * if the context has the given permission.
3739 * @returns {boolean}
3740 * True if the context has permission for the given namespace.
3742 checkPermissions(namespace, wrapperFuncs) {
3743 let ns = this.getNamespace(namespace);
3744 if (ns && ns.permissions) {
3745 return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
3751 * Inject registered extension APIs into `dest`.
3753 * @param {object} dest The root namespace for the APIs.
3754 * This object is usually exposed to extensions as "chrome" or "browser".
3755 * @param {InjectionContext} wrapperFuncs An implementation of the InjectionContext
3756 * interface, which runs the actual functionality of the generated API.
3758 inject(dest, wrapperFuncs) {
3759 let context = new InjectionContext(wrapperFuncs, this);
3761 this.injectInto(dest, context);
3764 injectInto(dest, context) {
3765 // For schema graphs where multiple schema roots have the same base, don't
3766 // inject it more than once.
3768 if (!context.injectedRoots.has(this)) {
3769 context.injectedRoots.add(this);
3771 this.base.injectInto(dest, context);
3773 super.injectInto(dest, context);
3778 * Normalize `obj` according to the loaded schema for `typeName`.
3780 * @param {object} obj The object to normalize against the schema.
3781 * @param {string} typeName The name in the format namespace.propertyname
3782 * @param {object} context An implementation of Context. Any validation errors
3783 * are reported to the given context.
3784 * @returns {object} The normalized object.
3786 normalize(obj, typeName, context) {
3787 let [namespaceName, prop] = typeName.split(".");
3788 let ns = this.getNamespace(namespaceName);
3789 let type = ns.get(prop);
3791 let result = type.normalize(obj, new Context(context));
3793 return { error: forceString(result.error) };
3800 * @typedef {{ inject: typeof Schemas.inject }} SchemaInject
3801 * Interface SchemaInject as used by SchemaApiManager,
3802 * with the one method shared across Schemas and SchemaRoot.
3804 export var Schemas = {
3807 REVOKE: Symbol("@@revoke"),
3809 // Maps a schema URL to the JSON contained in that schema file. This
3810 // is useful for sending the JSON across processes.
3811 schemaJSON: new Map(),
3813 // A map of schema JSON which should be available in all content processes.
3814 contentSchemaJSON: new Map(),
3816 // A map of schema JSON which should only be available to extension processes.
3817 privilegedSchemaJSON: new Map(),
3821 // A weakmap for the validation Context class instances given an extension
3822 // context (keyed by the extensin context instance).
3823 // This is used instead of the InjectionContext for webIDL API validation
3824 // and normalization (see Schemas.checkParameters).
3825 paramsValidationContexts: new DefaultWeakMap(
3826 extContext => new Context(extContext)
3829 /** @returns {SchemaRoot} */
3831 if (!this.initialized) {
3834 if (!this._rootSchema) {
3835 this._rootSchema = new SchemaRoot(null, this.schemaJSON);
3836 this._rootSchema.parseSchemas();
3838 return this._rootSchema;
3841 getNamespace(name) {
3842 return this.rootSchema.getNamespace(name);
3846 if (this.initialized) {
3849 this.initialized = true;
3851 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
3852 let addSchemas = schemas => {
3853 for (let [key, value] of schemas.entries()) {
3854 this.schemaJSON.set(key, value);
3858 if (WebExtensionPolicy.isExtensionProcess || DEBUG) {
3859 addSchemas(Services.cpmm.sharedData.get(KEY_PRIVILEGED_SCHEMAS));
3862 let schemas = Services.cpmm.sharedData.get(KEY_CONTENT_SCHEMAS);
3864 addSchemas(schemas);
3869 _loadCachedSchemasPromise: null,
3870 loadCachedSchemas() {
3871 if (!this._loadCachedSchemasPromise) {
3872 this._loadCachedSchemasPromise = lazy.StartupCache.schemas
3879 return this._loadCachedSchemasPromise;
3882 addSchema(url, schema, content = false) {
3883 this.schemaJSON.set(url, schema);
3886 this.contentSchemaJSON.set(url, schema);
3888 this.privilegedSchemaJSON.set(url, schema);
3891 if (this._rootSchema) {
3892 throw new Error("Schema loaded after root schema populated");
3896 updateSharedSchemas() {
3897 let { sharedData } = Services.ppmm;
3899 sharedData.set(KEY_CONTENT_SCHEMAS, this.contentSchemaJSON);
3900 sharedData.set(KEY_PRIVILEGED_SCHEMAS, this.privilegedSchemaJSON);
3904 return readJSONAndBlobbify(url);
3907 processSchema(json) {
3908 return blobbify(json);
3911 async load(url, content = false) {
3912 if (!isParentProcess) {
3916 const startTime = Cu.now();
3917 let schemaCache = await this.loadCachedSchemas();
3918 const fromCache = schemaCache.has(url);
3921 schemaCache.get(url) ||
3922 (await lazy.StartupCache.schemas.get(url, readJSONAndBlobbify));
3924 if (!this.schemaJSON.has(url)) {
3925 this.addSchema(url, blob, content);
3928 ChromeUtils.addProfilerMarker(
3931 `load ${url}, from cache: ${fromCache}`
3936 * Checks whether a given object has the necessary permissions to
3937 * expose the given namespace.
3939 * @param {string} namespace
3940 * The top-level namespace to check permissions for.
3941 * @param {object} wrapperFuncs
3942 * Wrapper functions for the given context.
3943 * @param {Function} wrapperFuncs.hasPermission
3944 * A function which, when given a string argument, returns true
3945 * if the context has the given permission.
3946 * @returns {boolean}
3947 * True if the context has permission for the given namespace.
3949 checkPermissions(namespace, wrapperFuncs) {
3950 return this.rootSchema.checkPermissions(namespace, wrapperFuncs);
3954 * Returns a sorted array of permission names for the given permission types.
3956 * @param {Array} types An array of permission types, defaults to all permissions.
3957 * @returns {Array} sorted array of permission names
3962 "OptionalPermission",
3963 "PermissionNoPrompt",
3964 "OptionalPermissionNoPrompt",
3965 "PermissionPrivileged",
3968 const ns = this.getNamespace("manifest");
3970 for (let typeName of types) {
3971 for (let choice of ns
3973 .choices.filter(choice => choice.enumeration)) {
3974 names = names.concat(choice.enumeration);
3977 return names.sort();
3983 * Inject registered extension APIs into `dest`.
3985 * @param {object} dest The root namespace for the APIs.
3986 * This object is usually exposed to extensions as "chrome" or "browser".
3987 * @param {InjectionContext} wrapperFuncs An implementation of the InjectionContext
3988 * interface, which runs the actual functionality of the generated API.
3990 inject(dest, wrapperFuncs) {
3991 this.rootSchema.inject(dest, wrapperFuncs);
3995 * Normalize `obj` according to the loaded schema for `typeName`.
3997 * @param {object} obj The object to normalize against the schema.
3998 * @param {string} typeName The name in the format namespace.propertyname
3999 * @param {object} context An implementation of Context. Any validation errors
4000 * are reported to the given context.
4001 * @returns {object} The normalized object.
4003 normalize(obj, typeName, context) {
4004 return this.rootSchema.normalize(obj, typeName, context);
4008 * Validate and normalize the arguments for an API request originated
4009 * from the webIDL API bindings.
4011 * This provides for calls originating through WebIDL the parameters
4012 * validation and normalization guarantees that the ext-APINAMESPACE.js
4013 * scripts expects (what InjectionContext does for the regular bindings).
4015 * @param {object} extContext
4016 * @param {mozIExtensionAPIRequest } apiRequest
4018 * @returns {Array<any>} Normalized arguments array.
4020 checkWebIDLRequestParameters(extContext, apiRequest) {
4021 const getSchemaForProperty = (schemaObj, propName, schemaPath) => {
4022 if (schemaObj instanceof Namespace) {
4023 return schemaObj?.get(propName);
4024 } else if (schemaObj instanceof SubModuleProperty) {
4025 for (const fun of schemaObj.targetType.functions) {
4026 if (fun.name === propName) {
4031 for (const fun of schemaObj.targetType.events) {
4032 if (fun.name === propName) {
4036 } else if (schemaObj instanceof Event) {
4040 const schemaPathType = schemaObj?.constructor.name;
4042 `API Schema for "${propName}" not found in ${schemaPath} (${schemaPath} type is ${schemaPathType})`
4045 const { requestType, apiNamespace, apiName } = apiRequest;
4047 let [ns, ...rest] = (
4048 ["addListener", "removeListener"].includes(requestType)
4049 ? `${apiNamespace}.${apiName}.${requestType}`
4050 : `${apiNamespace}.${apiName}`
4052 /** @type {Namespace|CallEntry} */
4053 let apiSchema = this.getNamespace(ns);
4055 // Keep track of the current schema path, populated while navigating the nested API schema
4056 // data and then used to include the full path to the API schema that is hitting unexpected
4057 // errors due to schema data not found or an unexpected schema type.
4058 let schemaPath = [ns];
4060 while (rest.length) {
4061 // Nested property as namespace (e.g. used for proxy.settings requests).
4063 throw new Error(`API Schema not found for ${schemaPath.join(".")}`);
4066 let [propName, ...newRest] = rest;
4069 apiSchema = getSchemaForProperty(
4072 schemaPath.join(".")
4074 schemaPath.push(propName);
4078 throw new Error(`API Schema not found for ${schemaPath.join(".")}`);
4081 if (!(apiSchema instanceof CallEntry)) {
4083 `Unexpected API Schema type for ${schemaPath.join(
4085 )} (${schemaPath.join(".")} type is ${apiSchema.constructor.name})`
4089 return apiSchema.checkParameters(
4091 this.paramsValidationContexts.get(extContext)