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;
16 ChromeUtils.defineESModuleGetters(lazy, {
17 ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
18 NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
19 ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
22 XPCOMUtils.defineLazyServiceGetter(
24 "contentPolicyService",
25 "@mozilla.org/addons/content-policy;1",
26 "nsIAddonContentPolicy"
29 ChromeUtils.defineLazyGetter(
32 () => lazy.ExtensionParent.StartupCache
35 XPCOMUtils.defineLazyPreferenceGetter(
37 "treatWarningsAsErrors",
38 "extensions.webextensions.warnings-as-errors",
42 const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content";
43 const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged";
45 const MIN_MANIFEST_VERSION = 2;
46 const MAX_MANIFEST_VERSION = 3;
48 const { DEBUG } = AppConstants;
50 const isParentProcess =
51 Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
53 function readJSON(url) {
54 return new Promise((resolve, reject) => {
55 lazy.NetUtil.asyncFetch(
56 { uri: url, loadUsingSystemPrincipal: true },
57 (inputStream, status) => {
58 if (!Components.isSuccessCode(status)) {
59 // Convert status code to a string
60 let e = Components.Exception("", status);
61 reject(new Error(`Error while loading '${url}' (${e.name})`));
65 let text = lazy.NetUtil.readInputStreamToString(
67 inputStream.available()
70 // Chrome JSON files include a license comment that we need to
71 // strip off for this to be valid JSON. As a hack, we just
72 // look for the first '[' character, which signals the start
73 // of the JSON content.
74 let index = text.indexOf("[");
75 text = text.slice(index);
77 resolve(JSON.parse(text));
86 function stripDescriptions(json, stripThis = true) {
87 if (Array.isArray(json)) {
88 for (let i = 0; i < json.length; i++) {
89 if (typeof json[i] === "object" && json[i] !== null) {
90 json[i] = stripDescriptions(json[i]);
98 // Objects are handled much more efficiently, both in terms of memory and
99 // CPU, if they have the same shape as other objects that serve the same
100 // purpose. So, normalize the order of properties to increase the chances
101 // that the majority of schema objects wind up in large shape groups.
102 for (let key of Object.keys(json).sort()) {
103 if (stripThis && key === "description" && typeof json[key] === "string") {
107 if (typeof json[key] === "object" && json[key] !== null) {
108 result[key] = stripDescriptions(json[key], key !== "properties");
110 result[key] = json[key];
117 function blobbify(json) {
118 // We don't actually use descriptions at runtime, and they make up about a
119 // third of the size of our structured clone data, so strip them before
121 json = stripDescriptions(json);
123 return new StructuredCloneHolder("Schemas/blobbify", null, json);
126 async function readJSONAndBlobbify(url) {
127 let json = await readJSON(url);
129 return blobbify(json);
133 * Defines a lazy getter for the given property on the given object. Any
134 * security wrappers are waived on the object before the property is
135 * defined, and the getter and setter methods are wrapped for the target
138 * The given getter function is guaranteed to be called only once, even
139 * if the target scope retrieves the wrapped getter from the property
140 * descriptor and calls it directly.
142 * @param {object} object
143 * The object on which to define the getter.
144 * @param {string | symbol} prop
145 * The property name for which to define the getter.
146 * @param {Function} getter
147 * The function to call in order to generate the final property
150 function exportLazyGetter(object, prop, getter) {
151 object = ChromeUtils.waiveXrays(object);
153 let redefine = value => {
154 if (value === undefined) {
157 Object.defineProperty(object, prop, {
170 Object.defineProperty(object, prop, {
174 get: Cu.exportFunction(function () {
175 return redefine(getter.call(this));
178 set: Cu.exportFunction(value => {
185 * Defines a lazily-instantiated property descriptor on the given
186 * object. Any security wrappers are waived on the object before the
187 * property is defined.
189 * The given getter function is guaranteed to be called only once, even
190 * if the target scope retrieves the wrapped getter from the property
191 * descriptor and calls it directly.
193 * @param {object} object
194 * The object on which to define the getter.
195 * @param {string | symbol} prop
196 * The property name for which to define the getter.
197 * @param {Function} getter
198 * The function to call in order to generate the final property
199 * descriptor object. This will be called, and the property
200 * descriptor installed on the object, the first time the
201 * property is written or read. The function may return
202 * undefined, which will cause the property to be deleted.
204 function exportLazyProperty(object, prop, getter) {
205 object = ChromeUtils.waiveXrays(object);
207 let redefine = obj => {
208 let desc = getter.call(obj);
218 if (!desc.set && !desc.get) {
219 defaults.writable = true;
222 Object.defineProperty(object, prop, Object.assign(defaults, desc));
226 Object.defineProperty(object, prop, {
230 get: Cu.exportFunction(function () {
235 set: Cu.exportFunction(function (value) {
237 object[prop] = value;
242 const POSTPROCESSORS = {
243 convertImageDataToURL(imageData, context) {
244 let document = context.cloneScope.document;
245 let canvas = document.createElementNS(
246 "http://www.w3.org/1999/xhtml",
249 canvas.width = imageData.width;
250 canvas.height = imageData.height;
251 canvas.getContext("2d").putImageData(imageData, 0, 0);
253 return canvas.toDataURL("image/png");
255 webRequestBlockingPermissionRequired(string, context) {
256 if (string === "blocking" && !context.hasPermission("webRequestBlocking")) {
257 throw new context.cloneScope.Error(
258 "Using webRequest.addListener with the " +
259 "blocking option requires the 'webRequestBlocking' permission."
265 requireBackgroundServiceWorkerEnabled(value, context) {
266 if (WebExtensionPolicy.backgroundServiceWorkerEnabled) {
270 // Add an error to the manifest validations and throw the
272 const msg = "background.service_worker is currently disabled";
273 context.logError(context.makeError(msg));
274 throw new Error(msg);
277 manifestVersionCheck(value, context) {
281 Services.prefs.getBoolPref("extensions.manifestV3.enabled", false))
285 const msg = `Unsupported manifest version: ${value}`;
286 context.logError(context.makeError(msg));
287 throw new Error(msg);
290 webAccessibleMatching(value, context) {
291 // Ensure each object has at least one of matches or extension_ids array.
292 for (let obj of value) {
293 if (!obj.matches && !obj.extension_ids) {
294 const msg = `web_accessible_resources requires one of "matches" or "extension_ids"`;
295 context.logError(context.makeError(msg));
296 throw new Error(msg);
303 // Parses a regular expression, with support for the Python extended
304 // syntax that allows setting flags by including the string (?im)
305 function parsePattern(pattern) {
307 let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
309 [, flags, pattern] = match;
311 return new RegExp(pattern, flags);
314 function getValueBaseType(value) {
315 let type = typeof value;
318 if (value === null) {
321 if (Array.isArray(value)) {
327 if (value % 1 === 0) {
334 // Methods of Context that are used by Schemas.normalize. These methods can be
335 // overridden at the construction of Context.
336 const CONTEXT_FOR_VALIDATION = ["checkLoadURL", "hasPermission", "logError"];
338 // Methods of Context that are used by Schemas.inject.
339 // Callers of Schemas.inject should implement all of these methods.
340 const CONTEXT_FOR_INJECTION = [
341 ...CONTEXT_FOR_VALIDATION,
343 "isPermissionRevokable",
347 // If the message is a function, call it and return the result.
348 // Otherwise, assume it's a string.
349 function forceString(msg) {
350 if (typeof msg === "function") {
357 * A context for schema validation and error reporting. This class is only used
358 * internally within Schemas.
362 * @param {object} params Provides the implementation of this class.
363 * @param {Array<string>} overridableMethods
365 constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) {
366 this.params = params;
368 if (typeof params.manifestVersion !== "number") {
370 `Unexpected params.manifestVersion value: ${params.manifestVersion}`
375 this.preprocessors = {
376 localize(value, context) {
379 ...params.preprocessors,
382 this.postprocessors = POSTPROCESSORS;
383 this.isChromeCompat = params.isChromeCompat ?? false;
384 this.manifestVersion = params.manifestVersion;
386 this.currentChoices = new Set();
387 this.choicePathIndex = 0;
389 for (let method of overridableMethods) {
390 if (method in params) {
391 this[method] = params[method].bind(params);
397 let path = this.path.slice(this.choicePathIndex);
398 return path.join(".");
402 return this.params.cloneScope || undefined;
406 return this.params.url;
411 this.params.principal ||
412 Services.scriptSecurityManager.createNullPrincipal({})
417 * Checks whether `url` may be loaded by the extension in this context.
419 * @param {string} url The URL that the extension wished to load.
420 * @returns {boolean} Whether the context may load `url`.
423 let ssm = Services.scriptSecurityManager;
425 ssm.checkLoadURIWithPrincipal(
427 Services.io.newURI(url),
428 ssm.DISALLOW_INHERIT_PRINCIPAL
437 * Checks whether this context has the given permission.
439 * @param {string} permission
440 * The name of the permission to check.
442 * @returns {boolean} True if the context has the given permission.
444 hasPermission(permission) {
449 * Checks whether the given permission can be dynamically revoked or
452 * @param {string} permission
453 * The name of the permission to check.
455 * @returns {boolean} True if the given permission is revokable.
457 isPermissionRevokable(permission) {
462 * Returns an error result object with the given message, for return
463 * by Type normalization functions.
465 * If the context has a `currentTarget` value, this is prepended to
466 * the message to indicate the location of the error.
468 * @param {string | Function} errorMessage
469 * The error message which will be displayed when this is the
470 * only possible matching schema. If a function is passed, it
471 * will be evaluated when the error string is first needed, and
472 * must return a string.
473 * @param {string | Function} choicesMessage
474 * The message describing the valid what constitutes a valid
475 * value for this schema, which will be displayed when multiple
476 * schema choices are available and none match.
478 * A caller may pass `null` to prevent a choice from being
479 * added, but this should *only* be done from code processing a
481 * @param {boolean} [warning = false]
482 * If true, make message prefixed `Warning`. If false, make message
486 error(errorMessage, choicesMessage = undefined, warning = false) {
487 if (choicesMessage !== null) {
488 let { choicePath } = this;
490 choicesMessage = `.${choicePath} must ${choicesMessage}`;
493 this.currentChoices.add(choicesMessage);
496 if (this.currentTarget) {
497 let { currentTarget } = this;
501 warning ? "Warning" : "Error"
502 } processing ${currentTarget}: ${forceString(errorMessage)}`,
505 return { error: errorMessage };
509 * Creates an `Error` object belonging to the current unprivileged
510 * scope. If there is no unprivileged scope associated with this
511 * context, the message is returned as a string.
513 * If the context has a `currentTarget` value, this is prepended to
514 * the message, in the same way as for the `error` method.
516 * @param {string} message
517 * @param {object} [options]
518 * @param {boolean} [options.warning = false]
521 makeError(message, { warning = false } = {}) {
522 let error = forceString(this.error(message, null, warning).error);
523 if (this.cloneScope) {
524 return new this.cloneScope.Error(error);
530 * Logs the given error to the console. May be overridden to enable
533 * @param {Error|string} error
536 if (this.cloneScope) {
538 // Error objects logged using Cu.reportError are not associated
539 // to the related innerWindowID. This results in a leaked docshell
540 // since consoleService cannot release the error object when the
541 // extension global is destroyed.
542 typeof error == "string" ? error : String(error),
543 // Report the error with the appropriate stack trace when the
544 // is related to an actual extension global (instead of being
545 // related to a manifest validation).
546 this.principal && ChromeUtils.getCallerLocation(this.principal)
549 Cu.reportError(error);
554 * Logs a warning. An error might be thrown when we treat warnings as errors.
556 * @param {string} warningMessage
558 logWarning(warningMessage) {
559 let error = this.makeError(warningMessage, { warning: true });
560 this.logError(error);
562 if (lazy.treatWarningsAsErrors) {
563 // This pref is false by default, and true by default in tests to
564 // discourage the use of deprecated APIs in our unit tests.
565 // If a warning is an expected part of a test, temporarily set the pref
566 // to false, e.g. with the ExtensionTestUtils.failOnSchemaWarnings helper.
567 Services.console.logStringMessage(
568 "Treating warning as error because the preference " +
569 "extensions.webextensions.warnings-as-errors is set to true"
571 if (typeof error === "string") {
572 error = new Error(error);
579 * Returns the name of the value currently being normalized. For a
580 * nested object, this is usually approximately equivalent to the
581 * JavaScript property accessor for that property. Given:
583 * { foo: { bar: [{ baz: x }] } }
585 * When processing the value for `x`, the currentTarget is
588 get currentTarget() {
589 return this.path.join(".");
593 * Executes the given callback, and returns an array of choice strings
594 * passed to {@see #error} during its execution.
596 * @param {Function} callback
598 * An object with a `result` property containing the return
599 * value of the callback, and a `choice` property containing
600 * an array of choices.
602 withChoices(callback) {
603 let { currentChoices, choicePathIndex } = this;
605 let choices = new Set();
606 this.currentChoices = choices;
607 this.choicePathIndex = this.path.length;
610 let result = callback();
612 return { result, choices };
614 this.currentChoices = currentChoices;
615 this.choicePathIndex = choicePathIndex;
617 if (choices.size == 1) {
618 for (let choice of choices) {
619 currentChoices.add(choice);
621 } else if (choices.size) {
622 this.error(null, () => {
623 let array = Array.from(choices, forceString);
624 let n = array.length - 1;
625 array[n] = `or ${array[n]}`;
627 return `must either [${array.join(", ")}]`;
634 * Appends the given component to the `currentTarget` path to indicate
635 * that it is being processed, calls the given callback function, and
636 * then restores the original path.
638 * This is used to identify the path of the property being processed
639 * when reporting type errors.
641 * @param {string} component
642 * @param {Function} callback
645 withPath(component, callback) {
646 this.path.push(component);
654 matchManifestVersion(entry) {
655 let { manifestVersion } = this;
657 manifestVersion >= entry.min_manifest_version &&
658 manifestVersion <= entry.max_manifest_version
664 * Represents a schema entry to be injected into an object. Handles the
665 * injection, revocation, and permissions of said entry.
667 * @param {InjectionContext} context
668 * The injection context for the entry.
669 * @param {Entry} entry
670 * The entry to inject.
671 * @param {object} parentObject
672 * The object into which to inject this entry.
673 * @param {string} name
674 * The property name at which to inject this entry.
675 * @param {Array<string>} path
676 * The full path from the root entry to this entry.
677 * @param {Entry} parentEntry
678 * The parent entry for the injected entry.
680 class InjectionEntry {
681 constructor(context, entry, parentObj, name, path, parentEntry) {
682 this.context = context;
684 this.parentObj = parentObj;
687 this.parentEntry = parentEntry;
689 this.injected = null;
690 this.lazyInjected = null;
694 * @property {Array<string>} allowedContexts
695 * The list of allowed contexts into which the entry may be
698 get allowedContexts() {
699 let { allowedContexts } = this.entry;
700 if (allowedContexts.length) {
701 return allowedContexts;
703 return this.parentEntry.defaultContexts;
707 * @property {boolean} isRevokable
708 * Returns true if this entry may be dynamically injected or
709 * revoked based on its permissions.
713 this.entry.permissions &&
714 this.entry.permissions.some(perm =>
715 this.context.isPermissionRevokable(perm)
721 * @property {boolean} hasPermission
722 * Returns true if the injection context currently has the
723 * appropriate permissions to access this entry.
725 get hasPermission() {
727 !this.entry.permissions ||
728 this.entry.permissions.some(perm => this.context.hasPermission(perm))
733 * @property {boolean} shouldInject
734 * Returns true if this entry should be injected in the given
735 * context, without respect to permissions.
739 this.context.matchManifestVersion(this.entry) &&
740 this.context.shouldInject(
749 * Revokes this entry, removing its property from its parent object,
750 * and invalidating its wrappers.
753 if (this.lazyInjected) {
754 this.lazyInjected = false;
755 } else if (this.injected) {
756 if (this.injected.revoke) {
757 this.injected.revoke();
761 let unwrapped = ChromeUtils.waiveXrays(this.parentObj);
762 delete unwrapped[this.name];
767 let { value } = this.injected.descriptor;
769 this.context.revokeChildren(value);
772 this.injected = null;
777 * Returns a property descriptor object for this entry, if it should
778 * be injected, or undefined if it should not.
781 * A property descriptor object, or undefined if the property
785 this.lazyInjected = false;
788 let path = [...this.path, this.name];
790 `Attempting to re-inject already injected entry: ${path.join(".")}`
794 if (!this.shouldInject) {
798 if (this.isRevokable) {
799 this.context.pendingEntries.add(this);
802 if (!this.hasPermission) {
806 this.injected = this.entry.getDescriptor(this.path, this.context);
807 if (!this.injected) {
811 return this.injected.descriptor;
815 * Injects a lazy property descriptor into the parent object which
816 * checks permissions and eligibility for injection the first time it
820 if (this.lazyInjected || this.injected) {
821 let path = [...this.path, this.name];
823 `Attempting to re-lazy-inject already injected entry: ${path.join(".")}`
827 this.lazyInjected = true;
828 exportLazyProperty(this.parentObj, this.name, () => {
829 if (this.lazyInjected) {
830 return this.getDescriptor();
836 * Injects or revokes this entry if its current state does not match
837 * the context's current permissions.
839 permissionsChanged() {
848 if (!this.injected && !this.lazyInjected) {
854 if (this.injected && !this.hasPermission) {
861 * Holds methods that run the actual implementation of the extension APIs. These
862 * methods are only called if the extension API invocation matches the signature
863 * as defined in the schema. Otherwise an error is reported to the context.
865 class InjectionContext extends Context {
866 constructor(params, schemaRoot) {
867 super(params, CONTEXT_FOR_INJECTION);
869 this.schemaRoot = schemaRoot;
871 this.pendingEntries = new Set();
872 this.children = new DefaultWeakMap(() => new Map());
874 this.injectedRoots = new Set();
876 if (params.setPermissionsChangedCallback) {
877 params.setPermissionsChangedCallback(this.permissionsChanged.bind(this));
882 * Check whether the API should be injected.
885 * @param {string} namespace The namespace of the API. This may contain dots,
886 * e.g. in the case of "devtools.inspectedWindow".
887 * @param {string?} name The name of the property in the namespace.
888 * `null` if we are checking whether the namespace should be injected.
889 * @param {Array<string>} allowedContexts A list of additional contexts in
890 * which this API should be available. May include any of:
891 * "main" - The main chrome browser process.
892 * "addon" - An addon process.
893 * "content" - A content process.
894 * @returns {boolean} Whether the API should be injected.
896 shouldInject(namespace, name, allowedContexts) {
897 throw new Error("Not implemented");
901 * Generate the implementation for `namespace`.`name`.
904 * @param {string} namespace The full path to the namespace of the API, minus
905 * the name of the method or property. E.g. "storage.local".
906 * @param {string} name The name of the method, property or event.
907 * @returns {SchemaAPIInterface} The implementation of the API.
909 getImplementation(namespace, name) {
910 throw new Error("Not implemented");
914 * Updates all injection entries which may need to be updated after a
915 * permission change, revoking or re-injecting them as necessary.
917 permissionsChanged() {
918 for (let entry of this.pendingEntries) {
920 entry.permissionsChanged();
928 * Recursively revokes all child injection entries of the given
931 * @param {object} object
932 * The object for which to invoke children.
934 revokeChildren(object) {
935 if (!this.children.has(object)) {
939 let children = this.children.get(object);
940 for (let [name, entry] of children.entries()) {
946 children.delete(name);
948 // When we revoke children for an object, we consider that object
949 // dead. If the entry is ever reified again, a new object is
950 // created, with new child entries.
951 this.pendingEntries.delete(entry);
953 this.children.delete(object);
956 _getInjectionEntry(entry, dest, name, path, parentEntry) {
957 let injection = new InjectionEntry(
966 this.children.get(dest).set(name, injection);
972 * Returns the property descriptor for the given entry.
974 * @param {Entry} entry
975 * The entry instance to return a descriptor for.
976 * @param {object} dest
977 * The object into which this entry is being injected.
978 * @param {string} name
979 * The property name on the destination object where the entry
981 * @param {Array<string>} path
982 * The full path from the root injection object to this entry.
983 * @param {Partial<Entry>} parentEntry
984 * The parent entry for this entry.
987 * A property descriptor object, or null if the entry should
990 getDescriptor(entry, dest, name, path, parentEntry) {
991 let injection = this._getInjectionEntry(
999 return injection.getDescriptor();
1003 * Lazily injects the given entry into the given object.
1005 * @param {Entry} entry
1006 * The entry instance to lazily inject.
1007 * @param {object} dest
1008 * The object into which to inject this entry.
1009 * @param {string} name
1010 * The property name at which to inject the entry.
1011 * @param {Array<string>} path
1012 * The full path from the root injection object to this entry.
1013 * @param {Entry} parentEntry
1014 * The parent entry for this entry.
1016 injectInto(entry, dest, name, path, parentEntry) {
1017 let injection = this._getInjectionEntry(
1025 injection.lazyInject();
1030 * The methods in this singleton represent the "format" specifier for
1031 * JSON Schema string types.
1033 * Each method either returns a normalized version of the original
1034 * value, or throws an error if the value is not valid for the given
1038 hostname(string, context) {
1039 // TODO bug 1797376: Despite the name, this format is NOT a "hostname",
1040 // but hostname + port and may fail with IPv6. Use canonicalDomain instead.
1044 valid = new URL(`http://${string}`).host === string;
1050 throw new Error(`Invalid hostname ${string}`);
1056 canonicalDomain(string, context) {
1060 valid = new URL(`http://${string}`).hostname === string;
1066 // Require the input to be a canonical domain.
1067 // Rejects obvious non-domains such as URLs,
1068 // but also catches non-IDN (punycode) domains.
1069 throw new Error(`Invalid domain ${string}`);
1075 url(string, context) {
1076 let url = new URL(string).href;
1078 if (!context.checkLoadURL(url)) {
1079 throw new Error(`Access denied for URL ${url}`);
1084 origin(string, context) {
1087 url = new URL(string);
1089 throw new Error(`Invalid origin: ${string}`);
1091 if (!/^https?:/.test(url.protocol)) {
1092 throw new Error(`Invalid origin must be http or https for URL ${string}`);
1094 // url.origin is punycode so a direct check against string wont work.
1095 // url.href appends a slash even if not in the original string, we we
1096 // additionally check that string does not end in slash.
1097 if (string.endsWith("/") || url.href != new URL(url.origin).href) {
1099 `Invalid origin for URL ${string}, replace with origin ${url.origin}`
1102 if (!context.checkLoadURL(url.origin)) {
1103 throw new Error(`Access denied for URL ${url}`);
1108 relativeUrl(string, context) {
1110 // If there's no context URL, return relative URLs unresolved, and
1111 // skip security checks for them.
1119 let url = new URL(string, context.url).href;
1121 if (!context.checkLoadURL(url)) {
1122 throw new Error(`Access denied for URL ${url}`);
1127 strictRelativeUrl(string, context) {
1128 void FORMATS.unresolvedRelativeUrl(string, context);
1129 return FORMATS.relativeUrl(string, context);
1132 unresolvedRelativeUrl(string, context) {
1133 if (!string.startsWith("//")) {
1141 throw new SyntaxError(
1142 `String ${JSON.stringify(string)} must be a relative URL`
1146 homepageUrl(string, context) {
1147 // Pipes are used for separating homepages, but we only allow extensions to
1148 // set a single homepage. Encoding any pipes makes it one URL.
1149 return FORMATS.relativeUrl(
1150 string.replace(new RegExp("\\|", "g"), "%7C"),
1155 imageDataOrStrictRelativeUrl(string, context) {
1156 // Do not accept a string which resolves as an absolute URL, or any
1157 // protocol-relative URL, except PNG or JPG data URLs
1159 !string.startsWith("data:image/png;base64,") &&
1160 !string.startsWith("data:image/jpeg;base64,")
1163 return FORMATS.strictRelativeUrl(string, context);
1165 throw new SyntaxError(
1166 `String ${JSON.stringify(
1168 )} must be a relative or PNG or JPG data:image URL`
1175 contentSecurityPolicy(string, context) {
1176 // Manifest V3 extension_pages allows WASM. When sandbox is
1177 // implemented, or any other V3 or later directive, the flags
1178 // logic will need to be updated.
1180 context.manifestVersion < 3
1181 ? Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY
1182 : Ci.nsIAddonContentPolicy.CSP_ALLOW_WASM;
1183 let error = lazy.contentPolicyService.validateAddonCSP(string, flags);
1184 if (error != null) {
1185 // The CSP validation error is not reported as part of the "choices" error message,
1186 // we log the CSP validation error explicitly here to make it easier for the addon developers
1187 // to see and fix the extension CSP.
1188 context.logError(`Error processing ${context.currentTarget}: ${error}`);
1194 date(string, context) {
1195 // A valid ISO 8601 timestamp.
1197 /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/;
1198 if (!PATTERN.test(string)) {
1199 throw new Error(`Invalid date string ${string}`);
1201 // Our pattern just checks the format, we could still have invalid
1202 // values (e.g., month=99 or month=02 and day=31). Let the Date
1203 // constructor do the dirty work of validating.
1204 if (isNaN(Date.parse(string))) {
1205 throw new Error(`Invalid date string ${string}`);
1210 manifestShortcutKey(string, context) {
1211 if (lazy.ShortcutUtils.validate(string) == lazy.ShortcutUtils.IS_VALID) {
1215 `Value "${string}" must consist of ` +
1216 `either a combination of one or two modifiers, including ` +
1217 `a mandatory primary modifier and a key, separated by '+', ` +
1218 `or a media key. For details see: ` +
1219 `https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`;
1220 throw new Error(errorMessage);
1223 manifestShortcutKeyOrEmpty(string, context) {
1224 return string === "" ? "" : FORMATS.manifestShortcutKey(string, context);
1227 versionString(string, context) {
1228 const parts = string.split(".");
1231 // We accept up to 4 numbers.
1233 // Non-zero values cannot start with 0 and we allow numbers up to 9 digits.
1234 parts.some(part => !/^(0|[1-9][0-9]{0,8})$/.test(part))
1237 `version must be a version string consisting of at most 4 integers ` +
1238 `of at most 9 digits without leading zeros, and separated with dots`
1242 // The idea is to only emit a warning when the version string does not
1243 // match the simple format we want to encourage developers to use. Given
1244 // the version is required, we always accept the value as is.
1249 // Schema files contain namespaces, and each namespace contains types,
1250 // properties, functions, and events. An Entry is a base class for
1251 // types, properties, functions, and events.
1253 constructor(schema = {}) {
1255 * If set to any value which evaluates as true, this entry is
1256 * deprecated, and any access to it will result in a deprecation
1257 * warning being logged to the browser console.
1259 * If the value is a string, it will be appended to the deprecation
1260 * message. If it contains the substring "${value}", it will be
1261 * replaced with a string representation of the value being
1264 * If the value is any other truthy value, a generic deprecation
1265 * message will be emitted.
1267 this.deprecated = false;
1268 if ("deprecated" in schema) {
1269 this.deprecated = schema.deprecated;
1273 * @property {string} [preprocessor]
1274 * If set to a string value, and a preprocessor of the same is
1275 * defined in the validation context, it will be applied to this
1276 * value prior to any normalization.
1278 this.preprocessor = schema.preprocess || null;
1281 * @property {string} [postprocessor]
1282 * If set to a string value, and a postprocessor of the same is
1283 * defined in the validation context, it will be applied to this
1284 * value after any normalization.
1286 this.postprocessor = schema.postprocess || null;
1289 * @property {Array<string>} allowedContexts A list of allowed contexts
1290 * to consider before generating the API.
1291 * These are not parsed by the schema, but passed to `shouldInject`.
1293 this.allowedContexts = schema.allowedContexts || [];
1295 this.min_manifest_version =
1296 schema.min_manifest_version ?? MIN_MANIFEST_VERSION;
1297 this.max_manifest_version =
1298 schema.max_manifest_version ?? MAX_MANIFEST_VERSION;
1302 * Preprocess the given value with the preprocessor declared in
1306 * @param {Context} context
1309 preprocess(value, context) {
1310 if (this.preprocessor) {
1311 return context.preprocessors[this.preprocessor](value, context);
1317 * Postprocess the given result with the postprocessor declared in
1320 * @param {object} result
1321 * @param {Context} context
1324 postprocess(result, context) {
1325 if (result.error || !this.postprocessor) {
1329 let value = context.postprocessors[this.postprocessor](
1337 * Logs a deprecation warning for this entry, based on the value of
1338 * its `deprecated` property.
1340 * @param {Context} context
1341 * @param {any} [value]
1343 logDeprecation(context, value = null) {
1344 let message = "This property is deprecated";
1345 if (typeof this.deprecated == "string") {
1346 message = this.deprecated;
1347 if (message.includes("${value}")) {
1349 value = JSON.stringify(value);
1351 value = String(value);
1353 message = message.replace(/\$\{value\}/g, () => value);
1357 context.logWarning(message);
1361 * Checks whether the entry is deprecated and, if so, logs a
1362 * deprecation message.
1364 * @param {Context} context
1365 * @param {any} [value]
1367 checkDeprecated(context, value = null) {
1368 if (this.deprecated) {
1369 this.logDeprecation(context, value);
1374 * Returns an object containing property descriptor for use when
1375 * injecting this entry into an API object.
1377 * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
1378 * @param {InjectionContext} context
1380 * @returns {object?}
1381 * An object containing a `descriptor` property, specifying the
1382 * entry's property descriptor, and an optional `revoke`
1383 * method, to be called when the entry is being revoked.
1385 getDescriptor(path, context) {
1390 // Corresponds either to a type declared in the "types" section of the
1391 // schema or else to any type object used throughout the schema.
1392 class Type extends Entry {
1394 * @property {Array<string>} EXTRA_PROPERTIES
1395 * An array of extra properties which may be present for
1396 * schemas of this type.
1398 static get EXTRA_PROPERTIES() {
1406 "min_manifest_version",
1407 "max_manifest_version",
1412 * Parses the given schema object and returns an instance of this
1413 * class which corresponds to its properties.
1415 * @param {SchemaRoot} root
1416 * The root schema for this type.
1417 * @param {object} schema
1418 * A JSON schema object which corresponds to a definition of
1420 * @param {Array<string>} path
1421 * The path to this schema object from the root schema,
1422 * corresponding to the property names and array indices
1423 * traversed during parsing in order to arrive at this schema
1425 * @param {Array<string>} [extraProperties]
1426 * An array of extra property names which are valid for this
1427 * schema in the current context.
1429 * An instance of this type which corresponds to the given
1433 static parseSchema(root, schema, path, extraProperties = []) {
1434 this.checkSchemaProperties(schema, path, extraProperties);
1436 return new this(schema);
1440 * Checks that all of the properties present in the given schema
1441 * object are valid properties for this type, and throws if invalid.
1443 * @param {object} schema
1444 * A JSON schema object.
1445 * @param {Array<string>} path
1446 * The path to this schema object from the root schema,
1447 * corresponding to the property names and array indices
1448 * traversed during parsing in order to arrive at this schema
1450 * @param {Iterable<string>} [extra]
1451 * An array of extra property names which are valid for this
1452 * schema in the current context.
1454 * An error describing the first invalid property found in the
1457 static checkSchemaProperties(schema, path, extra = []) {
1459 let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]);
1461 for (let prop of Object.keys(schema)) {
1462 if (!allowedSet.has(prop)) {
1464 `Internal error: Namespace ${path.join(".")} has ` +
1465 `invalid type property "${prop}" ` +
1466 `in type "${schema.id || JSON.stringify(schema)}"`
1473 // Takes a value, checks that it has the correct type, and returns a
1474 // "normalized" version of the value. The normalized version will
1475 // include "nulls" in place of omitted optional properties. The
1476 // result of this function is either {error: "Some type error"} or
1477 // {value: <normalized-value>}.
1478 normalize(value, context) {
1479 return context.error("invalid type");
1482 // Unlike normalize, this function does a shallow check to see if
1483 // |baseType| (one of the possible getValueBaseType results) is
1484 // valid for this type. It returns true or false. It's used to fill
1485 // in optional arguments to functions before actually type checking
1487 checkBaseType(baseType) {
1491 // Helper method that simply relies on checkBaseType to implement
1492 // normalize. Subclasses can choose to use it or not.
1493 normalizeBase(type, value, context) {
1494 if (this.checkBaseType(getValueBaseType(value))) {
1495 this.checkDeprecated(context, value);
1496 return { value: this.preprocess(value, context) };
1500 if ("aeiou".includes(type[0])) {
1501 choice = `be an ${type} value`;
1503 choice = `be a ${type} value`;
1506 return context.error(
1507 () => `Expected ${type} instead of ${JSON.stringify(value)}`,
1513 // Type that allows any value.
1514 class AnyType extends Type {
1515 normalize(value, context) {
1516 this.checkDeprecated(context, value);
1517 return this.postprocess({ value }, context);
1520 checkBaseType(baseType) {
1525 // An untagged union type.
1526 class ChoiceType extends Type {
1527 static get EXTRA_PROPERTIES() {
1528 return ["choices", ...super.EXTRA_PROPERTIES];
1531 /** @type {(root, schema, path, extraProperties?: Iterable) => ChoiceType} */
1532 static parseSchema(root, schema, path, extraProperties = []) {
1533 this.checkSchemaProperties(schema, path, extraProperties);
1535 let choices = schema.choices.map(t => root.parseSchema(t, path));
1536 return new this(schema, choices);
1539 constructor(schema, choices) {
1541 this.choices = choices;
1545 this.choices.push(...type.choices);
1550 normalize(value, context) {
1551 this.checkDeprecated(context, value);
1554 let { choices, result } = context.withChoices(() => {
1555 for (let choice of this.choices) {
1556 // Ignore a possible choice if it is not supported by
1557 // the manifest version we are normalizing.
1558 if (!context.matchManifestVersion(choice)) {
1562 let r = choice.normalize(value, context);
1574 if (choices.size <= 1) {
1578 choices = Array.from(choices, forceString);
1579 let n = choices.length - 1;
1580 choices[n] = `or ${choices[n]}`;
1583 if (typeof value === "object") {
1584 message = () => `Value must either: ${choices.join(", ")}`;
1587 `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`;
1590 return context.error(message, null);
1593 checkBaseType(baseType) {
1594 return this.choices.some(t => t.checkBaseType(baseType));
1597 getDescriptor(path, context) {
1598 // In StringType.getDescriptor, unlike any other Type, a descriptor is returned if
1599 // it is an enumeration. Since we need versioned choices in some cases, here we
1600 // build a list of valid enumerations that will work for a given manifest version.
1602 !this.choices.length ||
1603 !this.choices.every(t => t.checkBaseType("string") && t.enumeration)
1608 let obj = Cu.createObjectIn(context.cloneScope);
1609 let descriptor = { value: obj };
1610 for (let choice of this.choices) {
1611 // Ignore a possible choice if it is not supported by
1612 // the manifest version we are normalizing.
1613 if (!context.matchManifestVersion(choice)) {
1616 let d = choice.getDescriptor(path, context);
1618 Object.assign(obj, d.descriptor.value);
1622 return { descriptor };
1626 // This is a reference to another type--essentially a typedef.
1627 class RefType extends Type {
1628 static get EXTRA_PROPERTIES() {
1629 return ["$ref", ...super.EXTRA_PROPERTIES];
1632 /** @type {(root, schema, path, extraProperties?: Iterable) => RefType} */
1633 static parseSchema(root, schema, path, extraProperties = []) {
1634 this.checkSchemaProperties(schema, path, extraProperties);
1636 let ref = schema.$ref;
1637 let ns = path.join(".");
1638 if (ref.includes(".")) {
1639 [, ns, ref] = /^(.*)\.(.*?)$/.exec(ref);
1641 return new this(root, schema, ns, ref);
1644 // For a reference to a type named T declared in namespace NS,
1645 // namespaceName will be NS and reference will be T.
1646 constructor(root, schema, namespaceName, reference) {
1649 this.namespaceName = namespaceName;
1650 this.reference = reference;
1654 let ns = this.root.getNamespace(this.namespaceName);
1655 let type = ns.get(this.reference);
1657 throw new Error(`Internal error: Type ${this.reference} not found`);
1662 normalize(value, context) {
1663 this.checkDeprecated(context, value);
1664 return this.targetType.normalize(value, context);
1667 checkBaseType(baseType) {
1668 return this.targetType.checkBaseType(baseType);
1672 class StringType extends Type {
1673 static get EXTRA_PROPERTIES() {
1680 ...super.EXTRA_PROPERTIES,
1684 static parseSchema(root, schema, path, extraProperties = []) {
1685 this.checkSchemaProperties(schema, path, extraProperties);
1687 let enumeration = schema.enum || null;
1689 // The "enum" property is either a list of strings that are
1690 // valid values or else a list of {name, description} objects,
1691 // where the .name values are the valid values.
1692 enumeration = enumeration.map(e => {
1693 if (typeof e == "object") {
1701 if (schema.pattern) {
1703 pattern = parsePattern(schema.pattern);
1706 `Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}`
1712 if (schema.format) {
1713 if (!(schema.format in FORMATS)) {
1715 `Internal error: Invalid string format ${schema.format}`
1718 format = FORMATS[schema.format];
1722 schema.id || undefined,
1724 schema.minLength || 0,
1725 schema.maxLength || Infinity,
1742 this.enumeration = enumeration;
1743 this.minLength = minLength;
1744 this.maxLength = maxLength;
1745 this.pattern = pattern;
1746 this.format = format;
1749 normalize(value, context) {
1750 let r = this.normalizeBase("string", value, context);
1756 if (this.enumeration) {
1757 if (this.enumeration.includes(value)) {
1758 return this.postprocess({ value }, context);
1761 let choices = this.enumeration.map(JSON.stringify).join(", ");
1763 return context.error(
1764 () => `Invalid enumeration value ${JSON.stringify(value)}`,
1765 `be one of [${choices}]`
1769 if (value.length < this.minLength) {
1770 return context.error(
1772 `String ${JSON.stringify(value)} is too short (must be ${
1775 `be longer than ${this.minLength}`
1778 if (value.length > this.maxLength) {
1779 return context.error(
1781 `String ${JSON.stringify(value)} is too long (must be ${
1784 `be shorter than ${this.maxLength}`
1788 if (this.pattern && !this.pattern.test(value)) {
1789 return context.error(
1790 () => `String ${JSON.stringify(value)} must match ${this.pattern}`,
1791 `match the pattern ${this.pattern.toSource()}`
1797 r.value = this.format(r.value, context);
1799 return context.error(
1801 `match the format "${this.format.name}"`
1809 checkBaseType(baseType) {
1810 return baseType == "string";
1813 getDescriptor(path, context) {
1814 if (this.enumeration) {
1815 let obj = Cu.createObjectIn(context.cloneScope);
1817 for (let e of this.enumeration) {
1818 obj[e.toUpperCase()] = e;
1822 descriptor: { value: obj },
1828 class NullType extends Type {
1829 normalize(value, context) {
1830 return this.normalizeBase("null", value, context);
1833 checkBaseType(baseType) {
1834 return baseType == "null";
1842 class ObjectType extends Type {
1843 static get EXTRA_PROPERTIES() {
1846 "patternProperties",
1848 ...super.EXTRA_PROPERTIES,
1852 static parseSchema(root, schema, path, extraProperties = []) {
1853 if ("functions" in schema) {
1854 return SubModuleType.parseSchema(root, schema, path, extraProperties);
1857 if (DEBUG && !("$extend" in schema)) {
1858 // Only allow extending "properties" and "patternProperties".
1860 "additionalProperties",
1865 this.checkSchemaProperties(schema, path, extraProperties);
1867 let imported = null;
1868 if ("$import" in schema) {
1869 let importPath = schema.$import;
1870 let idx = importPath.indexOf(".");
1872 imported = [path[0], importPath];
1874 imported = [importPath.slice(0, idx), importPath.slice(idx + 1)];
1878 let parseProperty = (schema, extraProps = []) => {
1880 type: root.parseSchema(
1891 optional: schema.optional || false,
1892 unsupported: schema.unsupported || false,
1893 onError: schema.onError || null,
1894 default: schema.default === undefined ? null : schema.default,
1898 // Parse explicit "properties" object.
1899 let properties = Object.create(null);
1900 for (let propName of Object.keys(schema.properties || {})) {
1901 properties[propName] = parseProperty(schema.properties[propName], [
1906 // Parse regexp properties from "patternProperties" object.
1907 let patternProperties = [];
1908 for (let propName of Object.keys(schema.patternProperties || {})) {
1911 pattern = parsePattern(propName);
1914 `Internal error: Invalid property pattern ${JSON.stringify(propName)}`
1918 patternProperties.push({
1920 type: parseProperty(schema.patternProperties[propName]),
1924 // Parse "additionalProperties" schema.
1925 let additionalProperties = null;
1926 if (schema.additionalProperties) {
1927 let type = schema.additionalProperties;
1928 if (type === true) {
1929 type = { type: "any" };
1932 additionalProperties = root.parseSchema(type, path);
1938 additionalProperties,
1940 schema.isInstanceOf || null,
1948 additionalProperties,
1954 this.properties = properties;
1955 this.additionalProperties = additionalProperties;
1956 this.patternProperties = patternProperties;
1957 this.isInstanceOf = isInstanceOf;
1960 let [ns, path] = imported;
1961 ns = Schemas.getNamespace(ns);
1962 let importedType = ns.get(path);
1963 if (!importedType) {
1964 throw new Error(`Internal error: imported type ${path} not found`);
1967 if (DEBUG && !(importedType instanceof ObjectType)) {
1969 `Internal error: cannot import non-object type ${path}`
1973 this.properties = Object.assign(
1975 importedType.properties,
1978 this.patternProperties = [
1979 ...importedType.patternProperties,
1980 ...this.patternProperties,
1982 this.additionalProperties =
1983 importedType.additionalProperties || this.additionalProperties;
1988 for (let key of Object.keys(type.properties)) {
1989 if (key in this.properties) {
1991 `InternalError: Attempt to extend an object with conflicting property "${key}"`
1994 this.properties[key] = type.properties[key];
1997 this.patternProperties.push(...type.patternProperties);
2002 checkBaseType(baseType) {
2003 return baseType == "object";
2007 * Extracts the enumerable properties of the given object, including
2008 * function properties which would normally be omitted by X-ray
2011 * @param {object} value
2012 * @param {Context} context
2013 * The current parse context.
2015 * An object with an `error` or `value` property.
2017 extractProperties(value, context) {
2018 // |value| should be a JS Xray wrapping an object in the
2019 // extension compartment. This works well except when we need to
2020 // access callable properties on |value| since JS Xrays don't
2021 // support those. To work around the problem, we verify that
2022 // |value| is a plain JS object (i.e., not anything scary like a
2023 // Proxy). Then we copy the properties out of it into a normal
2024 // object using a waiver wrapper.
2026 let klass = ChromeUtils.getClassName(value, true);
2027 if (klass != "Object") {
2028 throw context.error(
2029 `Expected a plain JavaScript object, got a ${klass}`,
2030 `be a plain JavaScript object`
2034 return ChromeUtils.shallowClone(value);
2037 checkProperty(context, prop, propType, result, properties, remainingProps) {
2038 let { type, optional, unsupported, onError } = propType;
2041 if (!context.matchManifestVersion(type)) {
2042 if (prop in properties) {
2043 error = context.error(
2044 `Property "${prop}" is unsupported in Manifest Version ${context.manifestVersion}`,
2045 `not contain an unsupported "${prop}" property`
2048 context.logWarning(forceString(error.error));
2049 if (this.additionalProperties) {
2050 // When `additionalProperties` is set to UnrecognizedProperty, the
2051 // caller (i.e. ObjectType's normalize method) assigns the original
2052 // value to `result[prop]`. Erase the property now to prevent
2053 // `result[prop]` from becoming anything other than `undefined.
2055 // A warning was already logged above, so we do not need to also log
2056 // "An unexpected property was found in the WebExtension manifest."
2057 remainingProps.delete(prop);
2059 // When `additionalProperties` is not set, ObjectType's normalize method
2060 // will return an error because prop is still in remainingProps.
2063 } else if (unsupported) {
2064 if (prop in properties) {
2065 error = context.error(
2066 `Property "${prop}" is unsupported by Firefox`,
2067 `not contain an unsupported "${prop}" property`
2070 } else if (prop in properties) {
2073 (properties[prop] === null || properties[prop] === undefined)
2075 result[prop] = propType.default;
2077 let r = context.withPath(prop, () =>
2078 type.normalize(properties[prop], context)
2083 result[prop] = r.value;
2084 properties[prop] = r.value;
2087 remainingProps.delete(prop);
2088 } else if (!optional) {
2089 error = context.error(
2090 `Property "${prop}" is required`,
2091 `contain the required "${prop}" property`
2093 } else if (optional !== "omit-key-if-missing") {
2094 result[prop] = propType.default;
2098 if (onError == "warn") {
2099 context.logWarning(forceString(error.error));
2100 } else if (onError != "ignore") {
2104 result[prop] = propType.default;
2108 normalize(value, context) {
2110 let v = this.normalizeBase("object", value, context);
2116 if (this.isInstanceOf) {
2119 Object.keys(this.properties).length ||
2120 this.patternProperties.length ||
2121 !(this.additionalProperties instanceof AnyType)
2124 "InternalError: isInstanceOf can only be used " +
2125 "with objects that are otherwise unrestricted"
2131 ChromeUtils.getClassName(value) !== this.isInstanceOf &&
2132 (this.isInstanceOf !== "Element" || value.nodeType !== 1)
2134 return context.error(
2135 `Object must be an instance of ${this.isInstanceOf}`,
2136 `be an instance of ${this.isInstanceOf}`
2140 // This is kind of a hack, but we can't normalize things that
2141 // aren't JSON, so we just return them.
2142 return this.postprocess({ value }, context);
2145 let properties = this.extractProperties(value, context);
2146 let remainingProps = new Set(Object.keys(properties));
2149 for (let prop of Object.keys(this.properties)) {
2153 this.properties[prop],
2160 for (let prop of Object.keys(properties)) {
2161 for (let { pattern, type } of this.patternProperties) {
2162 if (pattern.test(prop)) {
2175 if (this.additionalProperties) {
2176 for (let prop of remainingProps) {
2177 let r = context.withPath(prop, () =>
2178 this.additionalProperties.normalize(properties[prop], context)
2183 result[prop] = r.value;
2185 } else if (remainingProps.size == 1) {
2186 return context.error(
2187 `Unexpected property "${[...remainingProps]}"`,
2188 `not contain an unexpected "${[...remainingProps]}" property`
2190 } else if (remainingProps.size) {
2191 let props = [...remainingProps].sort().join(", ");
2192 return context.error(
2193 `Unexpected properties: ${props}`,
2194 `not contain the unexpected properties [${props}]`
2198 return this.postprocess({ value: result }, context);
2208 // This type is just a placeholder to be referred to by
2209 // SubModuleProperty. No value is ever expected to have this type.
2210 SubModuleType = class SubModuleType extends Type {
2211 static get EXTRA_PROPERTIES() {
2212 return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
2215 static parseSchema(root, schema, path, extraProperties = []) {
2216 this.checkSchemaProperties(schema, path, extraProperties);
2218 // The path we pass in here is only used for error messages.
2219 path = [...path, schema.id];
2220 let functions = schema.functions
2221 .filter(fun => !fun.unsupported)
2222 .map(fun => FunctionEntry.parseSchema(root, fun, path));
2226 if (schema.events) {
2227 events = schema.events
2228 .filter(event => !event.unsupported)
2229 .map(event => Event.parseSchema(root, event, path));
2232 return new this(schema, functions, events);
2235 constructor(schema, functions, events) {
2236 // schema contains properties such as min/max_manifest_version needed
2237 // in the base class so that the Context class can version compare
2238 // any entries against the manifest version.
2240 this.functions = functions;
2241 this.events = events;
2245 class NumberType extends Type {
2246 normalize(value, context) {
2247 let r = this.normalizeBase("number", value, context);
2252 if (isNaN(r.value) || !Number.isFinite(r.value)) {
2253 return context.error(
2254 "NaN and infinity are not valid",
2255 "be a finite number"
2262 checkBaseType(baseType) {
2263 return baseType == "number" || baseType == "integer";
2267 class IntegerType extends Type {
2268 static get EXTRA_PROPERTIES() {
2269 return ["minimum", "maximum", ...super.EXTRA_PROPERTIES];
2272 static parseSchema(root, schema, path, extraProperties = []) {
2273 this.checkSchemaProperties(schema, path, extraProperties);
2275 let { minimum = -Infinity, maximum = Infinity } = schema;
2276 return new this(schema, minimum, maximum);
2279 constructor(schema, minimum, maximum) {
2281 this.minimum = minimum;
2282 this.maximum = maximum;
2285 normalize(value, context) {
2286 let r = this.normalizeBase("integer", value, context);
2292 // Ensure it's between -2**31 and 2**31-1
2293 if (!Number.isSafeInteger(value)) {
2294 return context.error(
2295 "Integer is out of range",
2296 "be a valid 32 bit signed integer"
2300 if (value < this.minimum) {
2301 return context.error(
2302 `Integer ${value} is too small (must be at least ${this.minimum})`,
2303 `be at least ${this.minimum}`
2306 if (value > this.maximum) {
2307 return context.error(
2308 `Integer ${value} is too big (must be at most ${this.maximum})`,
2309 `be no greater than ${this.maximum}`
2313 return this.postprocess(r, context);
2316 checkBaseType(baseType) {
2317 return baseType == "integer";
2321 class BooleanType extends Type {
2322 static get EXTRA_PROPERTIES() {
2323 return ["enum", ...super.EXTRA_PROPERTIES];
2326 static parseSchema(root, schema, path, extraProperties = []) {
2327 this.checkSchemaProperties(schema, path, extraProperties);
2328 let enumeration = schema.enum || null;
2329 return new this(schema, enumeration);
2332 constructor(schema, enumeration) {
2334 this.enumeration = enumeration;
2337 normalize(value, context) {
2338 if (!this.checkBaseType(getValueBaseType(value))) {
2339 return context.error(
2340 () => `Expected boolean instead of ${JSON.stringify(value)}`,
2344 value = this.preprocess(value, context);
2345 if (this.enumeration && !this.enumeration.includes(value)) {
2346 return context.error(
2347 () => `Invalid value ${JSON.stringify(value)}`,
2348 `be ${this.enumeration}`
2351 this.checkDeprecated(context, value);
2355 checkBaseType(baseType) {
2356 return baseType == "boolean";
2360 class ArrayType extends Type {
2361 static get EXTRA_PROPERTIES() {
2362 return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
2365 static parseSchema(root, schema, path, extraProperties = []) {
2366 this.checkSchemaProperties(schema, path, extraProperties);
2368 let items = root.parseSchema(schema.items, path, ["onError"]);
2373 schema.minItems || 0,
2374 schema.maxItems || Infinity
2378 constructor(schema, itemType, minItems, maxItems) {
2380 this.itemType = itemType;
2381 this.minItems = minItems;
2382 this.maxItems = maxItems;
2383 this.onError = schema.items.onError || null;
2386 normalize(value, context) {
2387 let v = this.normalizeBase("array", value, context);
2394 for (let [i, element] of value.entries()) {
2395 element = context.withPath(String(i), () =>
2396 this.itemType.normalize(element, context)
2398 if (element.error) {
2399 if (this.onError == "warn") {
2400 context.logWarning(forceString(element.error));
2401 } else if (this.onError != "ignore") {
2406 result.push(element.value);
2409 if (result.length < this.minItems) {
2410 return context.error(
2411 `Array requires at least ${this.minItems} items; you have ${result.length}`,
2412 `have at least ${this.minItems} items`
2416 if (result.length > this.maxItems) {
2417 return context.error(
2418 `Array requires at most ${this.maxItems} items; you have ${result.length}`,
2419 `have at most ${this.maxItems} items`
2423 return this.postprocess({ value: result }, context);
2426 checkBaseType(baseType) {
2427 return baseType == "array";
2431 class FunctionType extends Type {
2432 static get EXTRA_PROPERTIES() {
2438 ...super.EXTRA_PROPERTIES,
2442 static parseSchema(root, schema, path, extraProperties = []) {
2443 this.checkSchemaProperties(schema, path, extraProperties);
2445 let isAsync = !!schema.async;
2446 let isExpectingCallback = typeof schema.async === "string";
2447 let parameters = null;
2448 if ("parameters" in schema) {
2450 for (let param of schema.parameters) {
2451 // Callbacks default to optional for now, because of promise
2453 let isCallback = isAsync && param.name == schema.async;
2455 isExpectingCallback = false;
2459 type: root.parseSchema(param, path, ["name", "optional", "default"]),
2461 optional: param.optional == null ? isCallback : param.optional,
2462 default: param.default == undefined ? null : param.default,
2466 let hasAsyncCallback = false;
2470 parameters.length &&
2471 parameters[parameters.length - 1].name == schema.async;
2475 if (isExpectingCallback) {
2477 `Internal error: Expected a callback parameter ` +
2478 `with name ${schema.async}`
2482 if (isAsync && schema.returns) {
2484 "Internal error: Async functions must not have return values."
2489 schema.allowAmbiguousOptionalArguments &&
2493 "Internal error: Async functions with ambiguous " +
2494 "arguments must declare the callback as the last parameter"
2504 !!schema.requireUserInput
2508 constructor(schema, parameters, isAsync, hasAsyncCallback, requireUserInput) {
2510 this.parameters = parameters;
2511 this.isAsync = isAsync;
2512 this.hasAsyncCallback = hasAsyncCallback;
2513 this.requireUserInput = requireUserInput;
2516 normalize(value, context) {
2517 return this.normalizeBase("function", value, context);
2520 checkBaseType(baseType) {
2521 return baseType == "function";
2525 // Represents a "property" defined in a schema namespace with a
2526 // particular value. Essentially this is a constant.
2527 class ValueProperty extends Entry {
2528 constructor(schema, name, value) {
2534 getDescriptor(path, context) {
2535 // Prevent injection if not a supported version.
2536 if (!context.matchManifestVersion(this)) {
2541 descriptor: { value: this.value },
2546 // Represents a "property" defined in a schema namespace that is not a
2548 class TypeProperty extends Entry {
2549 unsupported = false;
2551 constructor(schema, path, name, type, writable, permissions) {
2556 this.writable = writable;
2557 this.permissions = permissions;
2560 throwError(context, msg) {
2561 throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2564 getDescriptor(path, context) {
2565 if (this.unsupported || !context.matchManifestVersion(this)) {
2569 let apiImpl = context.getImplementation(path.join("."), this.name);
2571 let getStub = () => {
2572 this.checkDeprecated(context);
2573 return apiImpl.getProperty();
2577 get: Cu.exportFunction(getStub, context.cloneScope),
2580 if (this.writable) {
2581 let setStub = value => {
2582 let normalized = this.type.normalize(value, context);
2583 if (normalized.error) {
2584 this.throwError(context, forceString(normalized.error));
2587 apiImpl.setProperty(normalized.value);
2590 descriptor.set = Cu.exportFunction(setStub, context.cloneScope);
2603 class SubModuleProperty extends Entry {
2604 // A SubModuleProperty represents a tree of objects and properties
2605 // to expose to an extension. Currently we support only a limited
2606 // form of sub-module properties, where "$ref" points to a
2607 // SubModuleType containing a list of functions and "properties" is
2608 // a list of additional simple properties.
2610 // name: Name of the property stuff is being added to.
2611 // namespaceName: Namespace in which the property lives.
2612 // reference: Name of the type defining the functions to add to the property.
2613 // properties: Additional properties to add to the module (unsupported).
2614 constructor(root, schema, path, name, reference, properties, permissions) {
2619 this.namespaceName = path.join(".");
2620 this.reference = reference;
2621 this.properties = properties;
2622 this.permissions = permissions;
2626 let ns = this.root.getNamespace(this.namespaceName);
2627 let type = ns.get(this.reference);
2628 if (!type && this.reference.includes(".")) {
2629 let [namespaceName, ref] = this.reference.split(".");
2630 ns = this.root.getNamespace(namespaceName);
2636 getDescriptor(path, context) {
2637 let obj = Cu.createObjectIn(context.cloneScope);
2639 let ns = this.root.getNamespace(this.namespaceName);
2640 let type = this.targetType;
2642 // Prevent injection if not a supported version.
2643 if (!context.matchManifestVersion(type)) {
2648 if (!type || !(type instanceof SubModuleType)) {
2650 `Internal error: ${this.namespaceName}.${this.reference} ` +
2651 `is not a sub-module`
2655 let subpath = [...path, this.name];
2657 let functions = type.functions;
2658 for (let fun of functions) {
2659 context.injectInto(fun, obj, fun.name, subpath, ns);
2662 let events = type.events;
2663 for (let event of events) {
2664 context.injectInto(event, obj, event.name, subpath, ns);
2667 // TODO: Inject this.properties.
2670 descriptor: { value: obj },
2672 let unwrapped = ChromeUtils.waiveXrays(obj);
2673 for (let fun of functions) {
2675 delete unwrapped[fun.name];
2685 // This class is a base class for FunctionEntrys and Events. It takes
2686 // care of validating parameter lists (i.e., handling of optional
2687 // parameters and parameter type checking).
2688 class CallEntry extends Entry {
2689 hasAsyncCallback = false;
2691 constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) {
2695 this.parameters = parameters;
2696 this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments;
2699 throwError(context, msg) {
2700 throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2703 checkParameters(args, context) {
2706 // First we create a new array, fixedArgs, that is the same as
2707 // |args| but with default values in place of omitted optional parameters.
2708 let check = (parameterIndex, argIndex) => {
2709 if (parameterIndex == this.parameters.length) {
2710 if (argIndex == args.length) {
2716 let parameter = this.parameters[parameterIndex];
2717 if (parameter.optional) {
2719 fixedArgs[parameterIndex] = parameter.default;
2720 if (check(parameterIndex + 1, argIndex)) {
2725 if (argIndex == args.length) {
2729 let arg = args[argIndex];
2730 if (!parameter.type.checkBaseType(getValueBaseType(arg))) {
2731 // For Chrome compatibility, use the default value if null or undefined
2732 // is explicitly passed but is not a valid argument in this position.
2733 if (parameter.optional && (arg === null || arg === undefined)) {
2734 fixedArgs[parameterIndex] = Cu.cloneInto(parameter.default, {});
2739 fixedArgs[parameterIndex] = arg;
2742 return check(parameterIndex + 1, argIndex + 1);
2745 if (this.allowAmbiguousOptionalArguments) {
2746 // When this option is set, it's up to the implementation to
2748 // The last argument for asynchronous methods is either a function or null.
2749 // This is specifically done for runtime.sendMessage.
2750 if (this.hasAsyncCallback && typeof args[args.length - 1] != "function") {
2755 let success = check(0, 0);
2757 this.throwError(context, "Incorrect argument types");
2760 // Now we normalize (and fully type check) all non-omitted arguments.
2761 fixedArgs = fixedArgs.map((arg, parameterIndex) => {
2765 let parameter = this.parameters[parameterIndex];
2766 let r = parameter.type.normalize(arg, context);
2770 `Type error for parameter ${parameter.name} (${forceString(r.error)})`
2780 // Represents a "function" defined in a schema namespace.
2781 FunctionEntry = class FunctionEntry extends CallEntry {
2782 static parseSchema(root, schema, path) {
2783 // When not in DEBUG mode, we just need to know *if* this returns.
2784 /** @type {boolean|object} */
2785 let returns = !!schema.returns;
2786 if (DEBUG && "returns" in schema) {
2788 type: root.parseSchema(schema.returns, path, ["optional", "name"]),
2789 optional: schema.returns.optional || false,
2798 root.parseSchema(schema, path, [
2803 "allowAmbiguousOptionalArguments",
2804 "allowCrossOriginArguments",
2806 schema.unsupported || false,
2807 schema.allowAmbiguousOptionalArguments || false,
2808 schema.allowCrossOriginArguments || false,
2810 schema.permissions || null
2820 allowAmbiguousOptionalArguments,
2821 allowCrossOriginArguments,
2825 super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
2826 this.unsupported = unsupported;
2827 this.returns = returns;
2828 this.permissions = permissions;
2829 this.allowCrossOriginArguments = allowCrossOriginArguments;
2831 this.isAsync = type.isAsync;
2832 this.hasAsyncCallback = type.hasAsyncCallback;
2833 this.requireUserInput = type.requireUserInput;
2836 checkValue({ type, optional, name }, value, context) {
2837 if (optional && value == null) {
2841 type.reference === "ExtensionPanel" ||
2842 type.reference === "ExtensionSidebarPane" ||
2843 type.reference === "Port"
2845 // TODO: We currently treat objects with functions as SubModuleType,
2846 // which is just wrong, and a bigger yak. Skipping for now.
2849 const { error } = type.normalize(value, context);
2853 `Type error for ${name} value (${forceString(error)})`
2858 checkCallback(args, context) {
2859 const callback = this.parameters[this.parameters.length - 1];
2860 for (const [i, param] of callback.type.parameters.entries()) {
2861 this.checkValue(param, args[i], context);
2865 getDescriptor(path, context) {
2866 let apiImpl = context.getImplementation(path.join("."), this.name);
2870 stub = (...args) => {
2871 this.checkDeprecated(context);
2872 let actuals = this.checkParameters(args, context);
2873 let callback = null;
2874 if (this.hasAsyncCallback) {
2875 callback = actuals.pop();
2877 if (callback === null && context.isChromeCompat) {
2878 // We pass an empty stub function as a default callback for
2879 // the `chrome` API, so promise objects are not returned,
2880 // and lastError values are reported immediately.
2881 callback = () => {};
2883 if (DEBUG && this.hasAsyncCallback && callback) {
2884 let original = callback;
2885 callback = (...args) => {
2886 this.checkCallback(args, context);
2890 let result = apiImpl.callAsyncFunction(
2893 this.requireUserInput
2895 if (DEBUG && this.hasAsyncCallback && !callback) {
2896 return result.then(result => {
2897 this.checkCallback([result], context);
2903 } else if (!this.returns) {
2904 stub = (...args) => {
2905 this.checkDeprecated(context);
2906 let actuals = this.checkParameters(args, context);
2907 return apiImpl.callFunctionNoReturn(actuals);
2910 stub = (...args) => {
2911 this.checkDeprecated(context);
2912 let actuals = this.checkParameters(args, context);
2913 let result = apiImpl.callFunction(actuals);
2914 if (DEBUG && this.returns) {
2915 this.checkValue(this.returns, result, context);
2923 value: Cu.exportFunction(stub, context.cloneScope, {
2924 allowCrossOriginArguments: this.allowCrossOriginArguments,
2935 // Represents an "event" defined in a schema namespace.
2937 // TODO Bug 1369722: we should be able to remove the eslint-disable-line that follows
2938 // once Bug 1369722 has been fixed.
2939 // eslint-disable-next-line no-global-assign
2940 Event = class Event extends CallEntry {
2941 static parseSchema(root, event, path) {
2942 let extraParameters = Array.from(event.extraParameters || [], param => ({
2943 type: root.parseSchema(param, path, ["name", "optional", "default"]),
2945 optional: param.optional || false,
2946 default: param.default == undefined ? null : param.default,
2949 let extraProperties = [
2954 // We ignore these properties for now.
2963 root.parseSchema(event, path, extraProperties),
2965 event.unsupported || false,
2966 event.permissions || null
2979 super(schema, path, name, extraParameters);
2981 this.unsupported = unsupported;
2982 this.permissions = permissions;
2985 checkListener(listener, context) {
2986 let r = this.type.normalize(listener, context);
2988 this.throwError(context, "Invalid listener");
2993 getDescriptor(path, context) {
2994 let apiImpl = context.getImplementation(path.join("."), this.name);
2996 let addStub = (listener, ...args) => {
2997 listener = this.checkListener(listener, context);
2998 let actuals = this.checkParameters(args, context);
2999 apiImpl.addListener(listener, actuals);
3002 let removeStub = listener => {
3003 listener = this.checkListener(listener, context);
3004 apiImpl.removeListener(listener);
3007 let hasStub = listener => {
3008 listener = this.checkListener(listener, context);
3009 return apiImpl.hasListener(listener);
3012 let obj = Cu.createObjectIn(context.cloneScope);
3014 Cu.exportFunction(addStub, obj, { defineAs: "addListener" });
3015 Cu.exportFunction(removeStub, obj, { defineAs: "removeListener" });
3016 Cu.exportFunction(hasStub, obj, { defineAs: "hasListener" });
3019 descriptor: { value: obj },
3024 let unwrapped = ChromeUtils.waiveXrays(obj);
3025 delete unwrapped.addListener;
3026 delete unwrapped.removeListener;
3027 delete unwrapped.hasListener;
3033 const TYPES = Object.freeze(
3034 Object.assign(Object.create(null), {
3037 boolean: BooleanType,
3038 function: FunctionType,
3039 integer: IntegerType,
3048 events: "loadEvent",
3049 functions: "loadFunction",
3050 properties: "loadProperty",
3054 class Namespace extends Map {
3055 constructor(root, name, path) {
3060 this._lazySchemas = [];
3061 this.initialized = false;
3064 this.path = name ? [...path, name] : [...path];
3066 this.superNamespace = null;
3068 this.min_manifest_version = MIN_MANIFEST_VERSION;
3069 this.max_manifest_version = MAX_MANIFEST_VERSION;
3071 this.permissions = null;
3072 this.allowedContexts = [];
3073 this.defaultContexts = [];
3077 * Adds a JSON Schema object to the set of schemas that represent this
3080 * @param {object} schema
3081 * A JSON schema object which partially describes this
3085 this._lazySchemas.push(schema);
3091 "min_manifest_version",
3092 "max_manifest_version",
3095 this[prop] = schema[prop];
3099 if (schema.$import) {
3100 this.superNamespace = this.root.getNamespace(schema.$import);
3105 * Initializes the keys of this namespace based on the schema objects
3106 * added via previous `addSchema` calls.
3109 if (this.initialized) {
3113 if (this.superNamespace) {
3114 this._lazySchemas.unshift(...this.superNamespace._lazySchemas);
3117 // Keep in sync with LOADERS above.
3118 this.types = new DefaultMap(() => []);
3119 this.properties = new DefaultMap(() => []);
3120 this.functions = new DefaultMap(() => []);
3121 this.events = new DefaultMap(() => []);
3123 for (let schema of this._lazySchemas) {
3124 for (let type of schema.types || []) {
3125 if (!type.unsupported) {
3126 this.types.get(type.$extend || type.id).push(type);
3130 for (let [name, prop] of Object.entries(schema.properties || {})) {
3131 if (!prop.unsupported) {
3132 this.properties.get(name).push(prop);
3136 for (let fun of schema.functions || []) {
3137 if (!fun.unsupported) {
3138 this.functions.get(fun.name).push(fun);
3142 for (let event of schema.events || []) {
3143 if (!event.unsupported) {
3144 this.events.get(event.name).push(event);
3149 // For each type of top-level property in the schema object, iterate
3150 // over all properties of that type, and create a temporary key for
3151 // each property pointing to its type. Those temporary properties
3152 // are later used to instantiate an Entry object based on the actual
3154 for (let type of Object.keys(LOADERS)) {
3155 for (let key of this[type].keys()) {
3156 this.set(key, type);
3160 this.initialized = true;
3163 for (let key of this.keys()) {
3170 * Initializes the value of a given key, by parsing the schema object
3171 * associated with it and replacing its temporary value with an `Entry`
3174 * @param {string} key
3175 * The name of the property to initialize.
3176 * @param {string} type
3177 * The type of property the key represents. Must have a
3178 * corresponding entry in the `LOADERS` object, pointing to the
3179 * initialization method for that type.
3183 initKey(key, type) {
3184 let loader = LOADERS[type];
3186 for (let schema of this[type].get(key)) {
3187 this.set(key, this[loader](key, schema));
3190 return this.get(key);
3193 loadType(name, type) {
3194 if ("$extend" in type) {
3195 return this.extendType(type);
3197 return this.root.parseSchema(type, this.path, ["id"]);
3201 let targetType = this.get(type.$extend);
3203 // Only allow extending object and choices types for now.
3204 if (targetType instanceof ObjectType) {
3205 type.type = "object";
3209 `Internal error: Attempt to extend a nonexistent type ${type.$extend}`
3211 } else if (!(targetType instanceof ChoiceType)) {
3213 `Internal error: Attempt to extend a non-extensible type ${type.$extend}`
3218 let parsed = this.root.parseSchema(type, this.path, ["$extend"]);
3220 if (DEBUG && parsed.constructor !== targetType.constructor) {
3221 throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
3224 targetType.extend(parsed);
3229 loadProperty(name, prop) {
3230 if ("$ref" in prop) {
3231 if (!prop.unsupported) {
3232 return new SubModuleProperty(
3238 prop.properties || {},
3239 prop.permissions || null
3242 } else if ("value" in prop) {
3243 return new ValueProperty(prop, name, prop.value);
3245 // We ignore the "optional" attribute on properties since we
3246 // don't inject anything here anyway.
3247 let type = this.root.parseSchema(
3250 ["optional", "permissions", "writable"]
3252 return new TypeProperty(
3257 prop.writable || false,
3258 prop.permissions || null
3263 loadFunction(name, fun) {
3264 return FunctionEntry.parseSchema(this.root, fun, this.path);
3267 loadEvent(name, event) {
3268 return Event.parseSchema(this.root, event, this.path);
3272 * Injects the properties of this namespace into the given object.
3274 * @param {object} dest
3275 * The object into which to inject the namespace properties.
3276 * @param {InjectionContext} context
3277 * The injection context with which to inject the properties.
3279 injectInto(dest, context) {
3280 for (let name of this.keys()) {
3281 // If the entry does not match the manifest version do not
3282 // inject the property. This prevents the item from being
3283 // enumerable in the namespace object. We cannot accomplish
3284 // this inside exportLazyProperty, it specifically injects
3285 // an enumerable object.
3286 let entry = this.get(name);
3287 if (!context.matchManifestVersion(entry)) {
3290 exportLazyProperty(dest, name, () => {
3291 let entry = this.get(name);
3293 return context.getDescriptor(entry, dest, name, this.path, this);
3298 getDescriptor(path, context) {
3299 let obj = Cu.createObjectIn(context.cloneScope);
3301 let ns = context.schemaRoot.getNamespace(this.path.join("."));
3302 ns.injectInto(obj, context);
3304 // Only inject the namespace object if it isn't empty.
3305 if (Object.keys(obj).length) {
3307 descriptor: { value: obj },
3314 return super.keys();
3317 /** @returns {Generator<[string, Entry]>} */
3319 for (let key of this.keys()) {
3320 yield [key, this.get(key)];
3326 let value = super.get(key);
3328 // The initial values of lazily-initialized schema properties are
3329 // strings, pointing to the type of property, corresponding to one
3330 // of the entries in the `LOADERS` object.
3331 if (typeof value === "string") {
3332 value = this.initKey(key, value);
3339 * Returns a Namespace object for the given namespace name. If a
3340 * namespace object with this name does not already exist, it is
3341 * created. If the name contains any '.' characters, namespaces are
3342 * recursively created, for each dot-separated component.
3344 * @param {string} name
3345 * The name of the sub-namespace to retrieve.
3346 * @param {boolean} [create = true]
3347 * If true, create any intermediate namespaces which don't
3350 * @returns {Namespace}
3352 getNamespace(name, create = true) {
3355 let idx = name.indexOf(".");
3357 subName = name.slice(idx + 1);
3358 name = name.slice(0, idx);
3361 let ns = super.get(name);
3366 ns = new Namespace(this.root, name, this.path);
3371 return ns.getNamespace(subName);
3376 getOwnNamespace(name) {
3377 return this.getNamespace(name);
3382 return super.has(key);
3387 * A namespace which combines the children of an arbitrary number of
3390 class Namespaces extends Namespace {
3391 constructor(root, name, path, namespaces) {
3392 super(root, name, path);
3394 this.namespaces = namespaces;
3397 injectInto(obj, context) {
3398 for (let ns of this.namespaces) {
3399 ns.injectInto(obj, context);
3405 * A root schema which combines the contents of an arbitrary number of base
3408 class SchemaRoots extends Namespaces {
3409 constructor(root, bases) {
3410 bases = bases.map(base => base.rootSchema || base);
3412 super(null, "", [], bases);
3416 this._namespaces = new Map();
3419 _getNamespace(name, create) {
3421 for (let root of this.bases) {
3422 let ns = root.getNamespace(name, create);
3428 if (results.length == 1) {
3432 if (results.length) {
3433 return new Namespaces(this.root, name, name.split("."), results);
3438 getNamespace(name, create) {
3439 let ns = this._namespaces.get(name);
3441 ns = this._getNamespace(name, create);
3443 this._namespaces.set(name, ns);
3449 *getNamespaces(name) {
3450 for (let root of this.bases) {
3451 yield* root.getNamespaces(name);
3457 * A root schema namespace containing schema data which is isolated from data in
3458 * other schema roots. May extend a base namespace, in which case schemas in
3459 * this root may refer to types in a base, but not vice versa.
3461 * @param {SchemaRoot|Array<SchemaRoot>|null} base
3462 * A base schema root (or roots) from which to derive, or null.
3463 * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON
3464 * A map of schema URLs and corresponding JSON blobs from which to
3465 * populate this root namespace.
3467 export class SchemaRoot extends Namespace {
3468 constructor(base, schemaJSON) {
3469 super(null, "", []);
3471 if (Array.isArray(base)) {
3472 base = new SchemaRoots(this, base);
3477 this.schemaJSON = schemaJSON;
3480 *getNamespaces(path) {
3481 let name = path.join(".");
3483 let ns = this.getNamespace(name, false);
3489 yield* this.base.getNamespaces(name);
3494 * Returns the sub-namespace with the given name. If the given namespace
3495 * doesn't already exist, attempts to find it in the base SchemaRoot before
3496 * creating a new empty namespace.
3498 * @param {string} name
3499 * The namespace to retrieve.
3500 * @param {boolean} [create = true]
3501 * If true, an empty namespace should be created if one does not
3503 * @returns {Namespace|null}
3505 getNamespace(name, create = true) {
3506 let ns = super.getNamespace(name, false);
3511 ns = this.base && this.base.getNamespace(name, false);
3515 return create && super.getNamespace(name, create);
3519 * Like getNamespace, but does not take the base SchemaRoot into account.
3521 * @param {string} name
3522 * The namespace to retrieve.
3523 * @returns {Namespace}
3525 getOwnNamespace(name) {
3526 return super.getNamespace(name);
3529 parseSchema(schema, path, extraProperties = []) {
3530 let allowedProperties = DEBUG && new Set(extraProperties);
3532 if ("choices" in schema) {
3533 return ChoiceType.parseSchema(this, schema, path, allowedProperties);
3534 } else if ("$ref" in schema) {
3535 return RefType.parseSchema(this, schema, path, allowedProperties);
3538 let type = TYPES[schema.type];
3541 allowedProperties.add("type");
3543 if (!("type" in schema)) {
3544 throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
3548 throw new Error(`Unexpected type ${schema.type}`);
3552 return type.parseSchema(this, schema, path, allowedProperties);
3556 for (let [key, schema] of this.schemaJSON.entries()) {
3558 if (typeof schema.deserialize === "function") {
3559 schema = schema.deserialize(globalThis, isParentProcess);
3561 // If we're in the parent process, we need to keep the
3562 // StructuredCloneHolder blob around in order to send to future child
3563 // processes. If we're in a child, we have no further use for it, so
3564 // just store the deserialized schema data in its place.
3565 if (!isParentProcess) {
3566 this.schemaJSON.set(key, schema);
3570 this.loadSchema(schema);
3578 for (let namespace of json) {
3579 this.getOwnNamespace(namespace.namespace).addSchema(namespace);
3584 * Checks whether a given object has the necessary permissions to
3585 * expose the given namespace.
3587 * @param {string} namespace
3588 * The top-level namespace to check permissions for.
3589 * @param {object} wrapperFuncs
3590 * Wrapper functions for the given context.
3591 * @param {Function} wrapperFuncs.hasPermission
3592 * A function which, when given a string argument, returns true
3593 * if the context has the given permission.
3594 * @returns {boolean}
3595 * True if the context has permission for the given namespace.
3597 checkPermissions(namespace, wrapperFuncs) {
3598 let ns = this.getNamespace(namespace);
3599 if (ns && ns.permissions) {
3600 return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
3606 * Inject registered extension APIs into `dest`.
3608 * @param {object} dest The root namespace for the APIs.
3609 * This object is usually exposed to extensions as "chrome" or "browser".
3610 * @param {object} wrapperFuncs An implementation of the InjectionContext
3611 * interface, which runs the actual functionality of the generated API.
3613 inject(dest, wrapperFuncs) {
3614 let context = new InjectionContext(wrapperFuncs, this);
3616 this.injectInto(dest, context);
3619 injectInto(dest, context) {
3620 // For schema graphs where multiple schema roots have the same base, don't
3621 // inject it more than once.
3623 if (!context.injectedRoots.has(this)) {
3624 context.injectedRoots.add(this);
3626 this.base.injectInto(dest, context);
3628 super.injectInto(dest, context);
3633 * Normalize `obj` according to the loaded schema for `typeName`.
3635 * @param {object} obj The object to normalize against the schema.
3636 * @param {string} typeName The name in the format namespace.propertyname
3637 * @param {object} context An implementation of Context. Any validation errors
3638 * are reported to the given context.
3639 * @returns {object} The normalized object.
3641 normalize(obj, typeName, context) {
3642 let [namespaceName, prop] = typeName.split(".");
3643 let ns = this.getNamespace(namespaceName);
3644 let type = ns.get(prop);
3646 let result = type.normalize(obj, new Context(context));
3648 return { error: forceString(result.error) };
3654 export var Schemas = {
3657 REVOKE: Symbol("@@revoke"),
3659 // Maps a schema URL to the JSON contained in that schema file. This
3660 // is useful for sending the JSON across processes.
3661 schemaJSON: new Map(),
3663 // A map of schema JSON which should be available in all content processes.
3664 contentSchemaJSON: new Map(),
3666 // A map of schema JSON which should only be available to extension processes.
3667 privilegedSchemaJSON: new Map(),
3671 // A weakmap for the validation Context class instances given an extension
3672 // context (keyed by the extensin context instance).
3673 // This is used instead of the InjectionContext for webIDL API validation
3674 // and normalization (see Schemas.checkParameters).
3675 paramsValidationContexts: new DefaultWeakMap(
3676 extContext => new Context(extContext)
3680 if (!this.initialized) {
3683 if (!this._rootSchema) {
3684 this._rootSchema = new SchemaRoot(null, this.schemaJSON);
3685 this._rootSchema.parseSchemas();
3687 return this._rootSchema;
3690 getNamespace(name) {
3691 return this.rootSchema.getNamespace(name);
3695 if (this.initialized) {
3698 this.initialized = true;
3700 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
3701 let addSchemas = schemas => {
3702 for (let [key, value] of schemas.entries()) {
3703 this.schemaJSON.set(key, value);
3707 if (WebExtensionPolicy.isExtensionProcess || DEBUG) {
3708 addSchemas(Services.cpmm.sharedData.get(KEY_PRIVILEGED_SCHEMAS));
3711 let schemas = Services.cpmm.sharedData.get(KEY_CONTENT_SCHEMAS);
3713 addSchemas(schemas);
3718 _loadCachedSchemasPromise: null,
3719 loadCachedSchemas() {
3720 if (!this._loadCachedSchemasPromise) {
3721 this._loadCachedSchemasPromise = lazy.StartupCache.schemas
3728 return this._loadCachedSchemasPromise;
3731 addSchema(url, schema, content = false) {
3732 this.schemaJSON.set(url, schema);
3735 this.contentSchemaJSON.set(url, schema);
3737 this.privilegedSchemaJSON.set(url, schema);
3740 if (this._rootSchema) {
3741 throw new Error("Schema loaded after root schema populated");
3745 updateSharedSchemas() {
3746 let { sharedData } = Services.ppmm;
3748 sharedData.set(KEY_CONTENT_SCHEMAS, this.contentSchemaJSON);
3749 sharedData.set(KEY_PRIVILEGED_SCHEMAS, this.privilegedSchemaJSON);
3753 return readJSONAndBlobbify(url);
3756 processSchema(json) {
3757 return blobbify(json);
3760 async load(url, content = false) {
3761 if (!isParentProcess) {
3765 const startTime = Cu.now();
3766 let schemaCache = await this.loadCachedSchemas();
3767 const fromCache = schemaCache.has(url);
3770 schemaCache.get(url) ||
3771 (await lazy.StartupCache.schemas.get(url, readJSONAndBlobbify));
3773 if (!this.schemaJSON.has(url)) {
3774 this.addSchema(url, blob, content);
3777 ChromeUtils.addProfilerMarker(
3780 `load ${url}, from cache: ${fromCache}`
3785 * Checks whether a given object has the necessary permissions to
3786 * expose the given namespace.
3788 * @param {string} namespace
3789 * The top-level namespace to check permissions for.
3790 * @param {object} wrapperFuncs
3791 * Wrapper functions for the given context.
3792 * @param {Function} wrapperFuncs.hasPermission
3793 * A function which, when given a string argument, returns true
3794 * if the context has the given permission.
3795 * @returns {boolean}
3796 * True if the context has permission for the given namespace.
3798 checkPermissions(namespace, wrapperFuncs) {
3799 return this.rootSchema.checkPermissions(namespace, wrapperFuncs);
3803 * Returns a sorted array of permission names for the given permission types.
3805 * @param {Array} types An array of permission types, defaults to all permissions.
3806 * @returns {Array} sorted array of permission names
3811 "OptionalPermission",
3812 "PermissionNoPrompt",
3813 "OptionalPermissionNoPrompt",
3814 "PermissionPrivileged",
3817 const ns = this.getNamespace("manifest");
3819 for (let typeName of types) {
3820 for (let choice of ns
3822 .choices.filter(choice => choice.enumeration)) {
3823 names = names.concat(choice.enumeration);
3826 return names.sort();
3832 * Inject registered extension APIs into `dest`.
3834 * @param {object} dest The root namespace for the APIs.
3835 * This object is usually exposed to extensions as "chrome" or "browser".
3836 * @param {object} wrapperFuncs An implementation of the InjectionContext
3837 * interface, which runs the actual functionality of the generated API.
3839 inject(dest, wrapperFuncs) {
3840 this.rootSchema.inject(dest, wrapperFuncs);
3844 * Normalize `obj` according to the loaded schema for `typeName`.
3846 * @param {object} obj The object to normalize against the schema.
3847 * @param {string} typeName The name in the format namespace.propertyname
3848 * @param {object} context An implementation of Context. Any validation errors
3849 * are reported to the given context.
3850 * @returns {object} The normalized object.
3852 normalize(obj, typeName, context) {
3853 return this.rootSchema.normalize(obj, typeName, context);
3857 * Validate and normalize the arguments for an API request originated
3858 * from the webIDL API bindings.
3860 * This provides for calls originating through WebIDL the parameters
3861 * validation and normalization guarantees that the ext-APINAMESPACE.js
3862 * scripts expects (what InjectionContext does for the regular bindings).
3864 * @param {object} extContext
3865 * @param {mozIExtensionAPIRequest } apiRequest
3867 * @returns {Array<any>} Normalized arguments array.
3869 checkWebIDLRequestParameters(extContext, apiRequest) {
3870 const getSchemaForProperty = (schemaObj, propName, schemaPath) => {
3871 if (schemaObj instanceof Namespace) {
3872 return schemaObj?.get(propName);
3873 } else if (schemaObj instanceof SubModuleProperty) {
3874 for (const fun of schemaObj.targetType.functions) {
3875 if (fun.name === propName) {
3880 for (const fun of schemaObj.targetType.events) {
3881 if (fun.name === propName) {
3885 } else if (schemaObj instanceof Event) {
3889 const schemaPathType = schemaObj?.constructor.name;
3891 `API Schema for "${propName}" not found in ${schemaPath} (${schemaPath} type is ${schemaPathType})`
3894 const { requestType, apiNamespace, apiName } = apiRequest;
3896 let [ns, ...rest] = (
3897 ["addListener", "removeListener"].includes(requestType)
3898 ? `${apiNamespace}.${apiName}.${requestType}`
3899 : `${apiNamespace}.${apiName}`
3901 let apiSchema = this.getNamespace(ns);
3903 // Keep track of the current schema path, populated while navigating the nested API schema
3904 // data and then used to include the full path to the API schema that is hitting unexpected
3905 // errors due to schema data not found or an unexpected schema type.
3906 let schemaPath = [ns];
3908 while (rest.length) {
3909 // Nested property as namespace (e.g. used for proxy.settings requests).
3911 throw new Error(`API Schema not found for ${schemaPath.join(".")}`);
3914 let [propName, ...newRest] = rest;
3917 apiSchema = getSchemaForProperty(
3920 schemaPath.join(".")
3922 schemaPath.push(propName);
3926 throw new Error(`API Schema not found for ${schemaPath.join(".")}`);
3929 if (!apiSchema.checkParameters) {
3931 `Unexpected API Schema type for ${schemaPath.join(
3933 )} (${schemaPath.join(".")} type is ${apiSchema.constructor.name})`
3937 return apiSchema.checkParameters(
3939 this.paramsValidationContexts.get(extContext)