Merge autoland to mozilla central a=merge
[gecko.git] / toolkit / components / extensions / Schemas.sys.mjs
blob92221a56077282acd253dcb47121f90d266ef7fb
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;
14 /** @type {Lazy} */
15 const lazy = {};
17 ChromeUtils.defineESModuleGetters(lazy, {
18   ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
19   NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
20   ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
21 });
23 XPCOMUtils.defineLazyServiceGetter(
24   lazy,
25   "contentPolicyService",
26   "@mozilla.org/addons/content-policy;1",
27   "nsIAddonContentPolicy"
30 ChromeUtils.defineLazyGetter(
31   lazy,
32   "StartupCache",
33   () => lazy.ExtensionParent.StartupCache
36 XPCOMUtils.defineLazyPreferenceGetter(
37   lazy,
38   "treatWarningsAsErrors",
39   "extensions.webextensions.warnings-as-errors",
40   false
43 const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content";
44 const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged";
46 const MIN_MANIFEST_VERSION = 2;
47 const MAX_MANIFEST_VERSION = 3;
49 const { DEBUG } = AppConstants;
51 const isParentProcess =
52   Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
54 function readJSON(url) {
55   return new Promise((resolve, reject) => {
56     lazy.NetUtil.asyncFetch(
57       { uri: url, loadUsingSystemPrincipal: true },
58       (inputStream, status) => {
59         if (!Components.isSuccessCode(status)) {
60           // Convert status code to a string
61           let e = Components.Exception("", status);
62           reject(new Error(`Error while loading '${url}' (${e.name})`));
63           return;
64         }
65         try {
66           let text = lazy.NetUtil.readInputStreamToString(
67             inputStream,
68             inputStream.available()
69           );
71           // Chrome JSON files include a license comment that we need to
72           // strip off for this to be valid JSON. As a hack, we just
73           // look for the first '[' character, which signals the start
74           // of the JSON content.
75           let index = text.indexOf("[");
76           text = text.slice(index);
78           resolve(JSON.parse(text));
79         } catch (e) {
80           reject(e);
81         }
82       }
83     );
84   });
87 function stripDescriptions(json, stripThis = true) {
88   if (Array.isArray(json)) {
89     for (let i = 0; i < json.length; i++) {
90       if (typeof json[i] === "object" && json[i] !== null) {
91         json[i] = stripDescriptions(json[i]);
92       }
93     }
94     return json;
95   }
97   let result = {};
99   // Objects are handled much more efficiently, both in terms of memory and
100   // CPU, if they have the same shape as other objects that serve the same
101   // purpose. So, normalize the order of properties to increase the chances
102   // that the majority of schema objects wind up in large shape groups.
103   for (let key of Object.keys(json).sort()) {
104     if (stripThis && key === "description" && typeof json[key] === "string") {
105       continue;
106     }
108     if (typeof json[key] === "object" && json[key] !== null) {
109       result[key] = stripDescriptions(json[key], key !== "properties");
110     } else {
111       result[key] = json[key];
112     }
113   }
115   return result;
118 function blobbify(json) {
119   // We don't actually use descriptions at runtime, and they make up about a
120   // third of the size of our structured clone data, so strip them before
121   // blobbifying.
122   json = stripDescriptions(json);
124   return new StructuredCloneHolder("Schemas/blobbify", null, json);
127 async function readJSONAndBlobbify(url) {
128   let json = await readJSON(url);
130   return blobbify(json);
134  * Defines a lazy getter for the given property on the given object. Any
135  * security wrappers are waived on the object before the property is
136  * defined, and the getter and setter methods are wrapped for the target
137  * scope.
139  * The given getter function is guaranteed to be called only once, even
140  * if the target scope retrieves the wrapped getter from the property
141  * descriptor and calls it directly.
143  * @param {object} object
144  *        The object on which to define the getter.
145  * @param {string | symbol} prop
146  *        The property name for which to define the getter.
147  * @param {Function} getter
148  *        The function to call in order to generate the final property
149  *        value.
150  */
151 function exportLazyGetter(object, prop, getter) {
152   object = ChromeUtils.waiveXrays(object);
154   let redefine = value => {
155     if (value === undefined) {
156       delete object[prop];
157     } else {
158       Object.defineProperty(object, prop, {
159         enumerable: true,
160         configurable: true,
161         writable: true,
162         value,
163       });
164     }
166     getter = null;
168     return value;
169   };
171   Object.defineProperty(object, prop, {
172     enumerable: true,
173     configurable: true,
175     get: Cu.exportFunction(function () {
176       return redefine(getter.call(this));
177     }, object),
179     set: Cu.exportFunction(value => {
180       redefine(value);
181     }, object),
182   });
186  * Defines a lazily-instantiated property descriptor on the given
187  * object. Any security wrappers are waived on the object before the
188  * property is defined.
190  * The given getter function is guaranteed to be called only once, even
191  * if the target scope retrieves the wrapped getter from the property
192  * descriptor and calls it directly.
194  * @param {object} object
195  *        The object on which to define the getter.
196  * @param {string | symbol} prop
197  *        The property name for which to define the getter.
198  * @param {Function} getter
199  *        The function to call in order to generate the final property
200  *        descriptor object. This will be called, and the property
201  *        descriptor installed on the object, the first time the
202  *        property is written or read. The function may return
203  *        undefined, which will cause the property to be deleted.
204  */
205 function exportLazyProperty(object, prop, getter) {
206   object = ChromeUtils.waiveXrays(object);
208   let redefine = obj => {
209     let desc = getter.call(obj);
210     getter = null;
212     delete object[prop];
213     if (desc) {
214       let defaults = {
215         configurable: true,
216         enumerable: true,
217       };
219       if (!desc.set && !desc.get) {
220         defaults.writable = true;
221       }
223       Object.defineProperty(object, prop, Object.assign(defaults, desc));
224     }
225   };
227   Object.defineProperty(object, prop, {
228     enumerable: true,
229     configurable: true,
231     get: Cu.exportFunction(function () {
232       redefine(this);
233       return object[prop];
234     }, object),
236     set: Cu.exportFunction(function (value) {
237       redefine(this);
238       object[prop] = value;
239     }, object),
240   });
243 const POSTPROCESSORS = {
244   convertImageDataToURL(imageData, context) {
245     let document = context.cloneScope.document;
246     let canvas = document.createElementNS(
247       "http://www.w3.org/1999/xhtml",
248       "canvas"
249     );
250     canvas.width = imageData.width;
251     canvas.height = imageData.height;
252     canvas.getContext("2d").putImageData(imageData, 0, 0);
254     return canvas.toDataURL("image/png");
255   },
256   mutuallyExclusiveBlockingOrAsyncBlocking(value, context) {
257     if (!Array.isArray(value)) {
258       return value;
259     }
260     if (value.includes("blocking") && value.includes("asyncBlocking")) {
261       throw new context.cloneScope.Error(
262         "'blocking' and 'asyncBlocking' are mutually exclusive"
263       );
264     }
265     return value;
266   },
267   webRequestBlockingPermissionRequired(string, context) {
268     if (string === "blocking" && !context.hasPermission("webRequestBlocking")) {
269       throw new context.cloneScope.Error(
270         "Using webRequest.addListener with the " +
271           "blocking option requires the 'webRequestBlocking' permission."
272       );
273     }
275     return string;
276   },
277   webRequestBlockingOrAuthProviderPermissionRequired(string, context) {
278     if (
279       string === "blocking" &&
280       !(
281         context.hasPermission("webRequestBlocking") ||
282         context.hasPermission("webRequestAuthProvider")
283       )
284     ) {
285       throw new context.cloneScope.Error(
286         "Using webRequest.onAuthRequired.addListener with the " +
287           "blocking option requires either the 'webRequestBlocking' " +
288           "or 'webRequestAuthProvider' permission."
289       );
290     }
292     return string;
293   },
294   requireBackgroundServiceWorkerEnabled(value, context) {
295     if (WebExtensionPolicy.backgroundServiceWorkerEnabled) {
296       return value;
297     }
299     // Add an error to the manifest validations and throw the
300     // same error.
301     const msg = "background.service_worker is currently disabled";
302     context.logError(context.makeError(msg));
303     throw new Error(msg);
304   },
306   manifestVersionCheck(value, context) {
307     if (
308       value == 2 ||
309       (value == 3 &&
310         Services.prefs.getBoolPref("extensions.manifestV3.enabled", false))
311     ) {
312       return value;
313     }
314     const msg = `Unsupported manifest version: ${value}`;
315     context.logError(context.makeError(msg));
316     throw new Error(msg);
317   },
319   webAccessibleMatching(value, context) {
320     // Ensure each object has at least one of matches or extension_ids array.
321     for (let obj of value) {
322       if (!obj.matches && !obj.extension_ids) {
323         const msg = `web_accessible_resources requires one of "matches" or "extension_ids"`;
324         context.logError(context.makeError(msg));
325         throw new Error(msg);
326       }
327     }
328     return value;
329   },
331   incognitoSplitUnsupportedAndFallback(value, context) {
332     if (value === "split") {
333       // incognito:split has not been implemented (bug 1380812). There are two
334       // alternatives: "spanning" and "not_allowed".
335       //
336       // "incognito":"split" is required by Chrome when extensions want to load
337       // any extension page in a tab in Chrome. In Firefox that is not required,
338       // so extensions could replace "split" with "spanning".
339       // Another (poorly documented) effect of "incognito":"split" is separation
340       // of some state between some extension APIs. Because this can in theory
341       // result in unwanted mixing of state between private and non-private
342       // browsing, we fall back to "not_allowed", which prevents the user from
343       // enabling the extension in private browsing windows.
344       value = "not_allowed";
345       context.logWarning(
346         `incognito "split" is unsupported. Falling back to incognito "${value}".`
347       );
348     }
349     return value;
350   },
353 // Parses a regular expression, with support for the Python extended
354 // syntax that allows setting flags by including the string (?im)
355 function parsePattern(pattern) {
356   let flags = "";
357   let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
358   if (match) {
359     [, flags, pattern] = match;
360   }
361   return new RegExp(pattern, flags);
364 function getValueBaseType(value) {
365   let type = typeof value;
366   switch (type) {
367     case "object":
368       if (value === null) {
369         return "null";
370       }
371       if (Array.isArray(value)) {
372         return "array";
373       }
374       break;
376     case "number":
377       if (value % 1 === 0) {
378         return "integer";
379       }
380   }
381   return type;
384 // Methods of Context that are used by Schemas.normalize. These methods can be
385 // overridden at the construction of Context.
386 const CONTEXT_FOR_VALIDATION = ["checkLoadURL", "hasPermission", "logError"];
388 // Methods of Context that are used by Schemas.inject.
389 // Callers of Schemas.inject should implement all of these methods.
390 const CONTEXT_FOR_INJECTION = [
391   ...CONTEXT_FOR_VALIDATION,
392   "getImplementation",
393   "isPermissionRevokable",
394   "shouldInject",
397 // If the message is a function, call it and return the result.
398 // Otherwise, assume it's a string.
399 function forceString(msg) {
400   if (typeof msg === "function") {
401     return msg();
402   }
403   return msg;
407  * A context for schema validation and error reporting. This class is only used
408  * internally within Schemas.
409  */
410 class Context {
411   /**
412    * @param {object} params Provides the implementation of this class.
413    * @param {Array<string>} overridableMethods
414    */
415   constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) {
416     this.params = params;
418     if (typeof params.manifestVersion !== "number") {
419       throw new Error(
420         `Unexpected params.manifestVersion value: ${params.manifestVersion}`
421       );
422     }
424     this.path = [];
425     this.preprocessors = {
426       localize(value) {
427         return value;
428       },
429       ...params.preprocessors,
430     };
432     this.postprocessors = POSTPROCESSORS;
433     this.isChromeCompat = params.isChromeCompat ?? false;
434     this.manifestVersion = params.manifestVersion;
436     this.currentChoices = new Set();
437     this.choicePathIndex = 0;
439     for (let method of overridableMethods) {
440       if (method in params) {
441         this[method] = params[method].bind(params);
442       }
443     }
444   }
446   get choicePath() {
447     let path = this.path.slice(this.choicePathIndex);
448     return path.join(".");
449   }
451   get cloneScope() {
452     return this.params.cloneScope || undefined;
453   }
455   get url() {
456     return this.params.url;
457   }
459   get ignoreUnrecognizedProperties() {
460     return !!this.params.ignoreUnrecognizedProperties;
461   }
463   get principal() {
464     return (
465       this.params.principal ||
466       Services.scriptSecurityManager.createNullPrincipal({})
467     );
468   }
470   /**
471    * Checks whether `url` may be loaded by the extension in this context.
472    *
473    * @param {string} url The URL that the extension wished to load.
474    * @returns {boolean} Whether the context may load `url`.
475    */
476   checkLoadURL(url) {
477     let ssm = Services.scriptSecurityManager;
478     try {
479       ssm.checkLoadURIWithPrincipal(
480         this.principal,
481         Services.io.newURI(url),
482         ssm.DISALLOW_INHERIT_PRINCIPAL
483       );
484     } catch (e) {
485       return false;
486     }
487     return true;
488   }
490   /**
491    * Checks whether this context has the given permission.
492    *
493    * @param {string} _permission
494    *        The name of the permission to check.
495    *
496    * @returns {boolean} True if the context has the given permission.
497    */
498   hasPermission(_permission) {
499     return false;
500   }
502   /**
503    * Checks whether the given permission can be dynamically revoked or
504    * granted.
505    *
506    * @param {string} _permission
507    *        The name of the permission to check.
508    *
509    * @returns {boolean} True if the given permission is revokable.
510    */
511   isPermissionRevokable(_permission) {
512     return false;
513   }
515   /**
516    * Returns an error result object with the given message, for return
517    * by Type normalization functions.
518    *
519    * If the context has a `currentTarget` value, this is prepended to
520    * the message to indicate the location of the error.
521    *
522    * @param {string | Function} errorMessage
523    *        The error message which will be displayed when this is the
524    *        only possible matching schema. If a function is passed, it
525    *        will be evaluated when the error string is first needed, and
526    *        must return a string.
527    * @param {string | Function} choicesMessage
528    *        The message describing the valid what constitutes a valid
529    *        value for this schema, which will be displayed when multiple
530    *        schema choices are available and none match.
531    *
532    *        A caller may pass `null` to prevent a choice from being
533    *        added, but this should *only* be done from code processing a
534    *        choices type.
535    * @param {boolean} [warning = false]
536    *        If true, make message prefixed `Warning`. If false, make message
537    *        prefixed `Error`
538    * @returns {object}
539    */
540   error(errorMessage, choicesMessage = undefined, warning = false) {
541     if (choicesMessage !== null) {
542       let { choicePath } = this;
543       if (choicePath) {
544         choicesMessage = `.${choicePath} must ${choicesMessage}`;
545       }
547       this.currentChoices.add(choicesMessage);
548     }
550     if (this.currentTarget) {
551       let { currentTarget } = this;
552       return {
553         error: () =>
554           `${
555             warning ? "Warning" : "Error"
556           } processing ${currentTarget}: ${forceString(errorMessage)}`,
557       };
558     }
559     return { error: errorMessage };
560   }
562   /**
563    * Creates an `Error` object belonging to the current unprivileged
564    * scope. If there is no unprivileged scope associated with this
565    * context, the message is returned as a string.
566    *
567    * If the context has a `currentTarget` value, this is prepended to
568    * the message, in the same way as for the `error` method.
569    *
570    * @param {string} message
571    * @param {object} [options]
572    * @param {boolean} [options.warning = false]
573    * @returns {Error}
574    */
575   makeError(message, { warning = false } = {}) {
576     let error = forceString(this.error(message, null, warning).error);
577     if (this.cloneScope) {
578       return new this.cloneScope.Error(error);
579     }
580     return error;
581   }
583   /**
584    * Logs the given error to the console. May be overridden to enable
585    * custom logging.
586    *
587    * @param {Error|string} error
588    */
589   logError(error) {
590     if (this.cloneScope) {
591       Cu.reportError(
592         // Error objects logged using Cu.reportError are not associated
593         // to the related innerWindowID. This results in a leaked docshell
594         // since consoleService cannot release the error object when the
595         // extension global is destroyed.
596         typeof error == "string" ? error : String(error),
597         // Report the error with the appropriate stack trace when the
598         // is related to an actual extension global (instead of being
599         // related to a manifest validation).
600         this.principal && ChromeUtils.getCallerLocation(this.principal)
601       );
602     } else {
603       Cu.reportError(error);
604     }
605   }
607   /**
608    * Logs a warning. An error might be thrown when we treat warnings as errors.
609    *
610    * @param {string} warningMessage
611    */
612   logWarning(warningMessage) {
613     let error = this.makeError(warningMessage, { warning: true });
614     this.logError(error);
616     if (lazy.treatWarningsAsErrors) {
617       // This pref is false by default, and true by default in tests to
618       // discourage the use of deprecated APIs in our unit tests.
619       // If a warning is an expected part of a test, temporarily set the pref
620       // to false, e.g. with the ExtensionTestUtils.failOnSchemaWarnings helper.
621       Services.console.logStringMessage(
622         "Treating warning as error because the preference " +
623           "extensions.webextensions.warnings-as-errors is set to true"
624       );
625       if (typeof error === "string") {
626         error = new Error(error);
627       }
628       throw error;
629     }
630   }
632   /**
633    * Returns the name of the value currently being normalized. For a
634    * nested object, this is usually approximately equivalent to the
635    * JavaScript property accessor for that property. Given:
636    *
637    *   { foo: { bar: [{ baz: x }] } }
638    *
639    * When processing the value for `x`, the currentTarget is
640    * 'foo.bar.0.baz'
641    */
642   get currentTarget() {
643     return this.path.join(".");
644   }
646   /**
647    * Executes the given callback, and returns an array of choice strings
648    * passed to {@see #error} during its execution.
649    *
650    * @param {Function} callback
651    * @returns {object}
652    *          An object with a `result` property containing the return
653    *          value of the callback, and a `choice` property containing
654    *          an array of choices.
655    */
656   withChoices(callback) {
657     let { currentChoices, choicePathIndex } = this;
659     let choices = new Set();
660     this.currentChoices = choices;
661     this.choicePathIndex = this.path.length;
663     try {
664       let result = callback();
666       return { result, choices };
667     } finally {
668       this.currentChoices = currentChoices;
669       this.choicePathIndex = choicePathIndex;
671       if (choices.size == 1) {
672         for (let choice of choices) {
673           currentChoices.add(choice);
674         }
675       } else if (choices.size) {
676         this.error(null, () => {
677           let array = Array.from(choices, forceString);
678           let n = array.length - 1;
679           array[n] = `or ${array[n]}`;
681           return `must either [${array.join(", ")}]`;
682         });
683       }
684     }
685   }
687   /**
688    * Appends the given component to the `currentTarget` path to indicate
689    * that it is being processed, calls the given callback function, and
690    * then restores the original path.
691    *
692    * This is used to identify the path of the property being processed
693    * when reporting type errors.
694    *
695    * @param {string} component
696    * @param {Function} callback
697    * @returns {*}
698    */
699   withPath(component, callback) {
700     this.path.push(component);
701     try {
702       return callback();
703     } finally {
704       this.path.pop();
705     }
706   }
708   matchManifestVersion(entry) {
709     let { manifestVersion } = this;
710     return (
711       manifestVersion >= entry.min_manifest_version &&
712       manifestVersion <= entry.max_manifest_version
713     );
714   }
718  * Represents a schema entry to be injected into an object. Handles the
719  * injection, revocation, and permissions of said entry.
721  * @param {InjectionContext} context
722  *        The injection context for the entry.
723  * @param {Entry} entry
724  *        The entry to inject.
725  * @param {object} parentObject
726  *        The object into which to inject this entry.
727  * @param {string} name
728  *        The property name at which to inject this entry.
729  * @param {Array<string>} path
730  *        The full path from the root entry to this entry.
731  * @param {Entry} parentEntry
732  *        The parent entry for the injected entry.
733  */
734 class InjectionEntry {
735   constructor(context, entry, parentObj, name, path, parentEntry) {
736     this.context = context;
737     this.entry = entry;
738     this.parentObj = parentObj;
739     this.name = name;
740     this.path = path;
741     this.parentEntry = parentEntry;
743     this.injected = null;
744     this.lazyInjected = null;
745   }
747   /**
748    * @property {Array<string>} allowedContexts
749    *        The list of allowed contexts into which the entry may be
750    *        injected.
751    */
752   get allowedContexts() {
753     let { allowedContexts } = this.entry;
754     if (allowedContexts.length) {
755       return allowedContexts;
756     }
757     return this.parentEntry.defaultContexts;
758   }
760   /**
761    * @property {boolean} isRevokable
762    *        Returns true if this entry may be dynamically injected or
763    *        revoked based on its permissions.
764    */
765   get isRevokable() {
766     return (
767       this.entry.permissions &&
768       this.entry.permissions.some(perm =>
769         this.context.isPermissionRevokable(perm)
770       )
771     );
772   }
774   /**
775    * @property {boolean} hasPermission
776    *        Returns true if the injection context currently has the
777    *        appropriate permissions to access this entry.
778    */
779   get hasPermission() {
780     return (
781       !this.entry.permissions ||
782       this.entry.permissions.some(perm => this.context.hasPermission(perm))
783     );
784   }
786   /**
787    * @property {boolean} shouldInject
788    *        Returns true if this entry should be injected in the given
789    *        context, without respect to permissions.
790    */
791   get shouldInject() {
792     return (
793       this.context.matchManifestVersion(this.entry) &&
794       this.context.shouldInject(
795         this.path.join("."),
796         this.name,
797         this.allowedContexts
798       )
799     );
800   }
802   /**
803    * Revokes this entry, removing its property from its parent object,
804    * and invalidating its wrappers.
805    */
806   revoke() {
807     if (this.lazyInjected) {
808       this.lazyInjected = false;
809     } else if (this.injected) {
810       if (this.injected.revoke) {
811         this.injected.revoke();
812       }
814       try {
815         let unwrapped = ChromeUtils.waiveXrays(this.parentObj);
816         delete unwrapped[this.name];
817       } catch (e) {
818         Cu.reportError(e);
819       }
821       let { value } = this.injected.descriptor;
822       if (value) {
823         this.context.revokeChildren(value);
824       }
826       this.injected = null;
827     }
828   }
830   /**
831    * Returns a property descriptor object for this entry, if it should
832    * be injected, or undefined if it should not.
833    *
834    * @returns {object?}
835    *        A property descriptor object, or undefined if the property
836    *        should be removed.
837    */
838   getDescriptor() {
839     this.lazyInjected = false;
841     if (this.injected) {
842       let path = [...this.path, this.name];
843       throw new Error(
844         `Attempting to re-inject already injected entry: ${path.join(".")}`
845       );
846     }
848     if (!this.shouldInject) {
849       return;
850     }
852     if (this.isRevokable) {
853       this.context.pendingEntries.add(this);
854     }
856     if (!this.hasPermission) {
857       return;
858     }
860     this.injected = this.entry.getDescriptor(this.path, this.context);
861     if (!this.injected) {
862       return undefined;
863     }
865     return this.injected.descriptor;
866   }
868   /**
869    * Injects a lazy property descriptor into the parent object which
870    * checks permissions and eligibility for injection the first time it
871    * is accessed.
872    */
873   lazyInject() {
874     if (this.lazyInjected || this.injected) {
875       let path = [...this.path, this.name];
876       throw new Error(
877         `Attempting to re-lazy-inject already injected entry: ${path.join(".")}`
878       );
879     }
881     this.lazyInjected = true;
882     exportLazyProperty(this.parentObj, this.name, () => {
883       if (this.lazyInjected) {
884         return this.getDescriptor();
885       }
886     });
887   }
889   /**
890    * Injects or revokes this entry if its current state does not match
891    * the context's current permissions.
892    */
893   permissionsChanged() {
894     if (this.injected) {
895       this.maybeRevoke();
896     } else {
897       this.maybeInject();
898     }
899   }
901   maybeInject() {
902     if (!this.injected && !this.lazyInjected) {
903       this.lazyInject();
904     }
905   }
907   maybeRevoke() {
908     if (this.injected && !this.hasPermission) {
909       this.revoke();
910     }
911   }
915  * Holds methods that run the actual implementation of the extension APIs. These
916  * methods are only called if the extension API invocation matches the signature
917  * as defined in the schema. Otherwise an error is reported to the context.
918  */
919 class InjectionContext extends Context {
920   constructor(params, schemaRoot) {
921     super(params, CONTEXT_FOR_INJECTION);
923     this.schemaRoot = schemaRoot;
925     this.pendingEntries = new Set();
926     this.children = new DefaultWeakMap(() => new Map());
928     this.injectedRoots = new Set();
930     if (params.setPermissionsChangedCallback) {
931       params.setPermissionsChangedCallback(this.permissionsChanged.bind(this));
932     }
933   }
935   /**
936    * Check whether the API should be injected.
937    *
938    * @abstract
939    * @param {string} _namespace The namespace of the API. This may contain dots,
940    *     e.g. in the case of "devtools.inspectedWindow".
941    * @param {string?} _name The name of the property in the namespace.
942    *     `null` if we are checking whether the namespace should be injected.
943    * @param {Array<string>} _allowedContexts A list of additional contexts in
944    *      which this API should be available. May include any of:
945    *         "main" - The main chrome browser process.
946    *         "addon" - An addon process.
947    *         "content" - A content process.
948    * @returns {boolean} Whether the API should be injected.
949    */
950   shouldInject(_namespace, _name, _allowedContexts) {
951     throw new Error("Not implemented");
952   }
954   /**
955    * Generate the implementation for `namespace`.`name`.
956    *
957    * @abstract
958    * @param {string} _namespace The full path to the namespace of the API, minus
959    *     the name of the method or property. E.g. "storage.local".
960    * @param {string} _name The name of the method, property or event.
961    * @returns {import("ExtensionCommon.sys.mjs").SchemaAPIInterface}
962    *          The implementation of the API.
963    */
964   getImplementation(_namespace, _name) {
965     throw new Error("Not implemented");
966   }
968   /**
969    * Updates all injection entries which may need to be updated after a
970    * permission change, revoking or re-injecting them as necessary.
971    */
972   permissionsChanged() {
973     for (let entry of this.pendingEntries) {
974       try {
975         entry.permissionsChanged();
976       } catch (e) {
977         Cu.reportError(e);
978       }
979     }
980   }
982   /**
983    * Recursively revokes all child injection entries of the given
984    * object.
985    *
986    * @param {object} object
987    *        The object for which to invoke children.
988    */
989   revokeChildren(object) {
990     if (!this.children.has(object)) {
991       return;
992     }
994     let children = this.children.get(object);
995     for (let [name, entry] of children.entries()) {
996       try {
997         entry.revoke();
998       } catch (e) {
999         Cu.reportError(e);
1000       }
1001       children.delete(name);
1003       // When we revoke children for an object, we consider that object
1004       // dead. If the entry is ever reified again, a new object is
1005       // created, with new child entries.
1006       this.pendingEntries.delete(entry);
1007     }
1008     this.children.delete(object);
1009   }
1011   _getInjectionEntry(entry, dest, name, path, parentEntry) {
1012     let injection = new InjectionEntry(
1013       this,
1014       entry,
1015       dest,
1016       name,
1017       path,
1018       parentEntry
1019     );
1021     this.children.get(dest).set(name, injection);
1023     return injection;
1024   }
1026   /**
1027    * Returns the property descriptor for the given entry.
1028    *
1029    * @param {Entry} entry
1030    *        The entry instance to return a descriptor for.
1031    * @param {object} dest
1032    *        The object into which this entry is being injected.
1033    * @param {string} name
1034    *        The property name on the destination object where the entry
1035    *        will be injected.
1036    * @param {Array<string>} path
1037    *        The full path from the root injection object to this entry.
1038    * @param {Partial<Entry>} parentEntry
1039    *        The parent entry for this entry.
1040    *
1041    * @returns {object?}
1042    *        A property descriptor object, or null if the entry should
1043    *        not be injected.
1044    */
1045   getDescriptor(entry, dest, name, path, parentEntry) {
1046     let injection = this._getInjectionEntry(
1047       entry,
1048       dest,
1049       name,
1050       path,
1051       parentEntry
1052     );
1054     return injection.getDescriptor();
1055   }
1057   /**
1058    * Lazily injects the given entry into the given object.
1059    *
1060    * @param {Entry} entry
1061    *        The entry instance to lazily inject.
1062    * @param {object} dest
1063    *        The object into which to inject this entry.
1064    * @param {string} name
1065    *        The property name at which to inject the entry.
1066    * @param {Array<string>} path
1067    *        The full path from the root injection object to this entry.
1068    * @param {Entry} parentEntry
1069    *        The parent entry for this entry.
1070    */
1071   injectInto(entry, dest, name, path, parentEntry) {
1072     let injection = this._getInjectionEntry(
1073       entry,
1074       dest,
1075       name,
1076       path,
1077       parentEntry
1078     );
1080     injection.lazyInject();
1081   }
1085  * The methods in this singleton represent the "format" specifier for
1086  * JSON Schema string types.
1088  * Each method either returns a normalized version of the original
1089  * value, or throws an error if the value is not valid for the given
1090  * format.
1091  */
1092 const FORMATS = {
1093   hostname(string) {
1094     // TODO bug 1797376: Despite the name, this format is NOT a "hostname",
1095     // but hostname + port and may fail with IPv6. Use canonicalDomain instead.
1096     let valid = true;
1098     try {
1099       valid = new URL(`http://${string}`).host === string;
1100     } catch (e) {
1101       valid = false;
1102     }
1104     if (!valid) {
1105       throw new Error(`Invalid hostname ${string}`);
1106     }
1108     return string;
1109   },
1111   canonicalDomain(string) {
1112     let valid;
1114     try {
1115       valid = new URL(`http://${string}`).hostname === string;
1116     } catch (e) {
1117       valid = false;
1118     }
1120     if (!valid) {
1121       // Require the input to be a canonical domain.
1122       // Rejects obvious non-domains such as URLs,
1123       // but also catches non-IDN (punycode) domains.
1124       throw new Error(`Invalid domain ${string}`);
1125     }
1127     return string;
1128   },
1130   url(string, context) {
1131     let url = new URL(string).href;
1133     if (!context.checkLoadURL(url)) {
1134       throw new Error(`Access denied for URL ${url}`);
1135     }
1136     return url;
1137   },
1139   origin(string, context) {
1140     let url;
1141     try {
1142       url = new URL(string);
1143     } catch (e) {
1144       throw new Error(`Invalid origin: ${string}`);
1145     }
1146     if (!/^https?:/.test(url.protocol)) {
1147       throw new Error(`Invalid origin must be http or https for URL ${string}`);
1148     }
1149     // url.origin is punycode so a direct check against string wont work.
1150     // url.href appends a slash even if not in the original string, we we
1151     // additionally check that string does not end in slash.
1152     if (string.endsWith("/") || url.href != new URL(url.origin).href) {
1153       throw new Error(
1154         `Invalid origin for URL ${string}, replace with origin ${url.origin}`
1155       );
1156     }
1157     if (!context.checkLoadURL(url.origin)) {
1158       throw new Error(`Access denied for URL ${url}`);
1159     }
1160     return url.origin;
1161   },
1163   relativeUrl(string, context) {
1164     if (!context.url) {
1165       // If there's no context URL, return relative URLs unresolved, and
1166       // skip security checks for them.
1167       try {
1168         new URL(string);
1169       } catch (e) {
1170         return string;
1171       }
1172     }
1174     let url = new URL(string, context.url).href;
1176     if (!context.checkLoadURL(url)) {
1177       throw new Error(`Access denied for URL ${url}`);
1178     }
1179     return url;
1180   },
1182   strictRelativeUrl(string, context) {
1183     void FORMATS.unresolvedRelativeUrl(string);
1184     return FORMATS.relativeUrl(string, context);
1185   },
1187   unresolvedRelativeUrl(string) {
1188     if (!string.startsWith("//")) {
1189       try {
1190         new URL(string);
1191       } catch (e) {
1192         return string;
1193       }
1194     }
1196     throw new SyntaxError(
1197       `String ${JSON.stringify(string)} must be a relative URL`
1198     );
1199   },
1201   homepageUrl(string, context) {
1202     // Pipes are used for separating homepages, but we only allow extensions to
1203     // set a single homepage. Encoding any pipes makes it one URL.
1204     return FORMATS.relativeUrl(
1205       string.replace(new RegExp("\\|", "g"), "%7C"),
1206       context
1207     );
1208   },
1210   imageDataOrStrictRelativeUrl(string, context) {
1211     // Do not accept a string which resolves as an absolute URL, or any
1212     // protocol-relative URL, except PNG or JPG data URLs
1213     if (
1214       !string.startsWith("data:image/png;base64,") &&
1215       !string.startsWith("data:image/jpeg;base64,")
1216     ) {
1217       try {
1218         return FORMATS.strictRelativeUrl(string, context);
1219       } catch (e) {
1220         throw new SyntaxError(
1221           `String ${JSON.stringify(
1222             string
1223           )} must be a relative or PNG or JPG data:image URL`
1224         );
1225       }
1226     }
1227     return string;
1228   },
1230   contentSecurityPolicy(string, context) {
1231     // Manifest V3 extension_pages allows WASM.  When sandbox is
1232     // implemented, or any other V3 or later directive, the flags
1233     // logic will need to be updated.
1234     let flags =
1235       context.manifestVersion < 3
1236         ? Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY
1237         : Ci.nsIAddonContentPolicy.CSP_ALLOW_WASM;
1238     let error = lazy.contentPolicyService.validateAddonCSP(string, flags);
1239     if (error != null) {
1240       // The CSP validation error is not reported as part of the "choices" error message,
1241       // we log the CSP validation error explicitly here to make it easier for the addon developers
1242       // to see and fix the extension CSP.
1243       context.logError(`Error processing ${context.currentTarget}: ${error}`);
1244       return null;
1245     }
1246     return string;
1247   },
1249   date(string) {
1250     // A valid ISO 8601 timestamp.
1251     const PATTERN =
1252       /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/;
1253     if (!PATTERN.test(string)) {
1254       throw new Error(`Invalid date string ${string}`);
1255     }
1256     // Our pattern just checks the format, we could still have invalid
1257     // values (e.g., month=99 or month=02 and day=31).  Let the Date
1258     // constructor do the dirty work of validating.
1259     if (isNaN(Date.parse(string))) {
1260       throw new Error(`Invalid date string ${string}`);
1261     }
1262     return string;
1263   },
1265   manifestShortcutKey(string) {
1266     if (lazy.ShortcutUtils.validate(string) == lazy.ShortcutUtils.IS_VALID) {
1267       return string;
1268     }
1269     let errorMessage =
1270       `Value "${string}" must consist of ` +
1271       `either a combination of one or two modifiers, including ` +
1272       `a mandatory primary modifier and a key, separated by '+', ` +
1273       `or a media key. For details see: ` +
1274       `https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`;
1275     throw new Error(errorMessage);
1276   },
1278   manifestShortcutKeyOrEmpty(string) {
1279     return string === "" ? "" : FORMATS.manifestShortcutKey(string);
1280   },
1282   versionString(string, context) {
1283     const parts = string.split(".");
1285     if (
1286       // We accept up to 4 numbers.
1287       parts.length > 4 ||
1288       // Non-zero values cannot start with 0 and we allow numbers up to 9 digits.
1289       parts.some(part => !/^(0|[1-9][0-9]{0,8})$/.test(part))
1290     ) {
1291       context.logWarning(
1292         `version must be a version string consisting of at most 4 integers ` +
1293           `of at most 9 digits without leading zeros, and separated with dots`
1294       );
1295     }
1297     // The idea is to only emit a warning when the version string does not
1298     // match the simple format we want to encourage developers to use. Given
1299     // the version is required, we always accept the value as is.
1300     return string;
1301   },
1304 // Schema files contain namespaces, and each namespace contains types,
1305 // properties, functions, and events. An Entry is a base class for
1306 // types, properties, functions, and events.
1307 class Entry {
1308   constructor(schema = {}) {
1309     /**
1310      * If set to any value which evaluates as true, this entry is
1311      * deprecated, and any access to it will result in a deprecation
1312      * warning being logged to the browser console.
1313      *
1314      * If the value is a string, it will be appended to the deprecation
1315      * message. If it contains the substring "${value}", it will be
1316      * replaced with a string representation of the value being
1317      * processed.
1318      *
1319      * If the value is any other truthy value, a generic deprecation
1320      * message will be emitted.
1321      */
1322     this.deprecated = false;
1323     if ("deprecated" in schema) {
1324       this.deprecated = schema.deprecated;
1325     }
1327     /**
1328      * @property {string} [preprocessor]
1329      * If set to a string value, and a preprocessor of the same is
1330      * defined in the validation context, it will be applied to this
1331      * value prior to any normalization.
1332      */
1333     this.preprocessor = schema.preprocess || null;
1335     /**
1336      * @property {string} [postprocessor]
1337      * If set to a string value, and a postprocessor of the same is
1338      * defined in the validation context, it will be applied to this
1339      * value after any normalization.
1340      */
1341     this.postprocessor = schema.postprocess || null;
1343     /**
1344      * @property {Array<string>} allowedContexts A list of allowed contexts
1345      * to consider before generating the API.
1346      * These are not parsed by the schema, but passed to `shouldInject`.
1347      */
1348     this.allowedContexts = schema.allowedContexts || [];
1350     this.min_manifest_version =
1351       schema.min_manifest_version ?? MIN_MANIFEST_VERSION;
1352     this.max_manifest_version =
1353       schema.max_manifest_version ?? MAX_MANIFEST_VERSION;
1354   }
1356   /**
1357    * Preprocess the given value with the preprocessor declared in
1358    * `preprocessor`.
1359    *
1360    * @param {*} value
1361    * @param {Context} context
1362    * @returns {*}
1363    */
1364   preprocess(value, context) {
1365     if (this.preprocessor) {
1366       return context.preprocessors[this.preprocessor](value, context);
1367     }
1368     return value;
1369   }
1371   /**
1372    * Postprocess the given result with the postprocessor declared in
1373    * `postprocessor`.
1374    *
1375    * @param {object} result
1376    * @param {Context} context
1377    * @returns {object}
1378    */
1379   postprocess(result, context) {
1380     if (result.error || !this.postprocessor) {
1381       return result;
1382     }
1384     let value = context.postprocessors[this.postprocessor](
1385       result.value,
1386       context
1387     );
1388     return { value };
1389   }
1391   /**
1392    * Logs a deprecation warning for this entry, based on the value of
1393    * its `deprecated` property.
1394    *
1395    * @param {Context} context
1396    * @param {any} [value]
1397    */
1398   logDeprecation(context, value = null) {
1399     let message = "This property is deprecated";
1400     if (typeof this.deprecated == "string") {
1401       message = this.deprecated;
1402       if (message.includes("${value}")) {
1403         try {
1404           value = JSON.stringify(value);
1405         } catch (e) {
1406           value = String(value);
1407         }
1408         message = message.replace(/\$\{value\}/g, () => value);
1409       }
1410     }
1412     context.logWarning(message);
1413   }
1415   /**
1416    * Checks whether the entry is deprecated and, if so, logs a
1417    * deprecation message.
1418    *
1419    * @param {Context} context
1420    * @param {any} [value]
1421    */
1422   checkDeprecated(context, value = null) {
1423     if (this.deprecated) {
1424       this.logDeprecation(context, value);
1425     }
1426   }
1428   /**
1429    * Returns an object containing property descriptor for use when
1430    * injecting this entry into an API object.
1431    *
1432    * @param {Array<string>} _path The API path, e.g. `["storage", "local"]`.
1433    * @param {InjectionContext} _context
1434    *
1435    * @returns {object?}
1436    *        An object containing a `descriptor` property, specifying the
1437    *        entry's property descriptor, and an optional `revoke`
1438    *        method, to be called when the entry is being revoked.
1439    */
1440   getDescriptor(_path, _context) {
1441     return undefined;
1442   }
1445 // Corresponds either to a type declared in the "types" section of the
1446 // schema or else to any type object used throughout the schema.
1447 class Type extends Entry {
1448   /**
1449    * @property {Array<string>} EXTRA_PROPERTIES
1450    *        An array of extra properties which may be present for
1451    *        schemas of this type.
1452    */
1453   static get EXTRA_PROPERTIES() {
1454     return [
1455       "description",
1456       "deprecated",
1457       "preprocess",
1458       "postprocess",
1459       "privileged",
1460       "allowedContexts",
1461       "min_manifest_version",
1462       "max_manifest_version",
1463     ];
1464   }
1466   /**
1467    * Parses the given schema object and returns an instance of this
1468    * class which corresponds to its properties.
1469    *
1470    * @param {SchemaRoot} root
1471    *        The root schema for this type.
1472    * @param {object} schema
1473    *        A JSON schema object which corresponds to a definition of
1474    *        this type.
1475    * @param {Array<string>} path
1476    *        The path to this schema object from the root schema,
1477    *        corresponding to the property names and array indices
1478    *        traversed during parsing in order to arrive at this schema
1479    *        object.
1480    * @param {Array<string>} [extraProperties]
1481    *        An array of extra property names which are valid for this
1482    *        schema in the current context.
1483    * @returns {Type}
1484    *        An instance of this type which corresponds to the given
1485    *        schema object.
1486    * @static
1487    */
1488   static parseSchema(root, schema, path, extraProperties = []) {
1489     this.checkSchemaProperties(schema, path, extraProperties);
1491     return new this(schema);
1492   }
1494   /**
1495    * Checks that all of the properties present in the given schema
1496    * object are valid properties for this type, and throws if invalid.
1497    *
1498    * @param {object} schema
1499    *        A JSON schema object.
1500    * @param {Array<string>} path
1501    *        The path to this schema object from the root schema,
1502    *        corresponding to the property names and array indices
1503    *        traversed during parsing in order to arrive at this schema
1504    *        object.
1505    * @param {Iterable<string>} [extra]
1506    *        An array of extra property names which are valid for this
1507    *        schema in the current context.
1508    * @throws {Error}
1509    *        An error describing the first invalid property found in the
1510    *        schema object.
1511    */
1512   static checkSchemaProperties(schema, path, extra = []) {
1513     if (DEBUG) {
1514       let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]);
1516       for (let prop of Object.keys(schema)) {
1517         if (!allowedSet.has(prop)) {
1518           throw new Error(
1519             `Internal error: Namespace ${path.join(".")} has ` +
1520               `invalid type property "${prop}" ` +
1521               `in type "${schema.id || JSON.stringify(schema)}"`
1522           );
1523         }
1524       }
1525     }
1526   }
1528   /**
1529    * Takes a value, checks that it has the correct type, and returns a
1530    * "normalized" version of the value. The normalized version will
1531    * include "nulls" in place of omitted optional properties. The
1532    * result of this function is either {error: "Some type error"} or
1533    * {value: <normalized-value>}.
1534    */
1535   normalize(value, context) {
1536     return context.error("invalid type");
1537   }
1539   /**
1540    * Unlike normalize, this function does a shallow check to see if
1541    * |baseType| (one of the possible getValueBaseType results) is
1542    * valid for this type. It returns true or false. It's used to fill
1543    * in optional arguments to functions before actually type checking
1544    *
1545    * @param {string} _baseType
1546    */
1547   checkBaseType(_baseType) {
1548     return false;
1549   }
1551   /**
1552    * Helper method that simply relies on checkBaseType to implement
1553    * normalize. Subclasses can choose to use it or not.
1554    */
1555   normalizeBase(type, value, context) {
1556     if (this.checkBaseType(getValueBaseType(value))) {
1557       this.checkDeprecated(context, value);
1558       return { value: this.preprocess(value, context) };
1559     }
1561     let choice;
1562     if ("aeiou".includes(type[0])) {
1563       choice = `be an ${type} value`;
1564     } else {
1565       choice = `be a ${type} value`;
1566     }
1568     return context.error(
1569       () => `Expected ${type} instead of ${JSON.stringify(value)}`,
1570       choice
1571     );
1572   }
1575 // Type that allows any value.
1576 class AnyType extends Type {
1577   normalize(value, context) {
1578     this.checkDeprecated(context, value);
1579     return this.postprocess({ value }, context);
1580   }
1582   checkBaseType() {
1583     return true;
1584   }
1587 // An untagged union type.
1588 class ChoiceType extends Type {
1589   static get EXTRA_PROPERTIES() {
1590     return ["choices", ...super.EXTRA_PROPERTIES];
1591   }
1593   /** @type {(root, schema, path, extraProperties?: Iterable) => ChoiceType} */
1594   static parseSchema(root, schema, path, extraProperties = []) {
1595     this.checkSchemaProperties(schema, path, extraProperties);
1597     let choices = schema.choices.map(t => root.parseSchema(t, path));
1598     return new this(schema, choices);
1599   }
1601   constructor(schema, choices) {
1602     super(schema);
1603     this.choices = choices;
1604   }
1606   extend(type) {
1607     this.choices.push(...type.choices);
1609     return this;
1610   }
1612   normalize(value, context) {
1613     this.checkDeprecated(context, value);
1615     let error;
1616     let { choices, result } = context.withChoices(() => {
1617       for (let choice of this.choices) {
1618         // Ignore a possible choice if it is not supported by
1619         // the manifest version we are normalizing.
1620         if (!context.matchManifestVersion(choice)) {
1621           continue;
1622         }
1624         let r = choice.normalize(value, context);
1625         if (!r.error) {
1626           return r;
1627         }
1629         error = r;
1630       }
1631     });
1633     if (result) {
1634       return result;
1635     }
1636     if (choices.size <= 1) {
1637       return error;
1638     }
1640     choices = Array.from(choices, forceString);
1641     let n = choices.length - 1;
1642     choices[n] = `or ${choices[n]}`;
1644     let message;
1645     if (typeof value === "object") {
1646       message = () => `Value must either: ${choices.join(", ")}`;
1647     } else {
1648       message = () =>
1649         `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`;
1650     }
1652     return context.error(message, null);
1653   }
1655   checkBaseType(baseType) {
1656     return this.choices.some(t => t.checkBaseType(baseType));
1657   }
1659   getDescriptor(path, context) {
1660     // In StringType.getDescriptor, unlike any other Type, a descriptor is returned if
1661     // it is an enumeration.  Since we need versioned choices in some cases, here we
1662     // build a list of valid enumerations that will work for a given manifest version.
1663     if (
1664       !this.choices.length ||
1665       !this.choices.every(t => t.checkBaseType("string") && t.enumeration)
1666     ) {
1667       return;
1668     }
1670     let obj = Cu.createObjectIn(context.cloneScope);
1671     let descriptor = { value: obj };
1672     for (let choice of this.choices) {
1673       // Ignore a possible choice if it is not supported by
1674       // the manifest version we are normalizing.
1675       if (!context.matchManifestVersion(choice)) {
1676         continue;
1677       }
1678       let d = choice.getDescriptor(path, context);
1679       if (d) {
1680         Object.assign(obj, d.descriptor.value);
1681       }
1682     }
1684     return { descriptor };
1685   }
1688 // This is a reference to another type--essentially a typedef.
1689 class RefType extends Type {
1690   static get EXTRA_PROPERTIES() {
1691     return ["$ref", ...super.EXTRA_PROPERTIES];
1692   }
1694   /** @type {(root, schema, path, extraProperties?: Iterable) => RefType} */
1695   static parseSchema(root, schema, path, extraProperties = []) {
1696     this.checkSchemaProperties(schema, path, extraProperties);
1698     let ref = schema.$ref;
1699     let ns = path.join(".");
1700     if (ref.includes(".")) {
1701       [, ns, ref] = /^(.*)\.(.*?)$/.exec(ref);
1702     }
1703     return new this(root, schema, ns, ref);
1704   }
1706   // For a reference to a type named T declared in namespace NS,
1707   // namespaceName will be NS and reference will be T.
1708   constructor(root, schema, namespaceName, reference) {
1709     super(schema);
1710     this.root = root;
1711     this.namespaceName = namespaceName;
1712     this.reference = reference;
1713   }
1715   get targetType() {
1716     let ns = this.root.getNamespace(this.namespaceName);
1717     let type = ns.get(this.reference);
1718     if (!type) {
1719       throw new Error(`Internal error: Type ${this.reference} not found`);
1720     }
1721     return type;
1722   }
1724   normalize(value, context) {
1725     this.checkDeprecated(context, value);
1726     return this.targetType.normalize(value, context);
1727   }
1729   checkBaseType(baseType) {
1730     return this.targetType.checkBaseType(baseType);
1731   }
1734 class StringType extends Type {
1735   static get EXTRA_PROPERTIES() {
1736     return [
1737       "enum",
1738       "minLength",
1739       "maxLength",
1740       "pattern",
1741       "format",
1742       ...super.EXTRA_PROPERTIES,
1743     ];
1744   }
1746   static parseSchema(root, schema, path, extraProperties = []) {
1747     this.checkSchemaProperties(schema, path, extraProperties);
1749     let enumeration = schema.enum || null;
1750     if (enumeration) {
1751       // The "enum" property is either a list of strings that are
1752       // valid values or else a list of {name, description} objects,
1753       // where the .name values are the valid values.
1754       enumeration = enumeration.map(e => {
1755         if (typeof e == "object") {
1756           return e.name;
1757         }
1758         return e;
1759       });
1760     }
1762     let pattern = null;
1763     if (schema.pattern) {
1764       try {
1765         pattern = parsePattern(schema.pattern);
1766       } catch (e) {
1767         throw new Error(
1768           `Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}`
1769         );
1770       }
1771     }
1773     let format = null;
1774     if (schema.format) {
1775       if (!(schema.format in FORMATS)) {
1776         throw new Error(
1777           `Internal error: Invalid string format ${schema.format}`
1778         );
1779       }
1780       format = FORMATS[schema.format];
1781     }
1782     return new this(
1783       schema,
1784       schema.id || undefined,
1785       enumeration,
1786       schema.minLength || 0,
1787       schema.maxLength || Infinity,
1788       pattern,
1789       format
1790     );
1791   }
1793   constructor(
1794     schema,
1795     name,
1796     enumeration,
1797     minLength,
1798     maxLength,
1799     pattern,
1800     format
1801   ) {
1802     super(schema);
1803     this.name = name;
1804     this.enumeration = enumeration;
1805     this.minLength = minLength;
1806     this.maxLength = maxLength;
1807     this.pattern = pattern;
1808     this.format = format;
1809   }
1811   normalize(value, context) {
1812     let r = this.normalizeBase("string", value, context);
1813     if (r.error) {
1814       return r;
1815     }
1816     value = r.value;
1818     if (this.enumeration) {
1819       if (this.enumeration.includes(value)) {
1820         return this.postprocess({ value }, context);
1821       }
1823       let choices = this.enumeration.map(JSON.stringify).join(", ");
1825       return context.error(
1826         () => `Invalid enumeration value ${JSON.stringify(value)}`,
1827         `be one of [${choices}]`
1828       );
1829     }
1831     if (value.length < this.minLength) {
1832       return context.error(
1833         () =>
1834           `String ${JSON.stringify(value)} is too short (must be ${
1835             this.minLength
1836           })`,
1837         `be longer than ${this.minLength}`
1838       );
1839     }
1840     if (value.length > this.maxLength) {
1841       return context.error(
1842         () =>
1843           `String ${JSON.stringify(value)} is too long (must be ${
1844             this.maxLength
1845           })`,
1846         `be shorter than ${this.maxLength}`
1847       );
1848     }
1850     if (this.pattern && !this.pattern.test(value)) {
1851       return context.error(
1852         () => `String ${JSON.stringify(value)} must match ${this.pattern}`,
1853         `match the pattern ${this.pattern.toSource()}`
1854       );
1855     }
1857     if (this.format) {
1858       try {
1859         r.value = this.format(r.value, context);
1860       } catch (e) {
1861         return context.error(
1862           String(e),
1863           `match the format "${this.format.name}"`
1864         );
1865       }
1866     }
1868     return r;
1869   }
1871   checkBaseType(baseType) {
1872     return baseType == "string";
1873   }
1875   getDescriptor(path, context) {
1876     if (this.enumeration) {
1877       let obj = Cu.createObjectIn(context.cloneScope);
1879       for (let e of this.enumeration) {
1880         obj[e.toUpperCase()] = e;
1881       }
1883       return {
1884         descriptor: { value: obj },
1885       };
1886     }
1887   }
1890 class NullType extends Type {
1891   normalize(value, context) {
1892     return this.normalizeBase("null", value, context);
1893   }
1895   checkBaseType(baseType) {
1896     return baseType == "null";
1897   }
1900 let FunctionEntry;
1901 let Event;
1902 let SubModuleType;
1904 class ObjectType extends Type {
1905   static get EXTRA_PROPERTIES() {
1906     return [
1907       "properties",
1908       "patternProperties",
1909       "$import",
1910       ...super.EXTRA_PROPERTIES,
1911     ];
1912   }
1914   static parseSchema(root, schema, path, extraProperties = []) {
1915     if ("functions" in schema) {
1916       return SubModuleType.parseSchema(root, schema, path, extraProperties);
1917     }
1919     if (DEBUG && !("$extend" in schema)) {
1920       // Only allow extending "properties" and "patternProperties".
1921       extraProperties = [
1922         "additionalProperties",
1923         "isInstanceOf",
1924         ...extraProperties,
1925       ];
1926     }
1927     this.checkSchemaProperties(schema, path, extraProperties);
1929     let imported = null;
1930     if ("$import" in schema) {
1931       let importPath = schema.$import;
1932       let idx = importPath.indexOf(".");
1933       if (idx === -1) {
1934         imported = [path[0], importPath];
1935       } else {
1936         imported = [importPath.slice(0, idx), importPath.slice(idx + 1)];
1937       }
1938     }
1940     let parseProperty = (schema, extraProps = []) => {
1941       return {
1942         type: root.parseSchema(
1943           schema,
1944           path,
1945           DEBUG && [
1946             "unsupported",
1947             "onError",
1948             "permissions",
1949             "default",
1950             ...extraProps,
1951           ]
1952         ),
1953         optional: schema.optional || false,
1954         unsupported: schema.unsupported || false,
1955         onError: schema.onError || null,
1956         default: schema.default === undefined ? null : schema.default,
1957       };
1958     };
1960     // Parse explicit "properties" object.
1961     let properties = Object.create(null);
1962     for (let propName of Object.keys(schema.properties || {})) {
1963       properties[propName] = parseProperty(schema.properties[propName], [
1964         "optional",
1965       ]);
1966     }
1968     // Parse regexp properties from "patternProperties" object.
1969     let patternProperties = [];
1970     for (let propName of Object.keys(schema.patternProperties || {})) {
1971       let pattern;
1972       try {
1973         pattern = parsePattern(propName);
1974       } catch (e) {
1975         throw new Error(
1976           `Internal error: Invalid property pattern ${JSON.stringify(propName)}`
1977         );
1978       }
1980       patternProperties.push({
1981         pattern,
1982         type: parseProperty(schema.patternProperties[propName]),
1983       });
1984     }
1986     // Parse "additionalProperties" schema.
1987     let additionalProperties = null;
1988     if (schema.additionalProperties) {
1989       let type = schema.additionalProperties;
1990       if (type === true) {
1991         type = { type: "any" };
1992       }
1994       additionalProperties = root.parseSchema(type, path);
1995     }
1997     return new this(
1998       schema,
1999       properties,
2000       additionalProperties,
2001       patternProperties,
2002       schema.isInstanceOf || null,
2003       imported
2004     );
2005   }
2007   constructor(
2008     schema,
2009     properties,
2010     additionalProperties,
2011     patternProperties,
2012     isInstanceOf,
2013     imported
2014   ) {
2015     super(schema);
2016     this.properties = properties;
2017     this.additionalProperties = additionalProperties;
2018     this.patternProperties = patternProperties;
2019     this.isInstanceOf = isInstanceOf;
2021     if (imported) {
2022       let [ns, path] = imported;
2023       ns = Schemas.getNamespace(ns);
2024       let importedType = ns.get(path);
2025       if (!importedType) {
2026         throw new Error(`Internal error: imported type ${path} not found`);
2027       }
2029       if (DEBUG && !(importedType instanceof ObjectType)) {
2030         throw new Error(
2031           `Internal error: cannot import non-object type ${path}`
2032         );
2033       }
2035       this.properties = Object.assign(
2036         {},
2037         importedType.properties,
2038         this.properties
2039       );
2040       this.patternProperties = [
2041         ...importedType.patternProperties,
2042         ...this.patternProperties,
2043       ];
2044       this.additionalProperties =
2045         importedType.additionalProperties || this.additionalProperties;
2046     }
2047   }
2049   extend(type) {
2050     for (let key of Object.keys(type.properties)) {
2051       if (key in this.properties) {
2052         throw new Error(
2053           `InternalError: Attempt to extend an object with conflicting property "${key}"`
2054         );
2055       }
2056       this.properties[key] = type.properties[key];
2057     }
2059     this.patternProperties.push(...type.patternProperties);
2061     return this;
2062   }
2064   checkBaseType(baseType) {
2065     return baseType == "object";
2066   }
2068   /**
2069    * Extracts the enumerable properties of the given object, including
2070    * function properties which would normally be omitted by X-ray
2071    * wrappers.
2072    *
2073    * @param {object} value
2074    * @param {Context} context
2075    *        The current parse context.
2076    * @returns {object}
2077    *        An object with an `error` or `value` property.
2078    */
2079   extractProperties(value, context) {
2080     // |value| should be a JS Xray wrapping an object in the
2081     // extension compartment. This works well except when we need to
2082     // access callable properties on |value| since JS Xrays don't
2083     // support those. To work around the problem, we verify that
2084     // |value| is a plain JS object (i.e., not anything scary like a
2085     // Proxy). Then we copy the properties out of it into a normal
2086     // object using a waiver wrapper.
2088     let klass = ChromeUtils.getClassName(value, true);
2089     if (klass != "Object") {
2090       throw context.error(
2091         `Expected a plain JavaScript object, got a ${klass}`,
2092         `be a plain JavaScript object`
2093       );
2094     }
2096     return ChromeUtils.shallowClone(value);
2097   }
2099   checkProperty(context, prop, propType, result, properties, remainingProps) {
2100     let { type, optional, unsupported, onError } = propType;
2101     let error = null;
2103     if (!context.matchManifestVersion(type)) {
2104       if (prop in properties) {
2105         error = context.error(
2106           `Property "${prop}" is unsupported in Manifest Version ${context.manifestVersion}`,
2107           `not contain an unsupported "${prop}" property`
2108         );
2110         context.logWarning(forceString(error.error));
2111         if (this.additionalProperties) {
2112           // When `additionalProperties` is set to UnrecognizedProperty, the
2113           // caller (i.e. ObjectType's normalize method) assigns the original
2114           // value to `result[prop]`. Erase the property now to prevent
2115           // `result[prop]` from becoming anything other than `undefined.
2116           //
2117           // A warning was already logged above, so we do not need to also log
2118           // "An unexpected property was found in the WebExtension manifest."
2119           remainingProps.delete(prop);
2120         }
2121         // When `additionalProperties` is not set, ObjectType's normalize method
2122         // will return an error because prop is still in remainingProps.
2123         return;
2124       }
2125     } else if (unsupported) {
2126       if (prop in properties) {
2127         error = context.error(
2128           `Property "${prop}" is unsupported by Firefox`,
2129           `not contain an unsupported "${prop}" property`
2130         );
2131       }
2132     } else if (prop in properties) {
2133       if (
2134         optional &&
2135         (properties[prop] === null || properties[prop] === undefined)
2136       ) {
2137         result[prop] = propType.default;
2138       } else {
2139         let r = context.withPath(prop, () =>
2140           type.normalize(properties[prop], context)
2141         );
2142         if (r.error) {
2143           error = r;
2144         } else {
2145           result[prop] = r.value;
2146           properties[prop] = r.value;
2147         }
2148       }
2149       remainingProps.delete(prop);
2150     } else if (!optional) {
2151       error = context.error(
2152         `Property "${prop}" is required`,
2153         `contain the required "${prop}" property`
2154       );
2155     } else if (optional !== "omit-key-if-missing") {
2156       result[prop] = propType.default;
2157     }
2159     if (error) {
2160       if (onError == "warn") {
2161         context.logWarning(forceString(error.error));
2162       } else if (onError != "ignore") {
2163         throw error;
2164       }
2166       result[prop] = propType.default;
2167     }
2168   }
2170   normalize(value, context) {
2171     try {
2172       let v = this.normalizeBase("object", value, context);
2173       if (v.error) {
2174         return v;
2175       }
2176       value = v.value;
2178       if (this.isInstanceOf) {
2179         if (DEBUG) {
2180           if (
2181             Object.keys(this.properties).length ||
2182             this.patternProperties.length ||
2183             !(this.additionalProperties instanceof AnyType)
2184           ) {
2185             throw new Error(
2186               "InternalError: isInstanceOf can only be used " +
2187                 "with objects that are otherwise unrestricted"
2188             );
2189           }
2190         }
2192         if (
2193           ChromeUtils.getClassName(value) !== this.isInstanceOf &&
2194           (this.isInstanceOf !== "Element" || value.nodeType !== 1)
2195         ) {
2196           return context.error(
2197             `Object must be an instance of ${this.isInstanceOf}`,
2198             `be an instance of ${this.isInstanceOf}`
2199           );
2200         }
2202         // This is kind of a hack, but we can't normalize things that
2203         // aren't JSON, so we just return them.
2204         return this.postprocess({ value }, context);
2205       }
2207       let properties = this.extractProperties(value, context);
2208       let remainingProps = new Set(Object.keys(properties));
2210       let result = {};
2211       for (let prop of Object.keys(this.properties)) {
2212         this.checkProperty(
2213           context,
2214           prop,
2215           this.properties[prop],
2216           result,
2217           properties,
2218           remainingProps
2219         );
2220       }
2222       for (let prop of Object.keys(properties)) {
2223         for (let { pattern, type } of this.patternProperties) {
2224           if (pattern.test(prop)) {
2225             this.checkProperty(
2226               context,
2227               prop,
2228               type,
2229               result,
2230               properties,
2231               remainingProps
2232             );
2233           }
2234         }
2235       }
2237       if (this.additionalProperties) {
2238         for (let prop of remainingProps) {
2239           let r = context.withPath(prop, () =>
2240             this.additionalProperties.normalize(properties[prop], context)
2241           );
2242           if (r.error) {
2243             return r;
2244           }
2245           result[prop] = r.value;
2246         }
2247       } else if (remainingProps.size && !context.ignoreUnrecognizedProperties) {
2248         if (remainingProps.size == 1) {
2249           return context.error(
2250             `Unexpected property "${[...remainingProps]}"`,
2251             `not contain an unexpected "${[...remainingProps]}" property`
2252           );
2253         } else if (remainingProps.size) {
2254           let props = [...remainingProps].sort().join(", ");
2255           return context.error(
2256             `Unexpected properties: ${props}`,
2257             `not contain the unexpected properties [${props}]`
2258           );
2259         }
2260       }
2262       return this.postprocess({ value: result }, context);
2263     } catch (e) {
2264       if (e.error) {
2265         return e;
2266       }
2267       throw e;
2268     }
2269   }
2272 // This type is just a placeholder to be referred to by
2273 // SubModuleProperty. No value is ever expected to have this type.
2274 SubModuleType = class SubModuleType extends Type {
2275   static get EXTRA_PROPERTIES() {
2276     return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
2277   }
2279   static parseSchema(root, schema, path, extraProperties = []) {
2280     this.checkSchemaProperties(schema, path, extraProperties);
2282     // The path we pass in here is only used for error messages.
2283     path = [...path, schema.id];
2284     let functions = schema.functions
2285       .filter(fun => !fun.unsupported)
2286       .map(fun => FunctionEntry.parseSchema(root, fun, path));
2288     let events = [];
2290     if (schema.events) {
2291       events = schema.events
2292         .filter(event => !event.unsupported)
2293         .map(event => Event.parseSchema(root, event, path));
2294     }
2296     return new this(schema, functions, events);
2297   }
2299   constructor(schema, functions, events) {
2300     // schema contains properties such as min/max_manifest_version needed
2301     // in the base class so that the Context class can version compare
2302     // any entries against the manifest version.
2303     super(schema);
2304     this.functions = functions;
2305     this.events = events;
2306   }
2309 class NumberType extends Type {
2310   normalize(value, context) {
2311     let r = this.normalizeBase("number", value, context);
2312     if (r.error) {
2313       return r;
2314     }
2316     if (isNaN(r.value) || !Number.isFinite(r.value)) {
2317       return context.error(
2318         "NaN and infinity are not valid",
2319         "be a finite number"
2320       );
2321     }
2323     return r;
2324   }
2326   checkBaseType(baseType) {
2327     return baseType == "number" || baseType == "integer";
2328   }
2331 class IntegerType extends Type {
2332   static get EXTRA_PROPERTIES() {
2333     return ["minimum", "maximum", ...super.EXTRA_PROPERTIES];
2334   }
2336   static parseSchema(root, schema, path, extraProperties = []) {
2337     this.checkSchemaProperties(schema, path, extraProperties);
2339     let { minimum = -Infinity, maximum = Infinity } = schema;
2340     return new this(schema, minimum, maximum);
2341   }
2343   constructor(schema, minimum, maximum) {
2344     super(schema);
2345     this.minimum = minimum;
2346     this.maximum = maximum;
2347   }
2349   normalize(value, context) {
2350     let r = this.normalizeBase("integer", value, context);
2351     if (r.error) {
2352       return r;
2353     }
2354     value = r.value;
2356     // Ensure it's between -2**31 and 2**31-1
2357     if (!Number.isSafeInteger(value)) {
2358       return context.error(
2359         "Integer is out of range",
2360         "be a valid 32 bit signed integer"
2361       );
2362     }
2364     if (value < this.minimum) {
2365       return context.error(
2366         `Integer ${value} is too small (must be at least ${this.minimum})`,
2367         `be at least ${this.minimum}`
2368       );
2369     }
2370     if (value > this.maximum) {
2371       return context.error(
2372         `Integer ${value} is too big (must be at most ${this.maximum})`,
2373         `be no greater than ${this.maximum}`
2374       );
2375     }
2377     return this.postprocess(r, context);
2378   }
2380   checkBaseType(baseType) {
2381     return baseType == "integer";
2382   }
2385 class BooleanType extends Type {
2386   static get EXTRA_PROPERTIES() {
2387     return ["enum", ...super.EXTRA_PROPERTIES];
2388   }
2390   static parseSchema(root, schema, path, extraProperties = []) {
2391     this.checkSchemaProperties(schema, path, extraProperties);
2392     let enumeration = schema.enum || null;
2393     return new this(schema, enumeration);
2394   }
2396   constructor(schema, enumeration) {
2397     super(schema);
2398     this.enumeration = enumeration;
2399   }
2401   normalize(value, context) {
2402     if (!this.checkBaseType(getValueBaseType(value))) {
2403       return context.error(
2404         () => `Expected boolean instead of ${JSON.stringify(value)}`,
2405         `be a boolean`
2406       );
2407     }
2408     value = this.preprocess(value, context);
2409     if (this.enumeration && !this.enumeration.includes(value)) {
2410       return context.error(
2411         () => `Invalid value ${JSON.stringify(value)}`,
2412         `be ${this.enumeration}`
2413       );
2414     }
2415     this.checkDeprecated(context, value);
2416     return { value };
2417   }
2419   checkBaseType(baseType) {
2420     return baseType == "boolean";
2421   }
2424 class ArrayType extends Type {
2425   static get EXTRA_PROPERTIES() {
2426     return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
2427   }
2429   static parseSchema(root, schema, path, extraProperties = []) {
2430     this.checkSchemaProperties(schema, path, extraProperties);
2432     let items = root.parseSchema(schema.items, path, ["onError"]);
2434     return new this(
2435       schema,
2436       items,
2437       schema.minItems || 0,
2438       schema.maxItems || Infinity
2439     );
2440   }
2442   constructor(schema, itemType, minItems, maxItems) {
2443     super(schema);
2444     this.itemType = itemType;
2445     this.minItems = minItems;
2446     this.maxItems = maxItems;
2447     this.onError = schema.items.onError || null;
2448   }
2450   normalize(value, context) {
2451     let v = this.normalizeBase("array", value, context);
2452     if (v.error) {
2453       return v;
2454     }
2455     value = v.value;
2457     let result = [];
2458     for (let [i, element] of value.entries()) {
2459       element = context.withPath(String(i), () =>
2460         this.itemType.normalize(element, context)
2461       );
2462       if (element.error) {
2463         if (this.onError == "warn") {
2464           context.logWarning(forceString(element.error));
2465         } else if (this.onError != "ignore") {
2466           return element;
2467         }
2468         continue;
2469       }
2470       result.push(element.value);
2471     }
2473     if (result.length < this.minItems) {
2474       return context.error(
2475         `Array requires at least ${this.minItems} items; you have ${result.length}`,
2476         `have at least ${this.minItems} items`
2477       );
2478     }
2480     if (result.length > this.maxItems) {
2481       return context.error(
2482         `Array requires at most ${this.maxItems} items; you have ${result.length}`,
2483         `have at most ${this.maxItems} items`
2484       );
2485     }
2487     return this.postprocess({ value: result }, context);
2488   }
2490   checkBaseType(baseType) {
2491     return baseType == "array";
2492   }
2495 class FunctionType extends Type {
2496   static get EXTRA_PROPERTIES() {
2497     return [
2498       "parameters",
2499       "async",
2500       "returns",
2501       "requireUserInput",
2502       ...super.EXTRA_PROPERTIES,
2503     ];
2504   }
2506   static parseSchema(root, schema, path, extraProperties = []) {
2507     this.checkSchemaProperties(schema, path, extraProperties);
2509     let isAsync = !!schema.async;
2510     let isExpectingCallback = typeof schema.async === "string";
2511     let parameters = null;
2512     if ("parameters" in schema) {
2513       parameters = [];
2514       for (let param of schema.parameters) {
2515         // Callbacks default to optional for now, because of promise
2516         // handling.
2517         let isCallback = isAsync && param.name == schema.async;
2518         if (isCallback) {
2519           isExpectingCallback = false;
2520         }
2522         parameters.push({
2523           type: root.parseSchema(param, path, ["name", "optional", "default"]),
2524           name: param.name,
2525           optional: param.optional == null ? isCallback : param.optional,
2526           default: param.default == undefined ? null : param.default,
2527         });
2528       }
2529     }
2530     let hasAsyncCallback = false;
2531     if (isAsync) {
2532       hasAsyncCallback =
2533         parameters &&
2534         parameters.length &&
2535         parameters[parameters.length - 1].name == schema.async;
2536     }
2538     if (DEBUG) {
2539       if (isExpectingCallback) {
2540         throw new Error(
2541           `Internal error: Expected a callback parameter ` +
2542             `with name ${schema.async}`
2543         );
2544       }
2546       if (isAsync && schema.returns) {
2547         throw new Error(
2548           "Internal error: Async functions must not have return values."
2549         );
2550       }
2551       if (
2552         isAsync &&
2553         schema.allowAmbiguousOptionalArguments &&
2554         !hasAsyncCallback
2555       ) {
2556         throw new Error(
2557           "Internal error: Async functions with ambiguous " +
2558             "arguments must declare the callback as the last parameter"
2559         );
2560       }
2561     }
2563     return new this(
2564       schema,
2565       parameters,
2566       isAsync,
2567       hasAsyncCallback,
2568       !!schema.requireUserInput
2569     );
2570   }
2572   constructor(schema, parameters, isAsync, hasAsyncCallback, requireUserInput) {
2573     super(schema);
2574     this.parameters = parameters;
2575     this.isAsync = isAsync;
2576     this.hasAsyncCallback = hasAsyncCallback;
2577     this.requireUserInput = requireUserInput;
2578   }
2580   normalize(value, context) {
2581     return this.normalizeBase("function", value, context);
2582   }
2584   checkBaseType(baseType) {
2585     return baseType == "function";
2586   }
2589 // Represents a "property" defined in a schema namespace with a
2590 // particular value. Essentially this is a constant.
2591 class ValueProperty extends Entry {
2592   constructor(schema, name, value) {
2593     super(schema);
2594     this.name = name;
2595     this.value = value;
2596   }
2598   getDescriptor(path, context) {
2599     // Prevent injection if not a supported version.
2600     if (!context.matchManifestVersion(this)) {
2601       return;
2602     }
2604     return {
2605       descriptor: { value: this.value },
2606     };
2607   }
2610 // Represents a "property" defined in a schema namespace that is not a
2611 // constant.
2612 class TypeProperty extends Entry {
2613   unsupported = false;
2615   constructor(schema, path, name, type, writable, permissions) {
2616     super(schema);
2617     this.path = path;
2618     this.name = name;
2619     this.type = type;
2620     this.writable = writable;
2621     this.permissions = permissions;
2622   }
2624   throwError(context, msg) {
2625     throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2626   }
2628   getDescriptor(path, context) {
2629     if (this.unsupported || !context.matchManifestVersion(this)) {
2630       return;
2631     }
2633     let apiImpl = context.getImplementation(path.join("."), this.name);
2635     let getStub = () => {
2636       this.checkDeprecated(context);
2637       return apiImpl.getProperty();
2638     };
2640     let descriptor = {
2641       get: Cu.exportFunction(getStub, context.cloneScope),
2642     };
2644     if (this.writable) {
2645       let setStub = value => {
2646         let normalized = this.type.normalize(value, context);
2647         if (normalized.error) {
2648           this.throwError(context, forceString(normalized.error));
2649         }
2651         apiImpl.setProperty(normalized.value);
2652       };
2654       descriptor.set = Cu.exportFunction(setStub, context.cloneScope);
2655     }
2657     return {
2658       descriptor,
2659       revoke() {
2660         apiImpl.revoke();
2661         apiImpl = null;
2662       },
2663     };
2664   }
2667 class SubModuleProperty extends Entry {
2668   // A SubModuleProperty represents a tree of objects and properties
2669   // to expose to an extension. Currently we support only a limited
2670   // form of sub-module properties, where "$ref" points to a
2671   // SubModuleType containing a list of functions and "properties" is
2672   // a list of additional simple properties.
2673   //
2674   // name: Name of the property stuff is being added to.
2675   // namespaceName: Namespace in which the property lives.
2676   // reference: Name of the type defining the functions to add to the property.
2677   // properties: Additional properties to add to the module (unsupported).
2678   constructor(root, schema, path, name, reference, properties, permissions) {
2679     super(schema);
2680     this.root = root;
2681     this.name = name;
2682     this.path = path;
2683     this.namespaceName = path.join(".");
2684     this.reference = reference;
2685     this.properties = properties;
2686     this.permissions = permissions;
2687   }
2689   get targetType() {
2690     let ns = this.root.getNamespace(this.namespaceName);
2691     let type = ns.get(this.reference);
2692     if (!type && this.reference.includes(".")) {
2693       let [namespaceName, ref] = this.reference.split(".");
2694       ns = this.root.getNamespace(namespaceName);
2695       type = ns.get(ref);
2696     }
2697     return type;
2698   }
2700   getDescriptor(path, context) {
2701     let obj = Cu.createObjectIn(context.cloneScope);
2703     let ns = this.root.getNamespace(this.namespaceName);
2704     let type = this.targetType;
2706     // Prevent injection if not a supported version.
2707     if (!context.matchManifestVersion(type)) {
2708       return;
2709     }
2711     if (DEBUG) {
2712       if (!type || !(type instanceof SubModuleType)) {
2713         throw new Error(
2714           `Internal error: ${this.namespaceName}.${this.reference} ` +
2715             `is not a sub-module`
2716         );
2717       }
2718     }
2719     let subpath = [...path, this.name];
2721     let functions = type.functions;
2722     for (let fun of functions) {
2723       context.injectInto(fun, obj, fun.name, subpath, ns);
2724     }
2726     let events = type.events;
2727     for (let event of events) {
2728       context.injectInto(event, obj, event.name, subpath, ns);
2729     }
2731     // TODO: Inject this.properties.
2733     return {
2734       descriptor: { value: obj },
2735       revoke() {
2736         let unwrapped = ChromeUtils.waiveXrays(obj);
2737         for (let fun of functions) {
2738           try {
2739             delete unwrapped[fun.name];
2740           } catch (e) {
2741             Cu.reportError(e);
2742           }
2743         }
2744       },
2745     };
2746   }
2749 // This class is a base class for FunctionEntrys and Events. It takes
2750 // care of validating parameter lists (i.e., handling of optional
2751 // parameters and parameter type checking).
2752 class CallEntry extends Entry {
2753   hasAsyncCallback = false;
2755   constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) {
2756     super(schema);
2757     this.path = path;
2758     this.name = name;
2759     this.parameters = parameters;
2760     this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments;
2761   }
2763   throwError(context, msg) {
2764     throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2765   }
2767   checkParameters(args, context) {
2768     let fixedArgs = [];
2770     // First we create a new array, fixedArgs, that is the same as
2771     // |args| but with default values in place of omitted optional parameters.
2772     let check = (parameterIndex, argIndex) => {
2773       if (parameterIndex == this.parameters.length) {
2774         if (argIndex == args.length) {
2775           return true;
2776         }
2777         return false;
2778       }
2780       let parameter = this.parameters[parameterIndex];
2781       if (parameter.optional) {
2782         // Try skipping it.
2783         fixedArgs[parameterIndex] = parameter.default;
2784         if (check(parameterIndex + 1, argIndex)) {
2785           return true;
2786         }
2787       }
2789       if (argIndex == args.length) {
2790         return false;
2791       }
2793       let arg = args[argIndex];
2794       if (!parameter.type.checkBaseType(getValueBaseType(arg))) {
2795         // For Chrome compatibility, use the default value if null or undefined
2796         // is explicitly passed but is not a valid argument in this position.
2797         if (parameter.optional && (arg === null || arg === undefined)) {
2798           fixedArgs[parameterIndex] = Cu.cloneInto(parameter.default, {});
2799         } else {
2800           return false;
2801         }
2802       } else {
2803         fixedArgs[parameterIndex] = arg;
2804       }
2806       return check(parameterIndex + 1, argIndex + 1);
2807     };
2809     if (this.allowAmbiguousOptionalArguments) {
2810       // When this option is set, it's up to the implementation to
2811       // parse arguments.
2812       // The last argument for asynchronous methods is either a function or null.
2813       // This is specifically done for runtime.sendMessage.
2814       if (this.hasAsyncCallback && typeof args[args.length - 1] != "function") {
2815         args.push(null);
2816       }
2817       return args;
2818     }
2819     let success = check(0, 0);
2820     if (!success) {
2821       this.throwError(context, "Incorrect argument types");
2822     }
2824     // Now we normalize (and fully type check) all non-omitted arguments.
2825     fixedArgs = fixedArgs.map((arg, parameterIndex) => {
2826       if (arg === null) {
2827         return null;
2828       }
2829       let parameter = this.parameters[parameterIndex];
2830       let r = parameter.type.normalize(arg, context);
2831       if (r.error) {
2832         this.throwError(
2833           context,
2834           `Type error for parameter ${parameter.name} (${forceString(r.error)})`
2835         );
2836       }
2837       return r.value;
2838     });
2840     return fixedArgs;
2841   }
2844 // Represents a "function" defined in a schema namespace.
2845 FunctionEntry = class FunctionEntry extends CallEntry {
2846   static parseSchema(root, schema, path) {
2847     // When not in DEBUG mode, we just need to know *if* this returns.
2848     /** @type {boolean|object} */
2849     let returns = !!schema.returns;
2850     if (DEBUG && "returns" in schema) {
2851       returns = {
2852         type: root.parseSchema(schema.returns, path, ["optional", "name"]),
2853         optional: schema.returns.optional || false,
2854         name: "result",
2855       };
2856     }
2858     return new this(
2859       schema,
2860       path,
2861       schema.name,
2862       root.parseSchema(schema, path, [
2863         "name",
2864         "unsupported",
2865         "returns",
2866         "permissions",
2867         "allowAmbiguousOptionalArguments",
2868         "allowCrossOriginArguments",
2869       ]),
2870       schema.unsupported || false,
2871       schema.allowAmbiguousOptionalArguments || false,
2872       schema.allowCrossOriginArguments || false,
2873       returns,
2874       schema.permissions || null
2875     );
2876   }
2878   constructor(
2879     schema,
2880     path,
2881     name,
2882     type,
2883     unsupported,
2884     allowAmbiguousOptionalArguments,
2885     allowCrossOriginArguments,
2886     returns,
2887     permissions
2888   ) {
2889     super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
2890     this.unsupported = unsupported;
2891     this.returns = returns;
2892     this.permissions = permissions;
2893     this.allowCrossOriginArguments = allowCrossOriginArguments;
2895     this.isAsync = type.isAsync;
2896     this.hasAsyncCallback = type.hasAsyncCallback;
2897     this.requireUserInput = type.requireUserInput;
2898   }
2900   checkValue({ type, optional, name }, value, context) {
2901     if (optional && value == null) {
2902       return;
2903     }
2904     if (
2905       type.reference === "ExtensionPanel" ||
2906       type.reference === "ExtensionSidebarPane" ||
2907       type.reference === "Port"
2908     ) {
2909       // TODO: We currently treat objects with functions as SubModuleType,
2910       // which is just wrong, and a bigger yak.  Skipping for now.
2911       return;
2912     }
2913     const { error } = type.normalize(value, context);
2914     if (error) {
2915       this.throwError(
2916         context,
2917         `Type error for ${name} value (${forceString(error)})`
2918       );
2919     }
2920   }
2922   checkCallback(args, context) {
2923     const callback = this.parameters[this.parameters.length - 1];
2924     for (const [i, param] of callback.type.parameters.entries()) {
2925       this.checkValue(param, args[i], context);
2926     }
2927   }
2929   getDescriptor(path, context) {
2930     let apiImpl = context.getImplementation(path.join("."), this.name);
2932     let stub;
2933     if (this.isAsync) {
2934       stub = (...args) => {
2935         this.checkDeprecated(context);
2936         let actuals = this.checkParameters(args, context);
2937         let callback = null;
2938         if (this.hasAsyncCallback) {
2939           callback = actuals.pop();
2940         }
2941         if (callback === null && context.isChromeCompat) {
2942           // We pass an empty stub function as a default callback for
2943           // the `chrome` API, so promise objects are not returned,
2944           // and lastError values are reported immediately.
2945           callback = () => {};
2946         }
2947         if (DEBUG && this.hasAsyncCallback && callback) {
2948           let original = callback;
2949           callback = (...args) => {
2950             this.checkCallback(args, context);
2951             original(...args);
2952           };
2953         }
2954         let result = apiImpl.callAsyncFunction(
2955           actuals,
2956           callback,
2957           this.requireUserInput
2958         );
2959         if (DEBUG && this.hasAsyncCallback && !callback) {
2960           return result.then(result => {
2961             this.checkCallback([result], context);
2962             return result;
2963           });
2964         }
2965         return result;
2966       };
2967     } else if (!this.returns) {
2968       stub = (...args) => {
2969         this.checkDeprecated(context);
2970         let actuals = this.checkParameters(args, context);
2971         return apiImpl.callFunctionNoReturn(actuals);
2972       };
2973     } else {
2974       stub = (...args) => {
2975         this.checkDeprecated(context);
2976         let actuals = this.checkParameters(args, context);
2977         let result = apiImpl.callFunction(actuals);
2978         if (DEBUG && this.returns) {
2979           this.checkValue(this.returns, result, context);
2980         }
2981         return result;
2982       };
2983     }
2985     return {
2986       descriptor: {
2987         value: Cu.exportFunction(stub, context.cloneScope, {
2988           allowCrossOriginArguments: this.allowCrossOriginArguments,
2989         }),
2990       },
2991       revoke() {
2992         apiImpl.revoke();
2993         apiImpl = null;
2994       },
2995     };
2996   }
2999 // Represents an "event" defined in a schema namespace.
3001 // TODO Bug 1369722: we should be able to remove the eslint-disable-line that follows
3002 // once Bug 1369722 has been fixed.
3003 // eslint-disable-next-line no-global-assign
3004 Event = class Event extends CallEntry {
3005   static parseSchema(root, event, path) {
3006     let extraParameters = Array.from(event.extraParameters || [], param => ({
3007       type: root.parseSchema(param, path, ["name", "optional", "default"]),
3008       name: param.name,
3009       optional: param.optional || false,
3010       default: param.default == undefined ? null : param.default,
3011     }));
3013     let extraProperties = [
3014       "name",
3015       "unsupported",
3016       "permissions",
3017       "extraParameters",
3018       // We ignore these properties for now.
3019       "returns",
3020       "filters",
3021     ];
3023     return new this(
3024       event,
3025       path,
3026       event.name,
3027       root.parseSchema(event, path, extraProperties),
3028       extraParameters,
3029       event.unsupported || false,
3030       event.permissions || null
3031     );
3032   }
3034   constructor(
3035     schema,
3036     path,
3037     name,
3038     type,
3039     extraParameters,
3040     unsupported,
3041     permissions
3042   ) {
3043     super(schema, path, name, extraParameters);
3044     this.type = type;
3045     this.unsupported = unsupported;
3046     this.permissions = permissions;
3047   }
3049   checkListener(listener, context) {
3050     let r = this.type.normalize(listener, context);
3051     if (r.error) {
3052       this.throwError(context, "Invalid listener");
3053     }
3054     return r.value;
3055   }
3057   getDescriptor(path, context) {
3058     let apiImpl = context.getImplementation(path.join("."), this.name);
3060     let addStub = (listener, ...args) => {
3061       listener = this.checkListener(listener, context);
3062       let actuals = this.checkParameters(args, context);
3063       apiImpl.addListener(listener, actuals);
3064     };
3066     let removeStub = listener => {
3067       listener = this.checkListener(listener, context);
3068       apiImpl.removeListener(listener);
3069     };
3071     let hasStub = listener => {
3072       listener = this.checkListener(listener, context);
3073       return apiImpl.hasListener(listener);
3074     };
3076     let obj = Cu.createObjectIn(context.cloneScope);
3078     Cu.exportFunction(addStub, obj, { defineAs: "addListener" });
3079     Cu.exportFunction(removeStub, obj, { defineAs: "removeListener" });
3080     Cu.exportFunction(hasStub, obj, { defineAs: "hasListener" });
3082     return {
3083       descriptor: { value: obj },
3084       revoke() {
3085         apiImpl.revoke();
3086         apiImpl = null;
3088         let unwrapped = ChromeUtils.waiveXrays(obj);
3089         delete unwrapped.addListener;
3090         delete unwrapped.removeListener;
3091         delete unwrapped.hasListener;
3092       },
3093     };
3094   }
3097 const TYPES = Object.freeze(
3098   Object.assign(Object.create(null), {
3099     any: AnyType,
3100     array: ArrayType,
3101     boolean: BooleanType,
3102     function: FunctionType,
3103     integer: IntegerType,
3104     null: NullType,
3105     number: NumberType,
3106     object: ObjectType,
3107     string: StringType,
3108   })
3111 const LOADERS = {
3112   events: "loadEvent",
3113   functions: "loadFunction",
3114   properties: "loadProperty",
3115   types: "loadType",
3118 class Namespace extends Map {
3119   constructor(root, name, path) {
3120     super();
3122     this.root = root;
3124     this._lazySchemas = [];
3125     this.initialized = false;
3127     this.name = name;
3128     this.path = name ? [...path, name] : [...path];
3130     this.superNamespace = null;
3132     this.min_manifest_version = MIN_MANIFEST_VERSION;
3133     this.max_manifest_version = MAX_MANIFEST_VERSION;
3135     this.permissions = null;
3136     this.allowedContexts = [];
3137     this.defaultContexts = [];
3138   }
3140   /**
3141    * Adds a JSON Schema object to the set of schemas that represent this
3142    * namespace.
3143    *
3144    * @param {object} schema
3145    *        A JSON schema object which partially describes this
3146    *        namespace.
3147    */
3148   addSchema(schema) {
3149     this._lazySchemas.push(schema);
3151     for (let prop of [
3152       "permissions",
3153       "allowedContexts",
3154       "defaultContexts",
3155       "min_manifest_version",
3156       "max_manifest_version",
3157     ]) {
3158       if (schema[prop]) {
3159         this[prop] = schema[prop];
3160       }
3161     }
3163     if (schema.$import) {
3164       this.superNamespace = this.root.getNamespace(schema.$import);
3165     }
3166   }
3168   /**
3169    * Initializes the keys of this namespace based on the schema objects
3170    * added via previous `addSchema` calls.
3171    */
3172   init() {
3173     if (this.initialized) {
3174       return;
3175     }
3177     if (this.superNamespace) {
3178       this._lazySchemas.unshift(...this.superNamespace._lazySchemas);
3179     }
3181     // Keep in sync with LOADERS above.
3182     this.types = new DefaultMap(() => []);
3183     this.properties = new DefaultMap(() => []);
3184     this.functions = new DefaultMap(() => []);
3185     this.events = new DefaultMap(() => []);
3187     for (let schema of this._lazySchemas) {
3188       for (let type of schema.types || []) {
3189         if (!type.unsupported) {
3190           this.types.get(type.$extend || type.id).push(type);
3191         }
3192       }
3194       for (let [name, prop] of Object.entries(schema.properties || {})) {
3195         if (!prop.unsupported) {
3196           this.properties.get(name).push(prop);
3197         }
3198       }
3200       for (let fun of schema.functions || []) {
3201         if (!fun.unsupported) {
3202           this.functions.get(fun.name).push(fun);
3203         }
3204       }
3206       for (let event of schema.events || []) {
3207         if (!event.unsupported) {
3208           this.events.get(event.name).push(event);
3209         }
3210       }
3211     }
3213     // For each type of top-level property in the schema object, iterate
3214     // over all properties of that type, and create a temporary key for
3215     // each property pointing to its type. Those temporary properties
3216     // are later used to instantiate an Entry object based on the actual
3217     // schema object.
3218     for (let type of Object.keys(LOADERS)) {
3219       for (let key of this[type].keys()) {
3220         this.set(key, type);
3221       }
3222     }
3224     this.initialized = true;
3226     if (DEBUG) {
3227       for (let key of this.keys()) {
3228         // Force initialization of all lazy keys to catch unexpected errors.
3229         this.get(key);
3230       }
3231       this.#verifyFallbackEntries();
3232     }
3233   }
3235   /**
3236    * Verify that multiple definitions via fallback entries (currently only
3237    * supported for functions and events) are defined for mutually exclusive
3238    * manifest versions.
3239    */
3240   #verifyFallbackEntries() {
3241     for (
3242       let manifestVersion = MIN_MANIFEST_VERSION;
3243       manifestVersion <= MAX_MANIFEST_VERSION;
3244       manifestVersion++
3245     ) {
3246       for (let key of this.keys()) {
3247         let hasMatch = false;
3248         let entry = this.get(key);
3249         do {
3250           let isMatch =
3251             manifestVersion >= entry.min_manifest_version &&
3252             manifestVersion <= entry.max_manifest_version;
3253           if (isMatch && hasMatch) {
3254             throw new Error(
3255               `Namespace ${this.path.join(".")} has ` +
3256                 `multiple definitions for ${key} ` +
3257                 `for manifest version ${manifestVersion}`
3258             );
3259           }
3260           hasMatch ||= isMatch;
3261           entry = entry.fallbackEntry;
3262         } while (entry);
3263       }
3264     }
3265   }
3267   /**
3268    * Returns the definition of the provided Entry or Namespace which is valid for
3269    * the manifest version of the provided context, or none.
3270    *
3271    * @param {Entry|Namespace} entryOrNs
3272    * @param {Context} context
3273    *
3274    * @returns {Entry|Namespace?}
3275    */
3276   #getMatchingDefinitionForContext(entryOrNs, context) {
3277     do {
3278       if (context.matchManifestVersion(entryOrNs)) {
3279         // Common case at first iteration.
3280         return entryOrNs;
3281       }
3282       entryOrNs = entryOrNs.fallbackEntry;
3283     } while (entryOrNs);
3284   }
3286   /**
3287    * Initializes the value of a given key, by parsing the schema object
3288    * associated with it and replacing its temporary value with an `Entry`
3289    * instance.
3290    *
3291    * @param {string} key
3292    *        The name of the property to initialize.
3293    * @param {string} type
3294    *        The type of property the key represents. Must have a
3295    *        corresponding entry in the `LOADERS` object, pointing to the
3296    *        initialization method for that type.
3297    *
3298    * @returns {Entry}
3299    */
3300   initKey(key, type) {
3301     let loader = LOADERS[type];
3303     let entry;
3304     for (let schema of this[type].get(key)) {
3305       // Note: The 3rd parameter is currently only supported by loadEvent() and
3306       // loadFunction(). It stores the entry from the last iteration as a
3307       // fallbackEntry (different definitions for different manifest versions).
3308       entry = this[loader](key, schema, entry);
3309       // entry is always an Entry past the first iteration.
3310       this.set(key, entry);
3311     }
3313     return this.get(key);
3314   }
3316   loadType(name, type) {
3317     if ("$extend" in type) {
3318       return this.extendType(type);
3319     }
3320     return this.root.parseSchema(type, this.path, ["id"]);
3321   }
3323   extendType(type) {
3324     let targetType = this.get(type.$extend);
3326     // Only allow extending object and choices types for now.
3327     if (targetType instanceof ObjectType) {
3328       type.type = "object";
3329     } else if (DEBUG) {
3330       if (!targetType) {
3331         throw new Error(
3332           `Internal error: Attempt to extend a nonexistent type ${type.$extend}`
3333         );
3334       } else if (!(targetType instanceof ChoiceType)) {
3335         throw new Error(
3336           `Internal error: Attempt to extend a non-extensible type ${type.$extend}`
3337         );
3338       }
3339     }
3341     let parsed = this.root.parseSchema(type, this.path, ["$extend"]);
3343     if (DEBUG && parsed.constructor !== targetType.constructor) {
3344       throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
3345     }
3347     targetType.extend(parsed);
3349     return targetType;
3350   }
3352   loadProperty(name, prop) {
3353     if ("$ref" in prop) {
3354       if (!prop.unsupported) {
3355         return new SubModuleProperty(
3356           this.root,
3357           prop,
3358           this.path,
3359           name,
3360           prop.$ref,
3361           prop.properties || {},
3362           prop.permissions || null
3363         );
3364       }
3365     } else if ("value" in prop) {
3366       return new ValueProperty(prop, name, prop.value);
3367     } else {
3368       // We ignore the "optional" attribute on properties since we
3369       // don't inject anything here anyway.
3370       let type = this.root.parseSchema(
3371         prop,
3372         [this.name],
3373         ["optional", "permissions", "writable"]
3374       );
3375       return new TypeProperty(
3376         prop,
3377         this.path,
3378         name,
3379         type,
3380         prop.writable || false,
3381         prop.permissions || null
3382       );
3383     }
3384   }
3386   loadFunction(name, fun, fallbackEntry) {
3387     const parsed = FunctionEntry.parseSchema(this.root, fun, this.path);
3388     // If there is already a valid entry, use it as a fallback for the current
3389     // one. Used for multiple definitions for different manifest versions.
3390     if (fallbackEntry) {
3391       parsed.fallbackEntry = fallbackEntry;
3392     }
3393     return parsed;
3394   }
3396   loadEvent(name, event, fallbackEntry) {
3397     const parsed = Event.parseSchema(this.root, event, this.path);
3398     // If there is already a valid entry, use it as a fallback for the current
3399     // one. Used for multiple definitions for different manifest versions.
3400     if (fallbackEntry) {
3401       parsed.fallbackEntry = fallbackEntry;
3402     }
3403     return parsed;
3404   }
3406   /**
3407    * Injects the properties of this namespace into the given object.
3408    *
3409    * @param {object} dest
3410    *        The object into which to inject the namespace properties.
3411    * @param {InjectionContext} context
3412    *        The injection context with which to inject the properties.
3413    */
3414   injectInto(dest, context) {
3415     for (let name of this.keys()) {
3416       // TODO bug 1896081: we should not call this.get() unconditionally, but
3417       //                   only for entries that have min_manifest_version or
3418       //                   max_manifest_version set.
3419       let entry = this.#getMatchingDefinitionForContext(
3420         this.get(name),
3421         context
3422       );
3423       // If no definition matches the manifest version, do not inject the property.
3424       // This prevents the item from being enumerable in the namespace object.
3425       // We cannot accomplish this inside exportLazyProperty, it specifically
3426       // injects an enumerable object.
3427       if (!entry) {
3428         continue;
3429       }
3431       exportLazyProperty(dest, name, () => {
3432         // See Bug 1896081.
3433         // entry ??= this.get(name);
3434         return context.getDescriptor(entry, dest, name, this.path, this);
3435       });
3436     }
3437   }
3439   getDescriptor(path, context) {
3440     let obj = Cu.createObjectIn(context.cloneScope);
3442     let ns = context.schemaRoot.getNamespace(this.path.join("."));
3443     ns.injectInto(obj, context);
3445     // Only inject the namespace object if it isn't empty.
3446     if (Object.keys(obj).length) {
3447       return {
3448         descriptor: { value: obj },
3449       };
3450     }
3451   }
3453   keys() {
3454     this.init();
3455     return super.keys();
3456   }
3458   /** @returns {Generator<[string, Entry]>} */
3459   *entries() {
3460     for (let key of this.keys()) {
3461       yield [key, this.get(key)];
3462     }
3463   }
3465   get(key) {
3466     this.init();
3467     let value = super.get(key);
3469     // The initial values of lazily-initialized schema properties are
3470     // strings, pointing to the type of property, corresponding to one
3471     // of the entries in the `LOADERS` object.
3472     if (typeof value === "string") {
3473       value = this.initKey(key, value);
3474     }
3476     return value;
3477   }
3479   /**
3480    * Returns a Namespace object for the given namespace name. If a
3481    * namespace object with this name does not already exist, it is
3482    * created. If the name contains any '.' characters, namespaces are
3483    * recursively created, for each dot-separated component.
3484    *
3485    * @param {string} name
3486    *        The name of the sub-namespace to retrieve.
3487    * @param {boolean} [create = true]
3488    *        If true, create any intermediate namespaces which don't
3489    *        exist.
3490    *
3491    * @returns {Namespace}
3492    */
3493   getNamespace(name, create = true) {
3494     let subName;
3496     let idx = name.indexOf(".");
3497     if (idx > 0) {
3498       subName = name.slice(idx + 1);
3499       name = name.slice(0, idx);
3500     }
3502     let ns = super.get(name);
3503     if (!ns) {
3504       if (!create) {
3505         return null;
3506       }
3507       ns = new Namespace(this.root, name, this.path);
3508       this.set(name, ns);
3509     }
3511     if (subName) {
3512       return ns.getNamespace(subName);
3513     }
3514     return ns;
3515   }
3517   getOwnNamespace(name) {
3518     return this.getNamespace(name);
3519   }
3521   has(key) {
3522     this.init();
3523     return super.has(key);
3524   }
3528  * A namespace which combines the children of an arbitrary number of
3529  * sub-namespaces.
3530  */
3531 class Namespaces extends Namespace {
3532   constructor(root, name, path, namespaces) {
3533     super(root, name, path);
3535     this.namespaces = namespaces;
3536   }
3538   injectInto(obj, context) {
3539     for (let ns of this.namespaces) {
3540       ns.injectInto(obj, context);
3541     }
3542   }
3546  * A root schema which combines the contents of an arbitrary number of base
3547  * schema roots.
3548  */
3549 class SchemaRoots extends Namespaces {
3550   constructor(root, bases) {
3551     bases = bases.map(base => base.rootSchema || base);
3553     super(null, "", [], bases);
3555     this.root = root;
3556     this.bases = bases;
3557     this._namespaces = new Map();
3558   }
3560   _getNamespace(name, create) {
3561     let results = [];
3562     for (let root of this.bases) {
3563       let ns = root.getNamespace(name, create);
3564       if (ns) {
3565         results.push(ns);
3566       }
3567     }
3569     if (results.length == 1) {
3570       return results[0];
3571     }
3573     if (results.length) {
3574       return new Namespaces(this.root, name, name.split("."), results);
3575     }
3576     return null;
3577   }
3579   getNamespace(name, create) {
3580     let ns = this._namespaces.get(name);
3581     if (!ns) {
3582       ns = this._getNamespace(name, create);
3583       if (ns) {
3584         this._namespaces.set(name, ns);
3585       }
3586     }
3587     return ns;
3588   }
3590   *getNamespaces(name) {
3591     for (let root of this.bases) {
3592       yield* root.getNamespaces(name);
3593     }
3594   }
3598  * A root schema namespace containing schema data which is isolated from data in
3599  * other schema roots. May extend a base namespace, in which case schemas in
3600  * this root may refer to types in a base, but not vice versa.
3602  * @implements {SchemaInject}
3603  */
3604 export class SchemaRoot extends Namespace {
3605   /**
3606    * @param {SchemaRoot|SchemaRoot[]} base
3607    *        A base schema root (or roots) from which to derive, or null.
3608    * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON
3609    *        A map of schema URLs and corresponding JSON blobs from which to
3610    *        populate this root namespace.
3611    */
3612   constructor(base, schemaJSON) {
3613     super(null, "", []);
3615     if (Array.isArray(base)) {
3616       this.base = new SchemaRoots(this, base);
3617     } else {
3618       this.base = base;
3619     }
3621     this.root = this;
3622     this.schemaJSON = schemaJSON;
3623   }
3625   *getNamespaces(path) {
3626     let name = path.join(".");
3628     let ns = this.getNamespace(name, false);
3629     if (ns) {
3630       yield ns;
3631     }
3633     if (this.base) {
3634       yield* this.base.getNamespaces(name);
3635     }
3636   }
3638   /**
3639    * Returns the sub-namespace with the given name. If the given namespace
3640    * doesn't already exist, attempts to find it in the base SchemaRoot before
3641    * creating a new empty namespace.
3642    *
3643    * @param {string} name
3644    *        The namespace to retrieve.
3645    * @param {boolean} [create = true]
3646    *        If true, an empty namespace should be created if one does not
3647    *        already exist.
3648    * @returns {Namespace|null}
3649    */
3650   getNamespace(name, create = true) {
3651     let ns = super.getNamespace(name, false);
3652     if (ns) {
3653       return ns;
3654     }
3656     ns = this.base && this.base.getNamespace(name, false);
3657     if (ns) {
3658       return ns;
3659     }
3660     return create && super.getNamespace(name, create);
3661   }
3663   /**
3664    * Like getNamespace, but does not take the base SchemaRoot into account.
3665    *
3666    * @param {string} name
3667    *        The namespace to retrieve.
3668    * @returns {Namespace}
3669    */
3670   getOwnNamespace(name) {
3671     return super.getNamespace(name);
3672   }
3674   parseSchema(schema, path, extraProperties = []) {
3675     let allowedProperties = DEBUG && new Set(extraProperties);
3677     if ("choices" in schema) {
3678       return ChoiceType.parseSchema(this, schema, path, allowedProperties);
3679     } else if ("$ref" in schema) {
3680       return RefType.parseSchema(this, schema, path, allowedProperties);
3681     }
3683     let type = TYPES[schema.type];
3685     if (DEBUG) {
3686       allowedProperties.add("type");
3688       if (!("type" in schema)) {
3689         throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
3690       }
3692       if (!type) {
3693         throw new Error(`Unexpected type ${schema.type}`);
3694       }
3695     }
3697     return type.parseSchema(this, schema, path, allowedProperties);
3698   }
3700   parseSchemas() {
3701     for (let [key, schema] of this.schemaJSON.entries()) {
3702       try {
3703         if (StructuredCloneHolder.isInstance(schema)) {
3704           schema = schema.deserialize(globalThis, isParentProcess);
3706           // If we're in the parent process, we need to keep the
3707           // StructuredCloneHolder blob around in order to send to future child
3708           // processes. If we're in a child, we have no further use for it, so
3709           // just store the deserialized schema data in its place.
3710           if (!isParentProcess) {
3711             this.schemaJSON.set(key, schema);
3712           }
3713         }
3715         this.loadSchema(schema);
3716       } catch (e) {
3717         Cu.reportError(e);
3718       }
3719     }
3720   }
3722   loadSchema(json) {
3723     for (let namespace of json) {
3724       this.getOwnNamespace(namespace.namespace).addSchema(namespace);
3725     }
3726   }
3728   /**
3729    * Checks whether a given object has the necessary permissions to
3730    * expose the given namespace.
3731    *
3732    * @param {string} namespace
3733    *        The top-level namespace to check permissions for.
3734    * @param {object} wrapperFuncs
3735    *        Wrapper functions for the given context.
3736    * @param {Function} wrapperFuncs.hasPermission
3737    *        A function which, when given a string argument, returns true
3738    *        if the context has the given permission.
3739    * @returns {boolean}
3740    *        True if the context has permission for the given namespace.
3741    */
3742   checkPermissions(namespace, wrapperFuncs) {
3743     let ns = this.getNamespace(namespace);
3744     if (ns && ns.permissions) {
3745       return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
3746     }
3747     return true;
3748   }
3750   /**
3751    * Inject registered extension APIs into `dest`.
3752    *
3753    * @param {object} dest The root namespace for the APIs.
3754    *     This object is usually exposed to extensions as "chrome" or "browser".
3755    * @param {InjectionContext} wrapperFuncs An implementation of the InjectionContext
3756    *     interface, which runs the actual functionality of the generated API.
3757    */
3758   inject(dest, wrapperFuncs) {
3759     let context = new InjectionContext(wrapperFuncs, this);
3761     this.injectInto(dest, context);
3762   }
3764   injectInto(dest, context) {
3765     // For schema graphs where multiple schema roots have the same base, don't
3766     // inject it more than once.
3768     if (!context.injectedRoots.has(this)) {
3769       context.injectedRoots.add(this);
3770       if (this.base) {
3771         this.base.injectInto(dest, context);
3772       }
3773       super.injectInto(dest, context);
3774     }
3775   }
3777   /**
3778    * Normalize `obj` according to the loaded schema for `typeName`.
3779    *
3780    * @param {object} obj The object to normalize against the schema.
3781    * @param {string} typeName The name in the format namespace.propertyname
3782    * @param {object} context An implementation of Context. Any validation errors
3783    *     are reported to the given context.
3784    * @returns {object} The normalized object.
3785    */
3786   normalize(obj, typeName, context) {
3787     let [namespaceName, prop] = typeName.split(".");
3788     let ns = this.getNamespace(namespaceName);
3789     let type = ns.get(prop);
3791     let result = type.normalize(obj, new Context(context));
3792     if (result.error) {
3793       return { error: forceString(result.error) };
3794     }
3795     return result;
3796   }
3800  * @typedef {{ inject: typeof Schemas.inject }} SchemaInject
3801  *          Interface SchemaInject as used by SchemaApiManager,
3802  *          with the one method shared across Schemas and SchemaRoot.
3803  */
3804 export var Schemas = {
3805   initialized: false,
3807   REVOKE: Symbol("@@revoke"),
3809   // Maps a schema URL to the JSON contained in that schema file. This
3810   // is useful for sending the JSON across processes.
3811   schemaJSON: new Map(),
3813   // A map of schema JSON which should be available in all content processes.
3814   contentSchemaJSON: new Map(),
3816   // A map of schema JSON which should only be available to extension processes.
3817   privilegedSchemaJSON: new Map(),
3819   _rootSchema: null,
3821   // A weakmap for the validation Context class instances given an extension
3822   // context (keyed by the extensin context instance).
3823   // This is used instead of the InjectionContext for webIDL API validation
3824   // and normalization (see Schemas.checkParameters).
3825   paramsValidationContexts: new DefaultWeakMap(
3826     extContext => new Context(extContext)
3827   ),
3829   /** @returns {SchemaRoot} */
3830   get rootSchema() {
3831     if (!this.initialized) {
3832       this.init();
3833     }
3834     if (!this._rootSchema) {
3835       this._rootSchema = new SchemaRoot(null, this.schemaJSON);
3836       this._rootSchema.parseSchemas();
3837     }
3838     return this._rootSchema;
3839   },
3841   getNamespace(name) {
3842     return this.rootSchema.getNamespace(name);
3843   },
3845   init() {
3846     if (this.initialized) {
3847       return;
3848     }
3849     this.initialized = true;
3851     if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
3852       let addSchemas = schemas => {
3853         for (let [key, value] of schemas.entries()) {
3854           this.schemaJSON.set(key, value);
3855         }
3856       };
3858       if (WebExtensionPolicy.isExtensionProcess || DEBUG) {
3859         addSchemas(Services.cpmm.sharedData.get(KEY_PRIVILEGED_SCHEMAS));
3860       }
3862       let schemas = Services.cpmm.sharedData.get(KEY_CONTENT_SCHEMAS);
3863       if (schemas) {
3864         addSchemas(schemas);
3865       }
3866     }
3867   },
3869   _loadCachedSchemasPromise: null,
3870   loadCachedSchemas() {
3871     if (!this._loadCachedSchemasPromise) {
3872       this._loadCachedSchemasPromise = lazy.StartupCache.schemas
3873         .getAll()
3874         .then(results => {
3875           return results;
3876         });
3877     }
3879     return this._loadCachedSchemasPromise;
3880   },
3882   addSchema(url, schema, content = false) {
3883     this.schemaJSON.set(url, schema);
3885     if (content) {
3886       this.contentSchemaJSON.set(url, schema);
3887     } else {
3888       this.privilegedSchemaJSON.set(url, schema);
3889     }
3891     if (this._rootSchema) {
3892       throw new Error("Schema loaded after root schema populated");
3893     }
3894   },
3896   updateSharedSchemas() {
3897     let { sharedData } = Services.ppmm;
3899     sharedData.set(KEY_CONTENT_SCHEMAS, this.contentSchemaJSON);
3900     sharedData.set(KEY_PRIVILEGED_SCHEMAS, this.privilegedSchemaJSON);
3901   },
3903   fetch(url) {
3904     return readJSONAndBlobbify(url);
3905   },
3907   processSchema(json) {
3908     return blobbify(json);
3909   },
3911   async load(url, content = false) {
3912     if (!isParentProcess) {
3913       return;
3914     }
3916     const startTime = Cu.now();
3917     let schemaCache = await this.loadCachedSchemas();
3918     const fromCache = schemaCache.has(url);
3920     let blob =
3921       schemaCache.get(url) ||
3922       (await lazy.StartupCache.schemas.get(url, readJSONAndBlobbify));
3924     if (!this.schemaJSON.has(url)) {
3925       this.addSchema(url, blob, content);
3926     }
3928     ChromeUtils.addProfilerMarker(
3929       "ExtensionSchemas",
3930       { startTime },
3931       `load ${url}, from cache: ${fromCache}`
3932     );
3933   },
3935   /**
3936    * Checks whether a given object has the necessary permissions to
3937    * expose the given namespace.
3938    *
3939    * @param {string} namespace
3940    *        The top-level namespace to check permissions for.
3941    * @param {object} wrapperFuncs
3942    *        Wrapper functions for the given context.
3943    * @param {Function} wrapperFuncs.hasPermission
3944    *        A function which, when given a string argument, returns true
3945    *        if the context has the given permission.
3946    * @returns {boolean}
3947    *        True if the context has permission for the given namespace.
3948    */
3949   checkPermissions(namespace, wrapperFuncs) {
3950     return this.rootSchema.checkPermissions(namespace, wrapperFuncs);
3951   },
3953   /**
3954    * Returns a sorted array of permission names for the given permission types.
3955    *
3956    * @param {Array} types An array of permission types, defaults to all permissions.
3957    * @returns {Array} sorted array of permission names
3958    */
3959   getPermissionNames(
3960     types = [
3961       "Permission",
3962       "OptionalPermission",
3963       "PermissionNoPrompt",
3964       "OptionalPermissionNoPrompt",
3965       "PermissionPrivileged",
3966     ]
3967   ) {
3968     const ns = this.getNamespace("manifest");
3969     let names = [];
3970     for (let typeName of types) {
3971       for (let choice of ns
3972         .get(typeName)
3973         .choices.filter(choice => choice.enumeration)) {
3974         names = names.concat(choice.enumeration);
3975       }
3976     }
3977     return names.sort();
3978   },
3980   exportLazyGetter,
3982   /**
3983    * Inject registered extension APIs into `dest`.
3984    *
3985    * @param {object} dest The root namespace for the APIs.
3986    *     This object is usually exposed to extensions as "chrome" or "browser".
3987    * @param {InjectionContext} wrapperFuncs An implementation of the InjectionContext
3988    *     interface, which runs the actual functionality of the generated API.
3989    */
3990   inject(dest, wrapperFuncs) {
3991     this.rootSchema.inject(dest, wrapperFuncs);
3992   },
3994   /**
3995    * Normalize `obj` according to the loaded schema for `typeName`.
3996    *
3997    * @param {object} obj The object to normalize against the schema.
3998    * @param {string} typeName The name in the format namespace.propertyname
3999    * @param {object} context An implementation of Context. Any validation errors
4000    *     are reported to the given context.
4001    * @returns {object} The normalized object.
4002    */
4003   normalize(obj, typeName, context) {
4004     return this.rootSchema.normalize(obj, typeName, context);
4005   },
4007   /**
4008    * Validate and normalize the arguments for an API request originated
4009    * from the webIDL API bindings.
4010    *
4011    * This provides for calls originating through WebIDL the parameters
4012    * validation and normalization guarantees that the ext-APINAMESPACE.js
4013    * scripts expects (what InjectionContext does for the regular bindings).
4014    *
4015    * @param {object}                   extContext
4016    * @param {mozIExtensionAPIRequest } apiRequest
4017    *
4018    * @returns {Array<any>} Normalized arguments array.
4019    */
4020   checkWebIDLRequestParameters(extContext, apiRequest) {
4021     const getSchemaForProperty = (schemaObj, propName, schemaPath) => {
4022       if (schemaObj instanceof Namespace) {
4023         return schemaObj?.get(propName);
4024       } else if (schemaObj instanceof SubModuleProperty) {
4025         for (const fun of schemaObj.targetType.functions) {
4026           if (fun.name === propName) {
4027             return fun;
4028           }
4029         }
4031         for (const fun of schemaObj.targetType.events) {
4032           if (fun.name === propName) {
4033             return fun;
4034           }
4035         }
4036       } else if (schemaObj instanceof Event) {
4037         return schemaObj;
4038       }
4040       const schemaPathType = schemaObj?.constructor.name;
4041       throw new Error(
4042         `API Schema for "${propName}" not found in ${schemaPath} (${schemaPath} type is ${schemaPathType})`
4043       );
4044     };
4045     const { requestType, apiNamespace, apiName } = apiRequest;
4047     let [ns, ...rest] = (
4048       ["addListener", "removeListener"].includes(requestType)
4049         ? `${apiNamespace}.${apiName}.${requestType}`
4050         : `${apiNamespace}.${apiName}`
4051     ).split(".");
4052     /** @type {Namespace|CallEntry} */
4053     let apiSchema = this.getNamespace(ns);
4055     // Keep track of the current schema path, populated while navigating the nested API schema
4056     // data and then used to include the full path to the API schema that is hitting unexpected
4057     // errors due to schema data not found or an unexpected schema type.
4058     let schemaPath = [ns];
4060     while (rest.length) {
4061       // Nested property as namespace (e.g. used for proxy.settings requests).
4062       if (!apiSchema) {
4063         throw new Error(`API Schema not found for ${schemaPath.join(".")}`);
4064       }
4066       let [propName, ...newRest] = rest;
4067       rest = newRest;
4069       apiSchema = getSchemaForProperty(
4070         apiSchema,
4071         propName,
4072         schemaPath.join(".")
4073       );
4074       schemaPath.push(propName);
4075     }
4077     if (!apiSchema) {
4078       throw new Error(`API Schema not found for ${schemaPath.join(".")}`);
4079     }
4081     if (!(apiSchema instanceof CallEntry)) {
4082       throw new Error(
4083         `Unexpected API Schema type for ${schemaPath.join(
4084           "."
4085         )} (${schemaPath.join(".")} type is ${apiSchema.constructor.name})`
4086       );
4087     }
4089     return apiSchema.checkParameters(
4090       apiRequest.args,
4091       this.paramsValidationContexts.get(extContext)
4092     );
4093   },