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/. */
10 const { AppConstants } = ChromeUtils.import(
11 "resource://gre/modules/AppConstants.jsm"
13 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14 const { XPCOMUtils } = ChromeUtils.import(
15 "resource://gre/modules/XPCOMUtils.jsm"
18 XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
20 const { ExtensionUtils } = ChromeUtils.import(
21 "resource://gre/modules/ExtensionUtils.jsm"
23 var { DefaultMap, DefaultWeakMap } = ExtensionUtils;
25 ChromeUtils.defineModuleGetter(
28 "resource://gre/modules/ExtensionParent.jsm"
30 ChromeUtils.defineModuleGetter(
33 "resource://gre/modules/NetUtil.jsm"
35 ChromeUtils.defineModuleGetter(
38 "resource://gre/modules/ShortcutUtils.jsm"
40 XPCOMUtils.defineLazyServiceGetter(
42 "contentPolicyService",
43 "@mozilla.org/addons/content-policy;1",
44 "nsIAddonContentPolicy"
47 XPCOMUtils.defineLazyGetter(
50 () => ExtensionParent.StartupCache
53 XPCOMUtils.defineLazyPreferenceGetter(
55 "treatWarningsAsErrors",
56 "extensions.webextensions.warnings-as-errors",
60 var EXPORTED_SYMBOLS = ["SchemaRoot", "Schemas"];
62 const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content";
63 const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged";
65 const MIN_MANIFEST_VERSION = 2;
66 const MAX_MANIFEST_VERSION = 3;
68 const { DEBUG } = AppConstants;
70 const isParentProcess =
71 Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
73 function readJSON(url) {
74 return new Promise((resolve, reject) => {
76 { uri: url, loadUsingSystemPrincipal: true },
77 (inputStream, status) => {
78 if (!Components.isSuccessCode(status)) {
79 // Convert status code to a string
80 let e = Components.Exception("", status);
81 reject(new Error(`Error while loading '${url}' (${e.name})`));
85 let text = NetUtil.readInputStreamToString(
87 inputStream.available()
90 // Chrome JSON files include a license comment that we need to
91 // strip off for this to be valid JSON. As a hack, we just
92 // look for the first '[' character, which signals the start
93 // of the JSON content.
94 let index = text.indexOf("[");
95 text = text.slice(index);
97 resolve(JSON.parse(text));
106 function stripDescriptions(json, stripThis = true) {
107 if (Array.isArray(json)) {
108 for (let i = 0; i < json.length; i++) {
109 if (typeof json[i] === "object" && json[i] !== null) {
110 json[i] = stripDescriptions(json[i]);
118 // Objects are handled much more efficiently, both in terms of memory and
119 // CPU, if they have the same shape as other objects that serve the same
120 // purpose. So, normalize the order of properties to increase the chances
121 // that the majority of schema objects wind up in large shape groups.
122 for (let key of Object.keys(json).sort()) {
123 if (stripThis && key === "description" && typeof json[key] === "string") {
127 if (typeof json[key] === "object" && json[key] !== null) {
128 result[key] = stripDescriptions(json[key], key !== "properties");
130 result[key] = json[key];
137 function blobbify(json) {
138 // We don't actually use descriptions at runtime, and they make up about a
139 // third of the size of our structured clone data, so strip them before
141 json = stripDescriptions(json);
143 return new StructuredCloneHolder(json);
146 async function readJSONAndBlobbify(url) {
147 let json = await readJSON(url);
149 return blobbify(json);
153 * Defines a lazy getter for the given property on the given object. Any
154 * security wrappers are waived on the object before the property is
155 * defined, and the getter and setter methods are wrapped for the target
158 * The given getter function is guaranteed to be called only once, even
159 * if the target scope retrieves the wrapped getter from the property
160 * descriptor and calls it directly.
162 * @param {object} object
163 * The object on which to define the getter.
164 * @param {string|Symbol} prop
165 * The property name for which to define the getter.
166 * @param {function} getter
167 * The function to call in order to generate the final property
170 function exportLazyGetter(object, prop, getter) {
171 object = ChromeUtils.waiveXrays(object);
173 let redefine = value => {
174 if (value === undefined) {
177 Object.defineProperty(object, prop, {
190 Object.defineProperty(object, prop, {
194 get: Cu.exportFunction(function() {
195 return redefine(getter.call(this));
198 set: Cu.exportFunction(value => {
205 * Defines a lazily-instantiated property descriptor on the given
206 * object. Any security wrappers are waived on the object before the
207 * property is defined.
209 * The given getter function is guaranteed to be called only once, even
210 * if the target scope retrieves the wrapped getter from the property
211 * descriptor and calls it directly.
213 * @param {object} object
214 * The object on which to define the getter.
215 * @param {string|Symbol} prop
216 * The property name for which to define the getter.
217 * @param {function} getter
218 * The function to call in order to generate the final property
219 * descriptor object. This will be called, and the property
220 * descriptor installed on the object, the first time the
221 * property is written or read. The function may return
222 * undefined, which will cause the property to be deleted.
224 function exportLazyProperty(object, prop, getter) {
225 object = ChromeUtils.waiveXrays(object);
227 let redefine = obj => {
228 let desc = getter.call(obj);
238 if (!desc.set && !desc.get) {
239 defaults.writable = true;
242 Object.defineProperty(object, prop, Object.assign(defaults, desc));
246 Object.defineProperty(object, prop, {
250 get: Cu.exportFunction(function() {
255 set: Cu.exportFunction(function(value) {
257 object[prop] = value;
262 const POSTPROCESSORS = {
263 convertImageDataToURL(imageData, context) {
264 let document = context.cloneScope.document;
265 let canvas = document.createElementNS(
266 "http://www.w3.org/1999/xhtml",
269 canvas.width = imageData.width;
270 canvas.height = imageData.height;
271 canvas.getContext("2d").putImageData(imageData, 0, 0);
273 return canvas.toDataURL("image/png");
275 webRequestBlockingPermissionRequired(string, context) {
276 if (string === "blocking" && !context.hasPermission("webRequestBlocking")) {
277 throw new context.cloneScope.Error(
278 "Using webRequest.addListener with the " +
279 "blocking option requires the 'webRequestBlocking' permission."
285 requireBackgroundServiceWorkerEnabled(value, context) {
286 if (WebExtensionPolicy.backgroundServiceWorkerEnabled) {
290 // Add an error to the manifest validations and throw the
292 const msg = "background.service_worker is currently disabled";
293 context.logError(context.makeError(msg));
294 throw new Error(msg);
297 manifestVersionCheck(value, context) {
301 Services.prefs.getBoolPref("extensions.manifestV3.enabled", false))
305 const msg = `Unsupported manifest version: ${value}`;
306 context.logError(context.makeError(msg));
307 throw new Error(msg);
311 // Parses a regular expression, with support for the Python extended
312 // syntax that allows setting flags by including the string (?im)
313 function parsePattern(pattern) {
315 let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
317 [, flags, pattern] = match;
319 return new RegExp(pattern, flags);
322 function getValueBaseType(value) {
323 let type = typeof value;
326 if (value === null) {
329 if (Array.isArray(value)) {
335 if (value % 1 === 0) {
342 // Methods of Context that are used by Schemas.normalize. These methods can be
343 // overridden at the construction of Context.
344 const CONTEXT_FOR_VALIDATION = ["checkLoadURL", "hasPermission", "logError"];
346 // Methods of Context that are used by Schemas.inject.
347 // Callers of Schemas.inject should implement all of these methods.
348 const CONTEXT_FOR_INJECTION = [
349 ...CONTEXT_FOR_VALIDATION,
351 "isPermissionRevokable",
355 // If the message is a function, call it and return the result.
356 // Otherwise, assume it's a string.
357 function forceString(msg) {
358 if (typeof msg === "function") {
365 * A context for schema validation and error reporting. This class is only used
366 * internally within Schemas.
370 * @param {object} params Provides the implementation of this class.
371 * @param {Array<string>} overridableMethods
373 constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) {
374 this.params = params;
376 if (typeof params.manifestVersion !== "number") {
378 `Unexpected params.manifestVersion value: ${params.manifestVersion}`
383 this.preprocessors = {
384 localize(value, context) {
388 this.postprocessors = POSTPROCESSORS;
389 this.isChromeCompat = false;
391 this.currentChoices = new Set();
392 this.choicePathIndex = 0;
394 for (let method of overridableMethods) {
395 if (method in params) {
396 this[method] = params[method].bind(params);
400 let props = ["preprocessors", "isChromeCompat", "manifestVersion"];
401 for (let prop of props) {
402 if (prop in params) {
403 if (prop in this && typeof this[prop] == "object") {
404 Object.assign(this[prop], params[prop]);
406 this[prop] = params[prop];
413 let path = this.path.slice(this.choicePathIndex);
414 return path.join(".");
418 return this.params.cloneScope || undefined;
422 return this.params.url;
427 this.params.principal ||
428 Services.scriptSecurityManager.createNullPrincipal({})
433 * Checks whether `url` may be loaded by the extension in this context.
435 * @param {string} url The URL that the extension wished to load.
436 * @returns {boolean} Whether the context may load `url`.
439 let ssm = Services.scriptSecurityManager;
441 ssm.checkLoadURIWithPrincipal(
443 Services.io.newURI(url),
444 ssm.DISALLOW_INHERIT_PRINCIPAL
453 * Checks whether this context has the given permission.
455 * @param {string} permission
456 * The name of the permission to check.
458 * @returns {boolean} True if the context has the given permission.
460 hasPermission(permission) {
465 * Checks whether the given permission can be dynamically revoked or
468 * @param {string} permission
469 * The name of the permission to check.
471 * @returns {boolean} True if the given permission is revokable.
473 isPermissionRevokable(permission) {
478 * Returns an error result object with the given message, for return
479 * by Type normalization functions.
481 * If the context has a `currentTarget` value, this is prepended to
482 * the message to indicate the location of the error.
484 * @param {string|function} errorMessage
485 * The error message which will be displayed when this is the
486 * only possible matching schema. If a function is passed, it
487 * will be evaluated when the error string is first needed, and
488 * must return a string.
489 * @param {string|function} choicesMessage
490 * The message describing the valid what constitutes a valid
491 * value for this schema, which will be displayed when multiple
492 * schema choices are available and none match.
494 * A caller may pass `null` to prevent a choice from being
495 * added, but this should *only* be done from code processing a
497 * @param {boolean} [warning = false]
498 * If true, make message prefixed `Warning`. If false, make message
502 error(errorMessage, choicesMessage = undefined, warning = false) {
503 if (choicesMessage !== null) {
504 let { choicePath } = this;
506 choicesMessage = `.${choicePath} must ${choicesMessage}`;
509 this.currentChoices.add(choicesMessage);
512 if (this.currentTarget) {
513 let { currentTarget } = this;
517 warning ? "Warning" : "Error"
518 } processing ${currentTarget}: ${forceString(errorMessage)}`,
521 return { error: errorMessage };
525 * Creates an `Error` object belonging to the current unprivileged
526 * scope. If there is no unprivileged scope associated with this
527 * context, the message is returned as a string.
529 * If the context has a `currentTarget` value, this is prepended to
530 * the message, in the same way as for the `error` method.
532 * @param {string} message
533 * @param {object} [options]
534 * @param {boolean} [options.warning = false]
537 makeError(message, { warning = false } = {}) {
538 let error = forceString(this.error(message, null, warning).error);
539 if (this.cloneScope) {
540 return new this.cloneScope.Error(error);
546 * Logs the given error to the console. May be overridden to enable
549 * @param {Error|string} error
552 if (this.cloneScope) {
554 // Error objects logged using Cu.reportError are not associated
555 // to the related innerWindowID. This results in a leaked docshell
556 // since consoleService cannot release the error object when the
557 // extension global is destroyed.
558 typeof error == "string" ? error : String(error),
559 // Report the error with the appropriate stack trace when the
560 // is related to an actual extension global (instead of being
561 // related to a manifest validation).
562 this.principal && ChromeUtils.getCallerLocation(this.principal)
565 Cu.reportError(error);
570 * Returns the name of the value currently being normalized. For a
571 * nested object, this is usually approximately equivalent to the
572 * JavaScript property accessor for that property. Given:
574 * { foo: { bar: [{ baz: x }] } }
576 * When processing the value for `x`, the currentTarget is
579 get currentTarget() {
580 return this.path.join(".");
584 * Executes the given callback, and returns an array of choice strings
585 * passed to {@see #error} during its execution.
587 * @param {function} callback
589 * An object with a `result` property containing the return
590 * value of the callback, and a `choice` property containing
591 * an array of choices.
593 withChoices(callback) {
594 let { currentChoices, choicePathIndex } = this;
596 let choices = new Set();
597 this.currentChoices = choices;
598 this.choicePathIndex = this.path.length;
601 let result = callback();
603 return { result, choices };
605 this.currentChoices = currentChoices;
606 this.choicePathIndex = choicePathIndex;
608 if (choices.size == 1) {
609 for (let choice of choices) {
610 currentChoices.add(choice);
612 } else if (choices.size) {
613 this.error(null, () => {
614 let array = Array.from(choices, forceString);
615 let n = array.length - 1;
616 array[n] = `or ${array[n]}`;
618 return `must either [${array.join(", ")}]`;
625 * Appends the given component to the `currentTarget` path to indicate
626 * that it is being processed, calls the given callback function, and
627 * then restores the original path.
629 * This is used to identify the path of the property being processed
630 * when reporting type errors.
632 * @param {string} component
633 * @param {function} callback
636 withPath(component, callback) {
637 this.path.push(component);
645 matchManifestVersion(entry) {
646 let { manifestVersion } = this;
648 manifestVersion >= entry.min_manifest_version &&
649 manifestVersion <= entry.max_manifest_version
655 * Represents a schema entry to be injected into an object. Handles the
656 * injection, revocation, and permissions of said entry.
658 * @param {InjectionContext} context
659 * The injection context for the entry.
660 * @param {Entry} entry
661 * The entry to inject.
662 * @param {object} parentObject
663 * The object into which to inject this entry.
664 * @param {string} name
665 * The property name at which to inject this entry.
666 * @param {Array<string>} path
667 * The full path from the root entry to this entry.
668 * @param {Entry} parentEntry
669 * The parent entry for the injected entry.
671 class InjectionEntry {
672 constructor(context, entry, parentObj, name, path, parentEntry) {
673 this.context = context;
675 this.parentObj = parentObj;
678 this.parentEntry = parentEntry;
680 this.injected = null;
681 this.lazyInjected = null;
685 * @property {Array<string>} allowedContexts
686 * The list of allowed contexts into which the entry may be
689 get allowedContexts() {
690 let { allowedContexts } = this.entry;
691 if (allowedContexts.length) {
692 return allowedContexts;
694 return this.parentEntry.defaultContexts;
698 * @property {boolean} isRevokable
699 * Returns true if this entry may be dynamically injected or
700 * revoked based on its permissions.
704 this.entry.permissions &&
705 this.entry.permissions.some(perm =>
706 this.context.isPermissionRevokable(perm)
712 * @property {boolean} hasPermission
713 * Returns true if the injection context currently has the
714 * appropriate permissions to access this entry.
716 get hasPermission() {
718 !this.entry.permissions ||
719 this.entry.permissions.some(perm => this.context.hasPermission(perm))
724 * @property {boolean} shouldInject
725 * Returns true if this entry should be injected in the given
726 * context, without respect to permissions.
730 this.context.matchManifestVersion(this.entry) &&
731 this.context.shouldInject(
740 * Revokes this entry, removing its property from its parent object,
741 * and invalidating its wrappers.
744 if (this.lazyInjected) {
745 this.lazyInjected = false;
746 } else if (this.injected) {
747 if (this.injected.revoke) {
748 this.injected.revoke();
752 let unwrapped = ChromeUtils.waiveXrays(this.parentObj);
753 delete unwrapped[this.name];
758 let { value } = this.injected.descriptor;
760 this.context.revokeChildren(value);
763 this.injected = null;
768 * Returns a property descriptor object for this entry, if it should
769 * be injected, or undefined if it should not.
772 * A property descriptor object, or undefined if the property
776 this.lazyInjected = false;
779 let path = [...this.path, this.name];
781 `Attempting to re-inject already injected entry: ${path.join(".")}`
785 if (!this.shouldInject) {
789 if (this.isRevokable) {
790 this.context.pendingEntries.add(this);
793 if (!this.hasPermission) {
797 this.injected = this.entry.getDescriptor(this.path, this.context);
798 if (!this.injected) {
802 return this.injected.descriptor;
806 * Injects a lazy property descriptor into the parent object which
807 * checks permissions and eligibility for injection the first time it
811 if (this.lazyInjected || this.injected) {
812 let path = [...this.path, this.name];
814 `Attempting to re-lazy-inject already injected entry: ${path.join(".")}`
818 this.lazyInjected = true;
819 exportLazyProperty(this.parentObj, this.name, () => {
820 if (this.lazyInjected) {
821 return this.getDescriptor();
827 * Injects or revokes this entry if its current state does not match
828 * the context's current permissions.
830 permissionsChanged() {
839 if (!this.injected && !this.lazyInjected) {
845 if (this.injected && !this.hasPermission) {
852 * Holds methods that run the actual implementation of the extension APIs. These
853 * methods are only called if the extension API invocation matches the signature
854 * as defined in the schema. Otherwise an error is reported to the context.
856 class InjectionContext extends Context {
857 constructor(params, schemaRoot) {
858 super(params, CONTEXT_FOR_INJECTION);
860 this.schemaRoot = schemaRoot;
862 this.pendingEntries = new Set();
863 this.children = new DefaultWeakMap(() => new Map());
865 this.injectedRoots = new Set();
867 if (params.setPermissionsChangedCallback) {
868 params.setPermissionsChangedCallback(this.permissionsChanged.bind(this));
873 * Check whether the API should be injected.
876 * @param {string} namespace The namespace of the API. This may contain dots,
877 * e.g. in the case of "devtools.inspectedWindow".
878 * @param {string} [name] The name of the property in the namespace.
879 * `null` if we are checking whether the namespace should be injected.
880 * @param {Array<string>} allowedContexts A list of additional contexts in which
881 * this API should be available. May include any of:
882 * "main" - The main chrome browser process.
883 * "addon" - An addon process.
884 * "content" - A content process.
885 * @returns {boolean} Whether the API should be injected.
887 shouldInject(namespace, name, allowedContexts) {
888 throw new Error("Not implemented");
892 * Generate the implementation for `namespace`.`name`.
895 * @param {string} namespace The full path to the namespace of the API, minus
896 * the name of the method or property. E.g. "storage.local".
897 * @param {string} name The name of the method, property or event.
898 * @returns {SchemaAPIInterface} The implementation of the API.
900 getImplementation(namespace, name) {
901 throw new Error("Not implemented");
905 * Updates all injection entries which may need to be updated after a
906 * permission change, revoking or re-injecting them as necessary.
908 permissionsChanged() {
909 for (let entry of this.pendingEntries) {
911 entry.permissionsChanged();
919 * Recursively revokes all child injection entries of the given
922 * @param {object} object
923 * The object for which to invoke children.
925 revokeChildren(object) {
926 if (!this.children.has(object)) {
930 let children = this.children.get(object);
931 for (let [name, entry] of children.entries()) {
937 children.delete(name);
939 // When we revoke children for an object, we consider that object
940 // dead. If the entry is ever reified again, a new object is
941 // created, with new child entries.
942 this.pendingEntries.delete(entry);
944 this.children.delete(object);
947 _getInjectionEntry(entry, dest, name, path, parentEntry) {
948 let injection = new InjectionEntry(
957 this.children.get(dest).set(name, injection);
963 * Returns the property descriptor for the given entry.
965 * @param {Entry} entry
966 * The entry instance to return a descriptor for.
967 * @param {object} dest
968 * The object into which this entry is being injected.
969 * @param {string} name
970 * The property name on the destination object where the entry
972 * @param {Array<string>} path
973 * The full path from the root injection object to this entry.
974 * @param {Entry} parentEntry
975 * The parent entry for this entry.
978 * A property descriptor object, or null if the entry should
981 getDescriptor(entry, dest, name, path, parentEntry) {
982 let injection = this._getInjectionEntry(
990 return injection.getDescriptor();
994 * Lazily injects the given entry into the given object.
996 * @param {Entry} entry
997 * The entry instance to lazily inject.
998 * @param {object} dest
999 * The object into which to inject this entry.
1000 * @param {string} name
1001 * The property name at which to inject the entry.
1002 * @param {Array<string>} path
1003 * The full path from the root injection object to this entry.
1004 * @param {Entry} parentEntry
1005 * The parent entry for this entry.
1007 injectInto(entry, dest, name, path, parentEntry) {
1008 let injection = this._getInjectionEntry(
1016 injection.lazyInject();
1021 * The methods in this singleton represent the "format" specifier for
1022 * JSON Schema string types.
1024 * Each method either returns a normalized version of the original
1025 * value, or throws an error if the value is not valid for the given
1029 hostname(string, context) {
1033 valid = new URL(`http://${string}`).host === string;
1039 throw new Error(`Invalid hostname ${string}`);
1045 url(string, context) {
1046 let url = new URL(string).href;
1048 if (!context.checkLoadURL(url)) {
1049 throw new Error(`Access denied for URL ${url}`);
1054 origin(string, context) {
1057 url = new URL(string);
1059 throw new Error(`Invalid origin: ${string}`);
1061 if (!/^https?:/.test(url.protocol)) {
1062 throw new Error(`Invalid origin must be http or https for URL ${string}`);
1064 // url.origin is punycode so a direct check against string wont work.
1065 // url.href appends a slash even if not in the original string, we we
1066 // additionally check that string does not end in slash.
1067 if (string.endsWith("/") || url.href != new URL(url.origin).href) {
1069 `Invalid origin for URL ${string}, replace with origin ${url.origin}`
1072 if (!context.checkLoadURL(url.origin)) {
1073 throw new Error(`Access denied for URL ${url}`);
1078 relativeUrl(string, context) {
1080 // If there's no context URL, return relative URLs unresolved, and
1081 // skip security checks for them.
1089 let url = new URL(string, context.url).href;
1091 if (!context.checkLoadURL(url)) {
1092 throw new Error(`Access denied for URL ${url}`);
1097 strictRelativeUrl(string, context) {
1098 void FORMATS.unresolvedRelativeUrl(string, context);
1099 return FORMATS.relativeUrl(string, context);
1102 unresolvedRelativeUrl(string, context) {
1103 if (!string.startsWith("//")) {
1111 throw new SyntaxError(
1112 `String ${JSON.stringify(string)} must be a relative URL`
1116 homepageUrl(string, context) {
1117 // Pipes are used for separating homepages, but we only allow extensions to
1118 // set a single homepage. Encoding any pipes makes it one URL.
1119 return FORMATS.relativeUrl(
1120 string.replace(new RegExp("\\|", "g"), "%7C"),
1125 imageDataOrStrictRelativeUrl(string, context) {
1126 // Do not accept a string which resolves as an absolute URL, or any
1127 // protocol-relative URL, except PNG or JPG data URLs
1129 !string.startsWith("data:image/png;base64,") &&
1130 !string.startsWith("data:image/jpeg;base64,")
1133 return FORMATS.strictRelativeUrl(string, context);
1135 throw new SyntaxError(
1136 `String ${JSON.stringify(
1138 )} must be a relative or PNG or JPG data:image URL`
1145 contentSecurityPolicy(string, context) {
1146 // Manifest V3 extension_pages allows localhost. When sandbox is
1147 // implemented, or any other V3 or later directive, the flags
1148 // logic will need to be updated.
1150 context.manifestVersion < 3
1151 ? Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY
1152 : Ci.nsIAddonContentPolicy.CSP_ALLOW_LOCALHOST;
1153 let error = contentPolicyService.validateAddonCSP(string, flags);
1154 if (error != null) {
1155 // The CSP validation error is not reported as part of the "choices" error message,
1156 // we log the CSP validation error explicitly here to make it easier for the addon developers
1157 // to see and fix the extension CSP.
1158 context.logError(`Error processing ${context.currentTarget}: ${error}`);
1164 date(string, context) {
1165 // A valid ISO 8601 timestamp.
1166 const PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/;
1167 if (!PATTERN.test(string)) {
1168 throw new Error(`Invalid date string ${string}`);
1170 // Our pattern just checks the format, we could still have invalid
1171 // values (e.g., month=99 or month=02 and day=31). Let the Date
1172 // constructor do the dirty work of validating.
1173 if (isNaN(new Date(string))) {
1174 throw new Error(`Invalid date string ${string}`);
1179 manifestShortcutKey(string, context) {
1180 if (ShortcutUtils.validate(string) == ShortcutUtils.IS_VALID) {
1184 `Value "${string}" must consist of ` +
1185 `either a combination of one or two modifiers, including ` +
1186 `a mandatory primary modifier and a key, separated by '+', ` +
1187 `or a media key. For details see: ` +
1188 `https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`;
1189 throw new Error(errorMessage);
1192 manifestShortcutKeyOrEmpty(string, context) {
1193 return string === "" ? "" : FORMATS.manifestShortcutKey(string, context);
1197 // Schema files contain namespaces, and each namespace contains types,
1198 // properties, functions, and events. An Entry is a base class for
1199 // types, properties, functions, and events.
1201 constructor(schema = {}) {
1203 * If set to any value which evaluates as true, this entry is
1204 * deprecated, and any access to it will result in a deprecation
1205 * warning being logged to the browser console.
1207 * If the value is a string, it will be appended to the deprecation
1208 * message. If it contains the substring "${value}", it will be
1209 * replaced with a string representation of the value being
1212 * If the value is any other truthy value, a generic deprecation
1213 * message will be emitted.
1215 this.deprecated = false;
1216 if ("deprecated" in schema) {
1217 this.deprecated = schema.deprecated;
1221 * @property {string} [preprocessor]
1222 * If set to a string value, and a preprocessor of the same is
1223 * defined in the validation context, it will be applied to this
1224 * value prior to any normalization.
1226 this.preprocessor = schema.preprocess || null;
1229 * @property {string} [postprocessor]
1230 * If set to a string value, and a postprocessor of the same is
1231 * defined in the validation context, it will be applied to this
1232 * value after any normalization.
1234 this.postprocessor = schema.postprocess || null;
1237 * @property {Array<string>} allowedContexts A list of allowed contexts
1238 * to consider before generating the API.
1239 * These are not parsed by the schema, but passed to `shouldInject`.
1241 this.allowedContexts = schema.allowedContexts || [];
1243 this.min_manifest_version =
1244 schema.min_manifest_version ?? MIN_MANIFEST_VERSION;
1245 this.max_manifest_version =
1246 schema.max_manifest_version ?? MAX_MANIFEST_VERSION;
1250 * Preprocess the given value with the preprocessor declared in
1254 * @param {Context} context
1257 preprocess(value, context) {
1258 if (this.preprocessor) {
1259 return context.preprocessors[this.preprocessor](value, context);
1265 * Postprocess the given result with the postprocessor declared in
1268 * @param {object} result
1269 * @param {Context} context
1272 postprocess(result, context) {
1273 if (result.error || !this.postprocessor) {
1277 let value = context.postprocessors[this.postprocessor](
1285 * Logs a deprecation warning for this entry, based on the value of
1286 * its `deprecated` property.
1288 * @param {Context} context
1289 * @param {value} [value]
1291 logDeprecation(context, value = null) {
1292 let message = "This property is deprecated";
1293 if (typeof this.deprecated == "string") {
1294 message = this.deprecated;
1295 if (message.includes("${value}")) {
1297 value = JSON.stringify(value);
1299 value = String(value);
1301 message = message.replace(/\$\{value\}/g, () => value);
1305 this.logWarning(context, message);
1309 * @param {Context} context
1310 * @param {string} warningMessage
1312 logWarning(context, warningMessage) {
1313 let error = context.makeError(warningMessage, { warning: true });
1314 context.logError(error);
1316 if (treatWarningsAsErrors) {
1317 // This pref is false by default, and true by default in tests to
1318 // discourage the use of deprecated APIs in our unit tests.
1319 // If a warning is an expected part of a test, temporarily set the pref
1320 // to false, e.g. with the ExtensionTestUtils.failOnSchemaWarnings helper.
1321 Services.console.logStringMessage(
1322 "Treating warning as error because the preference " +
1323 "extensions.webextensions.warnings-as-errors is set to true"
1325 if (typeof error === "string") {
1326 error = new Error(error);
1333 * Checks whether the entry is deprecated and, if so, logs a
1334 * deprecation message.
1336 * @param {Context} context
1337 * @param {value} [value]
1339 checkDeprecated(context, value = null) {
1340 if (this.deprecated) {
1341 this.logDeprecation(context, value);
1346 * Returns an object containing property descriptor for use when
1347 * injecting this entry into an API object.
1349 * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
1350 * @param {InjectionContext} context
1352 * @returns {object?}
1353 * An object containing a `descriptor` property, specifying the
1354 * entry's property descriptor, and an optional `revoke`
1355 * method, to be called when the entry is being revoked.
1357 getDescriptor(path, context) {
1362 // Corresponds either to a type declared in the "types" section of the
1363 // schema or else to any type object used throughout the schema.
1364 class Type extends Entry {
1366 * @property {Array<string>} EXTRA_PROPERTIES
1367 * An array of extra properties which may be present for
1368 * schemas of this type.
1370 static get EXTRA_PROPERTIES() {
1377 "min_manifest_version",
1378 "max_manifest_version",
1383 * Parses the given schema object and returns an instance of this
1384 * class which corresponds to its properties.
1386 * @param {SchemaRoot} root
1387 * The root schema for this type.
1388 * @param {object} schema
1389 * A JSON schema object which corresponds to a definition of
1391 * @param {Array<string>} path
1392 * The path to this schema object from the root schema,
1393 * corresponding to the property names and array indices
1394 * traversed during parsing in order to arrive at this schema
1396 * @param {Array<string>} [extraProperties]
1397 * An array of extra property names which are valid for this
1398 * schema in the current context.
1400 * An instance of this type which corresponds to the given
1404 static parseSchema(root, schema, path, extraProperties = []) {
1405 this.checkSchemaProperties(schema, path, extraProperties);
1407 return new this(schema);
1411 * Checks that all of the properties present in the given schema
1412 * object are valid properties for this type, and throws if invalid.
1414 * @param {object} schema
1415 * A JSON schema object.
1416 * @param {Array<string>} path
1417 * The path to this schema object from the root schema,
1418 * corresponding to the property names and array indices
1419 * traversed during parsing in order to arrive at this schema
1421 * @param {Array<string>} [extra]
1422 * An array of extra property names which are valid for this
1423 * schema in the current context.
1425 * An error describing the first invalid property found in the
1428 static checkSchemaProperties(schema, path, extra = []) {
1430 let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]);
1432 for (let prop of Object.keys(schema)) {
1433 if (!allowedSet.has(prop)) {
1435 `Internal error: Namespace ${path.join(".")} has ` +
1436 `invalid type property "${prop}" ` +
1437 `in type "${schema.id || JSON.stringify(schema)}"`
1444 // Takes a value, checks that it has the correct type, and returns a
1445 // "normalized" version of the value. The normalized version will
1446 // include "nulls" in place of omitted optional properties. The
1447 // result of this function is either {error: "Some type error"} or
1448 // {value: <normalized-value>}.
1449 normalize(value, context) {
1450 return context.error("invalid type");
1453 // Unlike normalize, this function does a shallow check to see if
1454 // |baseType| (one of the possible getValueBaseType results) is
1455 // valid for this type. It returns true or false. It's used to fill
1456 // in optional arguments to functions before actually type checking
1458 checkBaseType(baseType) {
1462 // Helper method that simply relies on checkBaseType to implement
1463 // normalize. Subclasses can choose to use it or not.
1464 normalizeBase(type, value, context) {
1465 if (this.checkBaseType(getValueBaseType(value))) {
1466 this.checkDeprecated(context, value);
1467 return { value: this.preprocess(value, context) };
1471 if ("aeiou".includes(type[0])) {
1472 choice = `be an ${type} value`;
1474 choice = `be a ${type} value`;
1477 return context.error(
1478 () => `Expected ${type} instead of ${JSON.stringify(value)}`,
1484 // Type that allows any value.
1485 class AnyType extends Type {
1486 normalize(value, context) {
1487 this.checkDeprecated(context, value);
1488 return this.postprocess({ value }, context);
1491 checkBaseType(baseType) {
1496 // An untagged union type.
1497 class ChoiceType extends Type {
1498 static get EXTRA_PROPERTIES() {
1499 return ["choices", ...super.EXTRA_PROPERTIES];
1502 static parseSchema(root, schema, path, extraProperties = []) {
1503 this.checkSchemaProperties(schema, path, extraProperties);
1505 let choices = schema.choices.map(t => root.parseSchema(t, path));
1506 return new this(schema, choices);
1509 constructor(schema, choices) {
1511 this.choices = choices;
1515 this.choices.push(...type.choices);
1520 normalize(value, context) {
1521 this.checkDeprecated(context, value);
1524 let { choices, result } = context.withChoices(() => {
1525 for (let choice of this.choices) {
1526 // Ignore a possible choice if it is not supported by
1527 // the manifest version we are normalizing.
1528 if (!context.matchManifestVersion(choice)) {
1532 let r = choice.normalize(value, context);
1544 if (choices.size <= 1) {
1548 choices = Array.from(choices, forceString);
1549 let n = choices.length - 1;
1550 choices[n] = `or ${choices[n]}`;
1553 if (typeof value === "object") {
1554 message = () => `Value must either: ${choices.join(", ")}`;
1557 `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`;
1560 return context.error(message, null);
1563 checkBaseType(baseType) {
1564 return this.choices.some(t => t.checkBaseType(baseType));
1567 getDescriptor(path, context) {
1568 // In StringType.getDescriptor, unlike any other Type, a descriptor is returned if
1569 // it is an enumeration. Since we need versioned choices in some cases, here we
1570 // build a list of valid enumerations that will work for a given manifest version.
1572 !this.choices.length ||
1573 !this.choices.every(t => t.checkBaseType("string") && t.enumeration)
1578 let obj = Cu.createObjectIn(context.cloneScope);
1579 let descriptor = { value: obj };
1580 for (let choice of this.choices) {
1581 // Ignore a possible choice if it is not supported by
1582 // the manifest version we are normalizing.
1583 if (!context.matchManifestVersion(choice)) {
1586 let d = choice.getDescriptor(path, context);
1588 Object.assign(obj, d.descriptor.value);
1592 return { descriptor };
1596 // This is a reference to another type--essentially a typedef.
1597 class RefType extends Type {
1598 static get EXTRA_PROPERTIES() {
1599 return ["$ref", ...super.EXTRA_PROPERTIES];
1602 static parseSchema(root, schema, path, extraProperties = []) {
1603 this.checkSchemaProperties(schema, path, extraProperties);
1605 let ref = schema.$ref;
1606 let ns = path.join(".");
1607 if (ref.includes(".")) {
1608 [, ns, ref] = /^(.*)\.(.*?)$/.exec(ref);
1610 return new this(root, schema, ns, ref);
1613 // For a reference to a type named T declared in namespace NS,
1614 // namespaceName will be NS and reference will be T.
1615 constructor(root, schema, namespaceName, reference) {
1618 this.namespaceName = namespaceName;
1619 this.reference = reference;
1623 let ns = this.root.getNamespace(this.namespaceName);
1624 let type = ns.get(this.reference);
1626 throw new Error(`Internal error: Type ${this.reference} not found`);
1631 normalize(value, context) {
1632 this.checkDeprecated(context, value);
1633 return this.targetType.normalize(value, context);
1636 checkBaseType(baseType) {
1637 return this.targetType.checkBaseType(baseType);
1641 class StringType extends Type {
1642 static get EXTRA_PROPERTIES() {
1649 ...super.EXTRA_PROPERTIES,
1653 static parseSchema(root, schema, path, extraProperties = []) {
1654 this.checkSchemaProperties(schema, path, extraProperties);
1656 let enumeration = schema.enum || null;
1658 // The "enum" property is either a list of strings that are
1659 // valid values or else a list of {name, description} objects,
1660 // where the .name values are the valid values.
1661 enumeration = enumeration.map(e => {
1662 if (typeof e == "object") {
1670 if (schema.pattern) {
1672 pattern = parsePattern(schema.pattern);
1675 `Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}`
1681 if (schema.format) {
1682 if (!(schema.format in FORMATS)) {
1684 `Internal error: Invalid string format ${schema.format}`
1687 format = FORMATS[schema.format];
1691 schema.id || undefined,
1693 schema.minLength || 0,
1694 schema.maxLength || Infinity,
1711 this.enumeration = enumeration;
1712 this.minLength = minLength;
1713 this.maxLength = maxLength;
1714 this.pattern = pattern;
1715 this.format = format;
1718 normalize(value, context) {
1719 let r = this.normalizeBase("string", value, context);
1725 if (this.enumeration) {
1726 if (this.enumeration.includes(value)) {
1727 return this.postprocess({ value }, context);
1730 let choices = this.enumeration.map(JSON.stringify).join(", ");
1732 return context.error(
1733 () => `Invalid enumeration value ${JSON.stringify(value)}`,
1734 `be one of [${choices}]`
1738 if (value.length < this.minLength) {
1739 return context.error(
1741 `String ${JSON.stringify(value)} is too short (must be ${
1744 `be longer than ${this.minLength}`
1747 if (value.length > this.maxLength) {
1748 return context.error(
1750 `String ${JSON.stringify(value)} is too long (must be ${
1753 `be shorter than ${this.maxLength}`
1757 if (this.pattern && !this.pattern.test(value)) {
1758 return context.error(
1759 () => `String ${JSON.stringify(value)} must match ${this.pattern}`,
1760 `match the pattern ${this.pattern.toSource()}`
1766 r.value = this.format(r.value, context);
1768 return context.error(
1770 `match the format "${this.format.name}"`
1778 checkBaseType(baseType) {
1779 return baseType == "string";
1782 getDescriptor(path, context) {
1783 if (this.enumeration) {
1784 let obj = Cu.createObjectIn(context.cloneScope);
1786 for (let e of this.enumeration) {
1787 obj[e.toUpperCase()] = e;
1791 descriptor: { value: obj },
1797 class NullType extends Type {
1798 normalize(value, context) {
1799 return this.normalizeBase("null", value, context);
1802 checkBaseType(baseType) {
1803 return baseType == "null";
1811 class ObjectType extends Type {
1812 static get EXTRA_PROPERTIES() {
1815 "patternProperties",
1817 ...super.EXTRA_PROPERTIES,
1821 static parseSchema(root, schema, path, extraProperties = []) {
1822 if ("functions" in schema) {
1823 return SubModuleType.parseSchema(root, schema, path, extraProperties);
1826 if (DEBUG && !("$extend" in schema)) {
1827 // Only allow extending "properties" and "patternProperties".
1829 "additionalProperties",
1834 this.checkSchemaProperties(schema, path, extraProperties);
1836 let imported = null;
1837 if ("$import" in schema) {
1838 let importPath = schema.$import;
1839 let idx = importPath.indexOf(".");
1841 imported = [path[0], importPath];
1843 imported = [importPath.slice(0, idx), importPath.slice(idx + 1)];
1847 let parseProperty = (schema, extraProps = []) => {
1849 type: root.parseSchema(
1860 optional: schema.optional || false,
1861 unsupported: schema.unsupported || false,
1862 onError: schema.onError || null,
1863 default: schema.default === undefined ? null : schema.default,
1867 // Parse explicit "properties" object.
1868 let properties = Object.create(null);
1869 for (let propName of Object.keys(schema.properties || {})) {
1870 properties[propName] = parseProperty(schema.properties[propName], [
1875 // Parse regexp properties from "patternProperties" object.
1876 let patternProperties = [];
1877 for (let propName of Object.keys(schema.patternProperties || {})) {
1880 pattern = parsePattern(propName);
1883 `Internal error: Invalid property pattern ${JSON.stringify(propName)}`
1887 patternProperties.push({
1889 type: parseProperty(schema.patternProperties[propName]),
1893 // Parse "additionalProperties" schema.
1894 let additionalProperties = null;
1895 if (schema.additionalProperties) {
1896 let type = schema.additionalProperties;
1897 if (type === true) {
1898 type = { type: "any" };
1901 additionalProperties = root.parseSchema(type, path);
1907 additionalProperties,
1909 schema.isInstanceOf || null,
1917 additionalProperties,
1923 this.properties = properties;
1924 this.additionalProperties = additionalProperties;
1925 this.patternProperties = patternProperties;
1926 this.isInstanceOf = isInstanceOf;
1929 let [ns, path] = imported;
1930 ns = Schemas.getNamespace(ns);
1931 let importedType = ns.get(path);
1932 if (!importedType) {
1933 throw new Error(`Internal error: imported type ${path} not found`);
1936 if (DEBUG && !(importedType instanceof ObjectType)) {
1938 `Internal error: cannot import non-object type ${path}`
1942 this.properties = Object.assign(
1944 importedType.properties,
1947 this.patternProperties = [
1948 ...importedType.patternProperties,
1949 ...this.patternProperties,
1951 this.additionalProperties =
1952 importedType.additionalProperties || this.additionalProperties;
1957 for (let key of Object.keys(type.properties)) {
1958 if (key in this.properties) {
1960 `InternalError: Attempt to extend an object with conflicting property "${key}"`
1963 this.properties[key] = type.properties[key];
1966 this.patternProperties.push(...type.patternProperties);
1971 checkBaseType(baseType) {
1972 return baseType == "object";
1976 * Extracts the enumerable properties of the given object, including
1977 * function properties which would normally be omitted by X-ray
1980 * @param {object} value
1981 * @param {Context} context
1982 * The current parse context.
1984 * An object with an `error` or `value` property.
1986 extractProperties(value, context) {
1987 // |value| should be a JS Xray wrapping an object in the
1988 // extension compartment. This works well except when we need to
1989 // access callable properties on |value| since JS Xrays don't
1990 // support those. To work around the problem, we verify that
1991 // |value| is a plain JS object (i.e., not anything scary like a
1992 // Proxy). Then we copy the properties out of it into a normal
1993 // object using a waiver wrapper.
1995 let klass = ChromeUtils.getClassName(value, true);
1996 if (klass != "Object") {
1997 throw context.error(
1998 `Expected a plain JavaScript object, got a ${klass}`,
1999 `be a plain JavaScript object`
2003 return ChromeUtils.shallowClone(value);
2006 checkProperty(context, prop, propType, result, properties, remainingProps) {
2007 let { type, optional, unsupported, onError } = propType;
2010 if (!context.matchManifestVersion(type)) {
2011 if (prop in properties) {
2012 error = context.error(
2013 `Property "${prop}" is unsupported in Manifest Version ${context.manifestVersion}`,
2014 `not contain an unsupported "${prop}" property`
2016 if (context.manifestVersion === 2) {
2017 // Existing MV2 extensions might have some of the new MV3 properties.
2018 // Since we've ignored them till now, we should just warn and bail.
2019 this.logWarning(context, forceString(error.error));
2023 } else if (unsupported) {
2024 if (prop in properties) {
2025 error = context.error(
2026 `Property "${prop}" is unsupported by Firefox`,
2027 `not contain an unsupported "${prop}" property`
2030 } else if (prop in properties) {
2033 (properties[prop] === null || properties[prop] === undefined)
2035 result[prop] = propType.default;
2037 let r = context.withPath(prop, () =>
2038 type.normalize(properties[prop], context)
2043 result[prop] = r.value;
2044 properties[prop] = r.value;
2047 remainingProps.delete(prop);
2048 } else if (!optional) {
2049 error = context.error(
2050 `Property "${prop}" is required`,
2051 `contain the required "${prop}" property`
2053 } else if (optional !== "omit-key-if-missing") {
2054 result[prop] = propType.default;
2058 if (onError == "warn") {
2059 this.logWarning(context, forceString(error.error));
2060 } else if (onError != "ignore") {
2064 result[prop] = propType.default;
2068 normalize(value, context) {
2070 let v = this.normalizeBase("object", value, context);
2076 if (this.isInstanceOf) {
2079 Object.keys(this.properties).length ||
2080 this.patternProperties.length ||
2081 !(this.additionalProperties instanceof AnyType)
2084 "InternalError: isInstanceOf can only be used " +
2085 "with objects that are otherwise unrestricted"
2091 ChromeUtils.getClassName(value) !== this.isInstanceOf &&
2092 (this.isInstanceOf !== "Element" || value.nodeType !== 1)
2094 return context.error(
2095 `Object must be an instance of ${this.isInstanceOf}`,
2096 `be an instance of ${this.isInstanceOf}`
2100 // This is kind of a hack, but we can't normalize things that
2101 // aren't JSON, so we just return them.
2102 return this.postprocess({ value }, context);
2105 let properties = this.extractProperties(value, context);
2106 let remainingProps = new Set(Object.keys(properties));
2109 for (let prop of Object.keys(this.properties)) {
2113 this.properties[prop],
2120 for (let prop of Object.keys(properties)) {
2121 for (let { pattern, type } of this.patternProperties) {
2122 if (pattern.test(prop)) {
2135 if (this.additionalProperties) {
2136 for (let prop of remainingProps) {
2137 let r = context.withPath(prop, () =>
2138 this.additionalProperties.normalize(properties[prop], context)
2143 result[prop] = r.value;
2145 } else if (remainingProps.size == 1) {
2146 return context.error(
2147 `Unexpected property "${[...remainingProps]}"`,
2148 `not contain an unexpected "${[...remainingProps]}" property`
2150 } else if (remainingProps.size) {
2151 let props = [...remainingProps].sort().join(", ");
2152 return context.error(
2153 `Unexpected properties: ${props}`,
2154 `not contain the unexpected properties [${props}]`
2158 return this.postprocess({ value: result }, context);
2168 // This type is just a placeholder to be referred to by
2169 // SubModuleProperty. No value is ever expected to have this type.
2170 SubModuleType = class SubModuleType extends Type {
2171 static get EXTRA_PROPERTIES() {
2172 return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
2175 static parseSchema(root, schema, path, extraProperties = []) {
2176 this.checkSchemaProperties(schema, path, extraProperties);
2178 // The path we pass in here is only used for error messages.
2179 path = [...path, schema.id];
2180 let functions = schema.functions
2181 .filter(fun => !fun.unsupported)
2182 .map(fun => FunctionEntry.parseSchema(root, fun, path));
2186 if (schema.events) {
2187 events = schema.events
2188 .filter(event => !event.unsupported)
2189 .map(event => Event.parseSchema(root, event, path));
2192 return new this(schema, functions, events);
2195 constructor(schema, functions, events) {
2196 // schema contains properties such as min/max_manifest_version needed
2197 // in the base class so that the Context class can version compare
2198 // any entries against the manifest version.
2200 this.functions = functions;
2201 this.events = events;
2205 class NumberType extends Type {
2206 normalize(value, context) {
2207 let r = this.normalizeBase("number", value, context);
2212 if (isNaN(r.value) || !Number.isFinite(r.value)) {
2213 return context.error(
2214 "NaN and infinity are not valid",
2215 "be a finite number"
2222 checkBaseType(baseType) {
2223 return baseType == "number" || baseType == "integer";
2227 class IntegerType extends Type {
2228 static get EXTRA_PROPERTIES() {
2229 return ["minimum", "maximum", ...super.EXTRA_PROPERTIES];
2232 static parseSchema(root, schema, path, extraProperties = []) {
2233 this.checkSchemaProperties(schema, path, extraProperties);
2235 let { minimum = -Infinity, maximum = Infinity } = schema;
2236 return new this(schema, minimum, maximum);
2239 constructor(schema, minimum, maximum) {
2241 this.minimum = minimum;
2242 this.maximum = maximum;
2245 normalize(value, context) {
2246 let r = this.normalizeBase("integer", value, context);
2252 // Ensure it's between -2**31 and 2**31-1
2253 if (!Number.isSafeInteger(value)) {
2254 return context.error(
2255 "Integer is out of range",
2256 "be a valid 32 bit signed integer"
2260 if (value < this.minimum) {
2261 return context.error(
2262 `Integer ${value} is too small (must be at least ${this.minimum})`,
2263 `be at least ${this.minimum}`
2266 if (value > this.maximum) {
2267 return context.error(
2268 `Integer ${value} is too big (must be at most ${this.maximum})`,
2269 `be no greater than ${this.maximum}`
2273 return this.postprocess(r, context);
2276 checkBaseType(baseType) {
2277 return baseType == "integer";
2281 class BooleanType extends Type {
2282 static get EXTRA_PROPERTIES() {
2283 return ["enum", ...super.EXTRA_PROPERTIES];
2286 static parseSchema(root, schema, path, extraProperties = []) {
2287 this.checkSchemaProperties(schema, path, extraProperties);
2288 let enumeration = schema.enum || null;
2289 return new this(schema, enumeration);
2292 constructor(schema, enumeration) {
2294 this.enumeration = enumeration;
2297 normalize(value, context) {
2298 if (!this.checkBaseType(getValueBaseType(value))) {
2299 return context.error(
2300 () => `Expected boolean instead of ${JSON.stringify(value)}`,
2304 value = this.preprocess(value, context);
2305 if (this.enumeration && !this.enumeration.includes(value)) {
2306 return context.error(
2307 () => `Invalid value ${JSON.stringify(value)}`,
2308 `be ${this.enumeration}`
2311 this.checkDeprecated(context, value);
2315 checkBaseType(baseType) {
2316 return baseType == "boolean";
2320 class ArrayType extends Type {
2321 static get EXTRA_PROPERTIES() {
2322 return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
2325 static parseSchema(root, schema, path, extraProperties = []) {
2326 this.checkSchemaProperties(schema, path, extraProperties);
2328 let items = root.parseSchema(schema.items, path, ["onError"]);
2333 schema.minItems || 0,
2334 schema.maxItems || Infinity
2338 constructor(schema, itemType, minItems, maxItems) {
2340 this.itemType = itemType;
2341 this.minItems = minItems;
2342 this.maxItems = maxItems;
2343 this.onError = schema.items.onError || null;
2346 normalize(value, context) {
2347 let v = this.normalizeBase("array", value, context);
2354 for (let [i, element] of value.entries()) {
2355 element = context.withPath(String(i), () =>
2356 this.itemType.normalize(element, context)
2358 if (element.error) {
2359 if (this.onError == "warn") {
2360 this.logWarning(context, forceString(element.error));
2361 } else if (this.onError != "ignore") {
2366 result.push(element.value);
2369 if (result.length < this.minItems) {
2370 return context.error(
2371 `Array requires at least ${this.minItems} items; you have ${result.length}`,
2372 `have at least ${this.minItems} items`
2376 if (result.length > this.maxItems) {
2377 return context.error(
2378 `Array requires at most ${this.maxItems} items; you have ${result.length}`,
2379 `have at most ${this.maxItems} items`
2383 return this.postprocess({ value: result }, context);
2386 checkBaseType(baseType) {
2387 return baseType == "array";
2391 class FunctionType extends Type {
2392 static get EXTRA_PROPERTIES() {
2398 ...super.EXTRA_PROPERTIES,
2402 static parseSchema(root, schema, path, extraProperties = []) {
2403 this.checkSchemaProperties(schema, path, extraProperties);
2405 let isAsync = !!schema.async;
2406 let isExpectingCallback = typeof schema.async === "string";
2407 let parameters = null;
2408 if ("parameters" in schema) {
2410 for (let param of schema.parameters) {
2411 // Callbacks default to optional for now, because of promise
2413 let isCallback = isAsync && param.name == schema.async;
2415 isExpectingCallback = false;
2419 type: root.parseSchema(param, path, ["name", "optional", "default"]),
2421 optional: param.optional == null ? isCallback : param.optional,
2422 default: param.default == undefined ? null : param.default,
2426 let hasAsyncCallback = false;
2430 parameters.length &&
2431 parameters[parameters.length - 1].name == schema.async;
2435 if (isExpectingCallback) {
2437 `Internal error: Expected a callback parameter ` +
2438 `with name ${schema.async}`
2442 if (isAsync && schema.returns) {
2444 "Internal error: Async functions must not have return values."
2449 schema.allowAmbiguousOptionalArguments &&
2453 "Internal error: Async functions with ambiguous " +
2454 "arguments must declare the callback as the last parameter"
2464 !!schema.requireUserInput
2468 constructor(schema, parameters, isAsync, hasAsyncCallback, requireUserInput) {
2470 this.parameters = parameters;
2471 this.isAsync = isAsync;
2472 this.hasAsyncCallback = hasAsyncCallback;
2473 this.requireUserInput = requireUserInput;
2476 normalize(value, context) {
2477 return this.normalizeBase("function", value, context);
2480 checkBaseType(baseType) {
2481 return baseType == "function";
2485 // Represents a "property" defined in a schema namespace with a
2486 // particular value. Essentially this is a constant.
2487 class ValueProperty extends Entry {
2488 constructor(schema, name, value) {
2494 getDescriptor(path, context) {
2495 // Prevent injection if not a supported version.
2496 if (!context.matchManifestVersion(this)) {
2501 descriptor: { value: this.value },
2506 // Represents a "property" defined in a schema namespace that is not a
2508 class TypeProperty extends Entry {
2509 constructor(schema, path, name, type, writable, permissions) {
2514 this.writable = writable;
2515 this.permissions = permissions;
2518 throwError(context, msg) {
2519 throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2522 getDescriptor(path, context) {
2523 if (this.unsupported || !context.matchManifestVersion(this)) {
2527 let apiImpl = context.getImplementation(path.join("."), this.name);
2529 let getStub = () => {
2530 this.checkDeprecated(context);
2531 return apiImpl.getProperty();
2535 get: Cu.exportFunction(getStub, context.cloneScope),
2538 if (this.writable) {
2539 let setStub = value => {
2540 let normalized = this.type.normalize(value, context);
2541 if (normalized.error) {
2542 this.throwError(context, forceString(normalized.error));
2545 apiImpl.setProperty(normalized.value);
2548 descriptor.set = Cu.exportFunction(setStub, context.cloneScope);
2561 class SubModuleProperty extends Entry {
2562 // A SubModuleProperty represents a tree of objects and properties
2563 // to expose to an extension. Currently we support only a limited
2564 // form of sub-module properties, where "$ref" points to a
2565 // SubModuleType containing a list of functions and "properties" is
2566 // a list of additional simple properties.
2568 // name: Name of the property stuff is being added to.
2569 // namespaceName: Namespace in which the property lives.
2570 // reference: Name of the type defining the functions to add to the property.
2571 // properties: Additional properties to add to the module (unsupported).
2572 constructor(root, schema, path, name, reference, properties, permissions) {
2577 this.namespaceName = path.join(".");
2578 this.reference = reference;
2579 this.properties = properties;
2580 this.permissions = permissions;
2583 getDescriptor(path, context) {
2584 let obj = Cu.createObjectIn(context.cloneScope);
2586 let ns = this.root.getNamespace(this.namespaceName);
2587 let type = ns.get(this.reference);
2588 if (!type && this.reference.includes(".")) {
2589 let [namespaceName, ref] = this.reference.split(".");
2590 ns = this.root.getNamespace(namespaceName);
2593 // Prevent injection if not a supported version.
2594 if (!context.matchManifestVersion(type)) {
2599 if (!type || !(type instanceof SubModuleType)) {
2601 `Internal error: ${this.namespaceName}.${this.reference} ` +
2602 `is not a sub-module`
2606 let subpath = [...path, this.name];
2608 let functions = type.functions;
2609 for (let fun of functions) {
2610 context.injectInto(fun, obj, fun.name, subpath, ns);
2613 let events = type.events;
2614 for (let event of events) {
2615 context.injectInto(event, obj, event.name, subpath, ns);
2618 // TODO: Inject this.properties.
2621 descriptor: { value: obj },
2623 let unwrapped = ChromeUtils.waiveXrays(obj);
2624 for (let fun of functions) {
2626 delete unwrapped[fun.name];
2636 // This class is a base class for FunctionEntrys and Events. It takes
2637 // care of validating parameter lists (i.e., handling of optional
2638 // parameters and parameter type checking).
2639 class CallEntry extends Entry {
2640 constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) {
2644 this.parameters = parameters;
2645 this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments;
2648 throwError(context, msg) {
2649 throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2652 checkParameters(args, context) {
2655 // First we create a new array, fixedArgs, that is the same as
2656 // |args| but with default values in place of omitted optional parameters.
2657 let check = (parameterIndex, argIndex) => {
2658 if (parameterIndex == this.parameters.length) {
2659 if (argIndex == args.length) {
2665 let parameter = this.parameters[parameterIndex];
2666 if (parameter.optional) {
2668 fixedArgs[parameterIndex] = parameter.default;
2669 if (check(parameterIndex + 1, argIndex)) {
2674 if (argIndex == args.length) {
2678 let arg = args[argIndex];
2679 if (!parameter.type.checkBaseType(getValueBaseType(arg))) {
2680 // For Chrome compatibility, use the default value if null or undefined
2681 // is explicitly passed but is not a valid argument in this position.
2682 if (parameter.optional && (arg === null || arg === undefined)) {
2683 fixedArgs[parameterIndex] = Cu.cloneInto(parameter.default, global);
2688 fixedArgs[parameterIndex] = arg;
2691 return check(parameterIndex + 1, argIndex + 1);
2694 if (this.allowAmbiguousOptionalArguments) {
2695 // When this option is set, it's up to the implementation to
2697 // The last argument for asynchronous methods is either a function or null.
2698 // This is specifically done for runtime.sendMessage.
2699 if (this.hasAsyncCallback && typeof args[args.length - 1] != "function") {
2704 let success = check(0, 0);
2706 this.throwError(context, "Incorrect argument types");
2709 // Now we normalize (and fully type check) all non-omitted arguments.
2710 fixedArgs = fixedArgs.map((arg, parameterIndex) => {
2714 let parameter = this.parameters[parameterIndex];
2715 let r = parameter.type.normalize(arg, context);
2719 `Type error for parameter ${parameter.name} (${forceString(r.error)})`
2729 // Represents a "function" defined in a schema namespace.
2730 FunctionEntry = class FunctionEntry extends CallEntry {
2731 static parseSchema(root, schema, path) {
2732 // When not in DEBUG mode, we just need to know *if* this returns.
2733 let returns = !!schema.returns;
2734 if (DEBUG && "returns" in schema) {
2736 type: root.parseSchema(schema.returns, path, ["optional", "name"]),
2737 optional: schema.returns.optional || false,
2746 root.parseSchema(schema, path, [
2751 "allowAmbiguousOptionalArguments",
2752 "allowCrossOriginArguments",
2754 schema.unsupported || false,
2755 schema.allowAmbiguousOptionalArguments || false,
2756 schema.allowCrossOriginArguments || false,
2758 schema.permissions || null
2768 allowAmbiguousOptionalArguments,
2769 allowCrossOriginArguments,
2773 super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
2774 this.unsupported = unsupported;
2775 this.returns = returns;
2776 this.permissions = permissions;
2777 this.allowCrossOriginArguments = allowCrossOriginArguments;
2779 this.isAsync = type.isAsync;
2780 this.hasAsyncCallback = type.hasAsyncCallback;
2781 this.requireUserInput = type.requireUserInput;
2784 checkValue({ type, optional, name }, value, context) {
2785 if (optional && value == null) {
2789 type.reference === "ExtensionPanel" ||
2790 type.reference === "ExtensionSidebarPane" ||
2791 type.reference === "Port"
2793 // TODO: We currently treat objects with functions as SubModuleType,
2794 // which is just wrong, and a bigger yak. Skipping for now.
2797 const { error } = type.normalize(value, context);
2801 `Type error for ${name} value (${forceString(error)})`
2806 checkCallback(args, context) {
2807 const callback = this.parameters[this.parameters.length - 1];
2808 for (const [i, param] of callback.type.parameters.entries()) {
2809 this.checkValue(param, args[i], context);
2813 getDescriptor(path, context) {
2814 let apiImpl = context.getImplementation(path.join("."), this.name);
2818 stub = (...args) => {
2819 this.checkDeprecated(context);
2820 let actuals = this.checkParameters(args, context);
2821 let callback = null;
2822 if (this.hasAsyncCallback) {
2823 callback = actuals.pop();
2825 if (callback === null && context.isChromeCompat) {
2826 // We pass an empty stub function as a default callback for
2827 // the `chrome` API, so promise objects are not returned,
2828 // and lastError values are reported immediately.
2829 callback = () => {};
2831 if (DEBUG && this.hasAsyncCallback && callback) {
2832 let original = callback;
2833 callback = (...args) => {
2834 this.checkCallback(args, context);
2838 let result = apiImpl.callAsyncFunction(
2841 this.requireUserInput
2843 if (DEBUG && this.hasAsyncCallback && !callback) {
2844 return result.then(result => {
2845 this.checkCallback([result], context);
2851 } else if (!this.returns) {
2852 stub = (...args) => {
2853 this.checkDeprecated(context);
2854 let actuals = this.checkParameters(args, context);
2855 return apiImpl.callFunctionNoReturn(actuals);
2858 stub = (...args) => {
2859 this.checkDeprecated(context);
2860 let actuals = this.checkParameters(args, context);
2861 let result = apiImpl.callFunction(actuals);
2862 if (DEBUG && this.returns) {
2863 this.checkValue(this.returns, result, context);
2871 value: Cu.exportFunction(stub, context.cloneScope, {
2872 allowCrossOriginArguments: this.allowCrossOriginArguments,
2883 // Represents an "event" defined in a schema namespace.
2885 // TODO Bug 1369722: we should be able to remove the eslint-disable-line that follows
2886 // once Bug 1369722 has been fixed.
2887 // eslint-disable-next-line no-global-assign
2888 Event = class Event extends CallEntry {
2889 static parseSchema(root, event, path) {
2890 let extraParameters = Array.from(event.extraParameters || [], param => ({
2891 type: root.parseSchema(param, path, ["name", "optional", "default"]),
2893 optional: param.optional || false,
2894 default: param.default == undefined ? null : param.default,
2897 let extraProperties = [
2902 // We ignore these properties for now.
2911 root.parseSchema(event, path, extraProperties),
2913 event.unsupported || false,
2914 event.permissions || null
2927 super(schema, path, name, extraParameters);
2929 this.unsupported = unsupported;
2930 this.permissions = permissions;
2933 checkListener(listener, context) {
2934 let r = this.type.normalize(listener, context);
2936 this.throwError(context, "Invalid listener");
2941 getDescriptor(path, context) {
2942 let apiImpl = context.getImplementation(path.join("."), this.name);
2944 let addStub = (listener, ...args) => {
2945 listener = this.checkListener(listener, context);
2946 let actuals = this.checkParameters(args, context);
2947 apiImpl.addListener(listener, actuals);
2950 let removeStub = listener => {
2951 listener = this.checkListener(listener, context);
2952 apiImpl.removeListener(listener);
2955 let hasStub = listener => {
2956 listener = this.checkListener(listener, context);
2957 return apiImpl.hasListener(listener);
2960 let obj = Cu.createObjectIn(context.cloneScope);
2962 Cu.exportFunction(addStub, obj, { defineAs: "addListener" });
2963 Cu.exportFunction(removeStub, obj, { defineAs: "removeListener" });
2964 Cu.exportFunction(hasStub, obj, { defineAs: "hasListener" });
2967 descriptor: { value: obj },
2972 let unwrapped = ChromeUtils.waiveXrays(obj);
2973 delete unwrapped.addListener;
2974 delete unwrapped.removeListener;
2975 delete unwrapped.hasListener;
2981 const TYPES = Object.freeze(
2982 Object.assign(Object.create(null), {
2985 boolean: BooleanType,
2986 function: FunctionType,
2987 integer: IntegerType,
2996 events: "loadEvent",
2997 functions: "loadFunction",
2998 properties: "loadProperty",
3002 class Namespace extends Map {
3003 constructor(root, name, path) {
3008 this._lazySchemas = [];
3009 this.initialized = false;
3012 this.path = name ? [...path, name] : [...path];
3014 this.superNamespace = null;
3016 this.min_manifest_version = MIN_MANIFEST_VERSION;
3017 this.max_manifest_version = MAX_MANIFEST_VERSION;
3019 this.permissions = null;
3020 this.allowedContexts = [];
3021 this.defaultContexts = [];
3025 * Adds a JSON Schema object to the set of schemas that represent this
3028 * @param {object} schema
3029 * A JSON schema object which partially describes this
3033 this._lazySchemas.push(schema);
3039 "min_manifest_version",
3040 "max_manifest_version",
3043 this[prop] = schema[prop];
3047 if (schema.$import) {
3048 this.superNamespace = this.root.getNamespace(schema.$import);
3053 * Initializes the keys of this namespace based on the schema objects
3054 * added via previous `addSchema` calls.
3057 if (this.initialized) {
3061 if (this.superNamespace) {
3062 this._lazySchemas.unshift(...this.superNamespace._lazySchemas);
3065 for (let type of Object.keys(LOADERS)) {
3066 this[type] = new DefaultMap(() => []);
3069 for (let schema of this._lazySchemas) {
3070 for (let type of schema.types || []) {
3071 if (!type.unsupported) {
3072 this.types.get(type.$extend || type.id).push(type);
3076 for (let [name, prop] of Object.entries(schema.properties || {})) {
3077 if (!prop.unsupported) {
3078 this.properties.get(name).push(prop);
3082 for (let fun of schema.functions || []) {
3083 if (!fun.unsupported) {
3084 this.functions.get(fun.name).push(fun);
3088 for (let event of schema.events || []) {
3089 if (!event.unsupported) {
3090 this.events.get(event.name).push(event);
3095 // For each type of top-level property in the schema object, iterate
3096 // over all properties of that type, and create a temporary key for
3097 // each property pointing to its type. Those temporary properties
3098 // are later used to instantiate an Entry object based on the actual
3100 for (let type of Object.keys(LOADERS)) {
3101 for (let key of this[type].keys()) {
3102 this.set(key, type);
3106 this.initialized = true;
3109 for (let key of this.keys()) {
3116 * Initializes the value of a given key, by parsing the schema object
3117 * associated with it and replacing its temporary value with an `Entry`
3120 * @param {string} key
3121 * The name of the property to initialize.
3122 * @param {string} type
3123 * The type of property the key represents. Must have a
3124 * corresponding entry in the `LOADERS` object, pointing to the
3125 * initialization method for that type.
3129 initKey(key, type) {
3130 let loader = LOADERS[type];
3132 for (let schema of this[type].get(key)) {
3133 this.set(key, this[loader](key, schema));
3136 return this.get(key);
3139 loadType(name, type) {
3140 if ("$extend" in type) {
3141 return this.extendType(type);
3143 return this.root.parseSchema(type, this.path, ["id"]);
3147 let targetType = this.get(type.$extend);
3149 // Only allow extending object and choices types for now.
3150 if (targetType instanceof ObjectType) {
3151 type.type = "object";
3155 `Internal error: Attempt to extend a nonexistent type ${type.$extend}`
3157 } else if (!(targetType instanceof ChoiceType)) {
3159 `Internal error: Attempt to extend a non-extensible type ${type.$extend}`
3164 let parsed = this.root.parseSchema(type, this.path, ["$extend"]);
3166 if (DEBUG && parsed.constructor !== targetType.constructor) {
3167 throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
3170 targetType.extend(parsed);
3175 loadProperty(name, prop) {
3176 if ("$ref" in prop) {
3177 if (!prop.unsupported) {
3178 return new SubModuleProperty(
3184 prop.properties || {},
3185 prop.permissions || null
3188 } else if ("value" in prop) {
3189 return new ValueProperty(prop, name, prop.value);
3191 // We ignore the "optional" attribute on properties since we
3192 // don't inject anything here anyway.
3193 let type = this.root.parseSchema(
3196 ["optional", "permissions", "writable"]
3198 return new TypeProperty(
3203 prop.writable || false,
3204 prop.permissions || null
3209 loadFunction(name, fun) {
3210 return FunctionEntry.parseSchema(this.root, fun, this.path);
3213 loadEvent(name, event) {
3214 return Event.parseSchema(this.root, event, this.path);
3218 * Injects the properties of this namespace into the given object.
3220 * @param {object} dest
3221 * The object into which to inject the namespace properties.
3222 * @param {InjectionContext} context
3223 * The injection context with which to inject the properties.
3225 injectInto(dest, context) {
3226 for (let name of this.keys()) {
3227 // If the entry does not match the manifest version do not
3228 // inject the property. This prevents the item from being
3229 // enumerable in the namespace object. We cannot accomplish
3230 // this inside exportLazyProperty, it specifically injects
3231 // an enumerable object.
3232 let entry = this.get(name);
3233 if (!context.matchManifestVersion(entry)) {
3236 exportLazyProperty(dest, name, () => {
3237 let entry = this.get(name);
3239 return context.getDescriptor(entry, dest, name, this.path, this);
3244 getDescriptor(path, context) {
3245 let obj = Cu.createObjectIn(context.cloneScope);
3247 let ns = context.schemaRoot.getNamespace(this.path.join("."));
3248 ns.injectInto(obj, context);
3250 // Only inject the namespace object if it isn't empty.
3251 if (Object.keys(obj).length) {
3253 descriptor: { value: obj },
3260 return super.keys();
3264 for (let key of this.keys()) {
3265 yield [key, this.get(key)];
3271 let value = super.get(key);
3273 // The initial values of lazily-initialized schema properties are
3274 // strings, pointing to the type of property, corresponding to one
3275 // of the entries in the `LOADERS` object.
3276 if (typeof value === "string") {
3277 value = this.initKey(key, value);
3284 * Returns a Namespace object for the given namespace name. If a
3285 * namespace object with this name does not already exist, it is
3286 * created. If the name contains any '.' characters, namespaces are
3287 * recursively created, for each dot-separated component.
3289 * @param {string} name
3290 * The name of the sub-namespace to retrieve.
3291 * @param {boolean} [create = true]
3292 * If true, create any intermediate namespaces which don't
3295 * @returns {Namespace}
3297 getNamespace(name, create = true) {
3300 let idx = name.indexOf(".");
3302 subName = name.slice(idx + 1);
3303 name = name.slice(0, idx);
3306 let ns = super.get(name);
3311 ns = new Namespace(this.root, name, this.path);
3316 return ns.getNamespace(subName);
3321 getOwnNamespace(name) {
3322 return this.getNamespace(name);
3327 return super.has(key);
3332 * A namespace which combines the children of an arbitrary number of
3335 class Namespaces extends Namespace {
3336 constructor(root, name, path, namespaces) {
3337 super(root, name, path);
3339 this.namespaces = namespaces;
3342 injectInto(obj, context) {
3343 for (let ns of this.namespaces) {
3344 ns.injectInto(obj, context);
3350 * A root schema which combines the contents of an arbitrary number of base
3353 class SchemaRoots extends Namespaces {
3354 constructor(root, bases) {
3355 bases = bases.map(base => base.rootSchema || base);
3357 super(null, "", [], bases);
3361 this._namespaces = new Map();
3364 _getNamespace(name, create) {
3366 for (let root of this.bases) {
3367 let ns = root.getNamespace(name, create);
3373 if (results.length == 1) {
3377 if (results.length) {
3378 return new Namespaces(this.root, name, name.split("."), results);
3383 getNamespace(name, create) {
3384 let ns = this._namespaces.get(name);
3386 ns = this._getNamespace(name, create);
3388 this._namespaces.set(name, ns);
3394 *getNamespaces(name) {
3395 for (let root of this.bases) {
3396 yield* root.getNamespaces(name);
3402 * A root schema namespace containing schema data which is isolated from data in
3403 * other schema roots. May extend a base namespace, in which case schemas in
3404 * this root may refer to types in a base, but not vice versa.
3406 * @param {SchemaRoot|Array<SchemaRoot>|null} base
3407 * A base schema root (or roots) from which to derive, or null.
3408 * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON
3409 * A map of schema URLs and corresponding JSON blobs from which to
3410 * populate this root namespace.
3412 class SchemaRoot extends Namespace {
3413 constructor(base, schemaJSON) {
3414 super(null, "", []);
3416 if (Array.isArray(base)) {
3417 base = new SchemaRoots(this, base);
3422 this.schemaJSON = schemaJSON;
3425 *getNamespaces(path) {
3426 let name = path.join(".");
3428 let ns = this.getNamespace(name, false);
3434 yield* this.base.getNamespaces(name);
3439 * Returns the sub-namespace with the given name. If the given namespace
3440 * doesn't already exist, attempts to find it in the base SchemaRoot before
3441 * creating a new empty namespace.
3443 * @param {string} name
3444 * The namespace to retrieve.
3445 * @param {boolean} [create = true]
3446 * If true, an empty namespace should be created if one does not
3448 * @returns {Namespace|null}
3450 getNamespace(name, create = true) {
3451 let ns = super.getNamespace(name, false);
3456 ns = this.base && this.base.getNamespace(name, false);
3460 return create && super.getNamespace(name, create);
3464 * Like getNamespace, but does not take the base SchemaRoot into account.
3466 * @param {string} name
3467 * The namespace to retrieve.
3468 * @returns {Namespace}
3470 getOwnNamespace(name) {
3471 return super.getNamespace(name);
3474 parseSchema(schema, path, extraProperties = []) {
3475 let allowedProperties = DEBUG && new Set(extraProperties);
3477 if ("choices" in schema) {
3478 return ChoiceType.parseSchema(this, schema, path, allowedProperties);
3479 } else if ("$ref" in schema) {
3480 return RefType.parseSchema(this, schema, path, allowedProperties);
3483 let type = TYPES[schema.type];
3486 allowedProperties.add("type");
3488 if (!("type" in schema)) {
3489 throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
3493 throw new Error(`Unexpected type ${schema.type}`);
3497 return type.parseSchema(this, schema, path, allowedProperties);
3501 for (let [key, schema] of this.schemaJSON.entries()) {
3503 if (typeof schema.deserialize === "function") {
3504 schema = schema.deserialize(global, isParentProcess);
3506 // If we're in the parent process, we need to keep the
3507 // StructuredCloneHolder blob around in order to send to future child
3508 // processes. If we're in a child, we have no further use for it, so
3509 // just store the deserialized schema data in its place.
3510 if (!isParentProcess) {
3511 this.schemaJSON.set(key, schema);
3515 this.loadSchema(schema);
3523 for (let namespace of json) {
3524 this.getOwnNamespace(namespace.namespace).addSchema(namespace);
3529 * Checks whether a given object has the necessary permissions to
3530 * expose the given namespace.
3532 * @param {string} namespace
3533 * The top-level namespace to check permissions for.
3534 * @param {object} wrapperFuncs
3535 * Wrapper functions for the given context.
3536 * @param {function} wrapperFuncs.hasPermission
3537 * A function which, when given a string argument, returns true
3538 * if the context has the given permission.
3539 * @returns {boolean}
3540 * True if the context has permission for the given namespace.
3542 checkPermissions(namespace, wrapperFuncs) {
3543 let ns = this.getNamespace(namespace);
3544 if (ns && ns.permissions) {
3545 return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
3551 * Inject registered extension APIs into `dest`.
3553 * @param {object} dest The root namespace for the APIs.
3554 * This object is usually exposed to extensions as "chrome" or "browser".
3555 * @param {object} wrapperFuncs An implementation of the InjectionContext
3556 * interface, which runs the actual functionality of the generated API.
3558 inject(dest, wrapperFuncs) {
3559 let context = new InjectionContext(wrapperFuncs, this);
3561 this.injectInto(dest, context);
3564 injectInto(dest, context) {
3565 // For schema graphs where multiple schema roots have the same base, don't
3566 // inject it more than once.
3568 if (!context.injectedRoots.has(this)) {
3569 context.injectedRoots.add(this);
3571 this.base.injectInto(dest, context);
3573 super.injectInto(dest, context);
3578 * Normalize `obj` according to the loaded schema for `typeName`.
3580 * @param {object} obj The object to normalize against the schema.
3581 * @param {string} typeName The name in the format namespace.propertyname
3582 * @param {object} context An implementation of Context. Any validation errors
3583 * are reported to the given context.
3584 * @returns {object} The normalized object.
3586 normalize(obj, typeName, context) {
3587 let [namespaceName, prop] = typeName.split(".");
3588 let ns = this.getNamespace(namespaceName);
3589 let type = ns.get(prop);
3591 let result = type.normalize(obj, new Context(context));
3593 return { error: forceString(result.error) };
3602 REVOKE: Symbol("@@revoke"),
3604 // Maps a schema URL to the JSON contained in that schema file. This
3605 // is useful for sending the JSON across processes.
3606 schemaJSON: new Map(),
3608 // A map of schema JSON which should be available in all content processes.
3609 contentSchemaJSON: new Map(),
3611 // A map of schema JSON which should only be available to extension processes.
3612 privilegedSchemaJSON: new Map(),
3616 // A weakmap for the validation Context class instances given an extension
3617 // context (keyed by the extensin context instance).
3618 // This is used instead of the InjectionContext for webIDL API validation
3619 // and normalization (see Schemas.checkParameters).
3620 paramsValidationContexts: new DefaultWeakMap(
3621 extContext => new Context(extContext)
3625 if (!this.initialized) {
3628 if (!this._rootSchema) {
3629 this._rootSchema = new SchemaRoot(null, this.schemaJSON);
3630 this._rootSchema.parseSchemas();
3632 return this._rootSchema;
3635 getNamespace(name) {
3636 return this.rootSchema.getNamespace(name);
3640 if (this.initialized) {
3643 this.initialized = true;
3645 if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
3646 let addSchemas = schemas => {
3647 for (let [key, value] of schemas.entries()) {
3648 this.schemaJSON.set(key, value);
3652 if (WebExtensionPolicy.isExtensionProcess || DEBUG) {
3653 addSchemas(Services.cpmm.sharedData.get(KEY_PRIVILEGED_SCHEMAS));
3656 let schemas = Services.cpmm.sharedData.get(KEY_CONTENT_SCHEMAS);
3658 addSchemas(schemas);
3663 _loadCachedSchemasPromise: null,
3664 loadCachedSchemas() {
3665 if (!this._loadCachedSchemasPromise) {
3666 this._loadCachedSchemasPromise = StartupCache.schemas
3673 return this._loadCachedSchemasPromise;
3676 addSchema(url, schema, content = false) {
3677 this.schemaJSON.set(url, schema);
3680 this.contentSchemaJSON.set(url, schema);
3682 this.privilegedSchemaJSON.set(url, schema);
3685 if (this._rootSchema) {
3686 throw new Error("Schema loaded after root schema populated");
3690 updateSharedSchemas() {
3691 let { sharedData } = Services.ppmm;
3693 sharedData.set(KEY_CONTENT_SCHEMAS, this.contentSchemaJSON);
3694 sharedData.set(KEY_PRIVILEGED_SCHEMAS, this.privilegedSchemaJSON);
3698 return readJSONAndBlobbify(url);
3701 processSchema(json) {
3702 return blobbify(json);
3705 async load(url, content = false) {
3706 if (!isParentProcess) {
3710 const startTime = Cu.now();
3711 let schemaCache = await this.loadCachedSchemas();
3712 const fromCache = schemaCache.has(url);
3715 schemaCache.get(url) ||
3716 (await StartupCache.schemas.get(url, readJSONAndBlobbify));
3718 if (!this.schemaJSON.has(url)) {
3719 this.addSchema(url, blob, content);
3722 ChromeUtils.addProfilerMarker(
3725 `load ${url}, from cache: ${fromCache}`
3730 * Checks whether a given object has the necessary permissions to
3731 * expose the given namespace.
3733 * @param {string} namespace
3734 * The top-level namespace to check permissions for.
3735 * @param {object} wrapperFuncs
3736 * Wrapper functions for the given context.
3737 * @param {function} wrapperFuncs.hasPermission
3738 * A function which, when given a string argument, returns true
3739 * if the context has the given permission.
3740 * @returns {boolean}
3741 * True if the context has permission for the given namespace.
3743 checkPermissions(namespace, wrapperFuncs) {
3744 return this.rootSchema.checkPermissions(namespace, wrapperFuncs);
3748 * Returns a sorted array of permission names for the given permission types.
3750 * @param {Array} types An array of permission types, defaults to all permissions.
3751 * @returns {Array} sorted array of permission names
3756 "OptionalPermission",
3757 "PermissionNoPrompt",
3758 "OptionalPermissionNoPrompt",
3761 const ns = this.getNamespace("manifest");
3763 for (let typeName of types) {
3764 for (let choice of ns
3766 .choices.filter(choice => choice.enumeration)) {
3767 names = names.concat(choice.enumeration);
3770 return names.sort();
3776 * Inject registered extension APIs into `dest`.
3778 * @param {object} dest The root namespace for the APIs.
3779 * This object is usually exposed to extensions as "chrome" or "browser".
3780 * @param {object} wrapperFuncs An implementation of the InjectionContext
3781 * interface, which runs the actual functionality of the generated API.
3783 inject(dest, wrapperFuncs) {
3784 this.rootSchema.inject(dest, wrapperFuncs);
3788 * Normalize `obj` according to the loaded schema for `typeName`.
3790 * @param {object} obj The object to normalize against the schema.
3791 * @param {string} typeName The name in the format namespace.propertyname
3792 * @param {object} context An implementation of Context. Any validation errors
3793 * are reported to the given context.
3794 * @returns {object} The normalized object.
3796 normalize(obj, typeName, context) {
3797 return this.rootSchema.normalize(obj, typeName, context);
3801 * Validate and normalize the arguments for an API request originated
3802 * from the webIDL API bindings.
3804 * This provides for calls originating through WebIDL the parameters
3805 * validation and normalization guarantees that the ext-APINAMESPACE.js
3806 * scripts expects (what InjectionContext does for the regular bindings).
3808 * @param {object} extContext
3809 * @param {string} apiNamespace
3810 * @param {string} apiName
3811 * @param {Array<any>} args
3813 * @returns {Array<any>} Normalized arguments array.
3815 checkParameters(extContext, apiNamespace, apiName, args) {
3816 const apiSchema = this.getNamespace(apiNamespace)?.get(apiName);
3818 throw new Error(`API Schema not found for ${apiNamespace}.${apiName}`);
3821 return apiSchema.checkParameters(
3823 this.paramsValidationContexts.get(extContext)