Bug 1871127 - Add tsconfig, basic types, and fix or ignore remaining type errors...
[gecko.git] / toolkit / components / extensions / Schemas.sys.mjs
blob9107e6a347e53ce1926366d862c80fe171e5bd74
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 const lazy = {};
16 ChromeUtils.defineESModuleGetters(lazy, {
17   ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
18   NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
19   ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
20 });
22 XPCOMUtils.defineLazyServiceGetter(
23   lazy,
24   "contentPolicyService",
25   "@mozilla.org/addons/content-policy;1",
26   "nsIAddonContentPolicy"
29 ChromeUtils.defineLazyGetter(
30   lazy,
31   "StartupCache",
32   () => lazy.ExtensionParent.StartupCache
35 XPCOMUtils.defineLazyPreferenceGetter(
36   lazy,
37   "treatWarningsAsErrors",
38   "extensions.webextensions.warnings-as-errors",
39   false
42 const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content";
43 const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged";
45 const MIN_MANIFEST_VERSION = 2;
46 const MAX_MANIFEST_VERSION = 3;
48 const { DEBUG } = AppConstants;
50 const isParentProcess =
51   Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
53 function readJSON(url) {
54   return new Promise((resolve, reject) => {
55     lazy.NetUtil.asyncFetch(
56       { uri: url, loadUsingSystemPrincipal: true },
57       (inputStream, status) => {
58         if (!Components.isSuccessCode(status)) {
59           // Convert status code to a string
60           let e = Components.Exception("", status);
61           reject(new Error(`Error while loading '${url}' (${e.name})`));
62           return;
63         }
64         try {
65           let text = lazy.NetUtil.readInputStreamToString(
66             inputStream,
67             inputStream.available()
68           );
70           // Chrome JSON files include a license comment that we need to
71           // strip off for this to be valid JSON. As a hack, we just
72           // look for the first '[' character, which signals the start
73           // of the JSON content.
74           let index = text.indexOf("[");
75           text = text.slice(index);
77           resolve(JSON.parse(text));
78         } catch (e) {
79           reject(e);
80         }
81       }
82     );
83   });
86 function stripDescriptions(json, stripThis = true) {
87   if (Array.isArray(json)) {
88     for (let i = 0; i < json.length; i++) {
89       if (typeof json[i] === "object" && json[i] !== null) {
90         json[i] = stripDescriptions(json[i]);
91       }
92     }
93     return json;
94   }
96   let result = {};
98   // Objects are handled much more efficiently, both in terms of memory and
99   // CPU, if they have the same shape as other objects that serve the same
100   // purpose. So, normalize the order of properties to increase the chances
101   // that the majority of schema objects wind up in large shape groups.
102   for (let key of Object.keys(json).sort()) {
103     if (stripThis && key === "description" && typeof json[key] === "string") {
104       continue;
105     }
107     if (typeof json[key] === "object" && json[key] !== null) {
108       result[key] = stripDescriptions(json[key], key !== "properties");
109     } else {
110       result[key] = json[key];
111     }
112   }
114   return result;
117 function blobbify(json) {
118   // We don't actually use descriptions at runtime, and they make up about a
119   // third of the size of our structured clone data, so strip them before
120   // blobbifying.
121   json = stripDescriptions(json);
123   return new StructuredCloneHolder("Schemas/blobbify", null, json);
126 async function readJSONAndBlobbify(url) {
127   let json = await readJSON(url);
129   return blobbify(json);
133  * Defines a lazy getter for the given property on the given object. Any
134  * security wrappers are waived on the object before the property is
135  * defined, and the getter and setter methods are wrapped for the target
136  * scope.
138  * The given getter function is guaranteed to be called only once, even
139  * if the target scope retrieves the wrapped getter from the property
140  * descriptor and calls it directly.
142  * @param {object} object
143  *        The object on which to define the getter.
144  * @param {string | symbol} prop
145  *        The property name for which to define the getter.
146  * @param {Function} getter
147  *        The function to call in order to generate the final property
148  *        value.
149  */
150 function exportLazyGetter(object, prop, getter) {
151   object = ChromeUtils.waiveXrays(object);
153   let redefine = value => {
154     if (value === undefined) {
155       delete object[prop];
156     } else {
157       Object.defineProperty(object, prop, {
158         enumerable: true,
159         configurable: true,
160         writable: true,
161         value,
162       });
163     }
165     getter = null;
167     return value;
168   };
170   Object.defineProperty(object, prop, {
171     enumerable: true,
172     configurable: true,
174     get: Cu.exportFunction(function () {
175       return redefine(getter.call(this));
176     }, object),
178     set: Cu.exportFunction(value => {
179       redefine(value);
180     }, object),
181   });
185  * Defines a lazily-instantiated property descriptor on the given
186  * object. Any security wrappers are waived on the object before the
187  * property is defined.
189  * The given getter function is guaranteed to be called only once, even
190  * if the target scope retrieves the wrapped getter from the property
191  * descriptor and calls it directly.
193  * @param {object} object
194  *        The object on which to define the getter.
195  * @param {string | symbol} prop
196  *        The property name for which to define the getter.
197  * @param {Function} getter
198  *        The function to call in order to generate the final property
199  *        descriptor object. This will be called, and the property
200  *        descriptor installed on the object, the first time the
201  *        property is written or read. The function may return
202  *        undefined, which will cause the property to be deleted.
203  */
204 function exportLazyProperty(object, prop, getter) {
205   object = ChromeUtils.waiveXrays(object);
207   let redefine = obj => {
208     let desc = getter.call(obj);
209     getter = null;
211     delete object[prop];
212     if (desc) {
213       let defaults = {
214         configurable: true,
215         enumerable: true,
216       };
218       if (!desc.set && !desc.get) {
219         defaults.writable = true;
220       }
222       Object.defineProperty(object, prop, Object.assign(defaults, desc));
223     }
224   };
226   Object.defineProperty(object, prop, {
227     enumerable: true,
228     configurable: true,
230     get: Cu.exportFunction(function () {
231       redefine(this);
232       return object[prop];
233     }, object),
235     set: Cu.exportFunction(function (value) {
236       redefine(this);
237       object[prop] = value;
238     }, object),
239   });
242 const POSTPROCESSORS = {
243   convertImageDataToURL(imageData, context) {
244     let document = context.cloneScope.document;
245     let canvas = document.createElementNS(
246       "http://www.w3.org/1999/xhtml",
247       "canvas"
248     );
249     canvas.width = imageData.width;
250     canvas.height = imageData.height;
251     canvas.getContext("2d").putImageData(imageData, 0, 0);
253     return canvas.toDataURL("image/png");
254   },
255   webRequestBlockingPermissionRequired(string, context) {
256     if (string === "blocking" && !context.hasPermission("webRequestBlocking")) {
257       throw new context.cloneScope.Error(
258         "Using webRequest.addListener with the " +
259           "blocking option requires the 'webRequestBlocking' permission."
260       );
261     }
263     return string;
264   },
265   requireBackgroundServiceWorkerEnabled(value, context) {
266     if (WebExtensionPolicy.backgroundServiceWorkerEnabled) {
267       return value;
268     }
270     // Add an error to the manifest validations and throw the
271     // same error.
272     const msg = "background.service_worker is currently disabled";
273     context.logError(context.makeError(msg));
274     throw new Error(msg);
275   },
277   manifestVersionCheck(value, context) {
278     if (
279       value == 2 ||
280       (value == 3 &&
281         Services.prefs.getBoolPref("extensions.manifestV3.enabled", false))
282     ) {
283       return value;
284     }
285     const msg = `Unsupported manifest version: ${value}`;
286     context.logError(context.makeError(msg));
287     throw new Error(msg);
288   },
290   webAccessibleMatching(value, context) {
291     // Ensure each object has at least one of matches or extension_ids array.
292     for (let obj of value) {
293       if (!obj.matches && !obj.extension_ids) {
294         const msg = `web_accessible_resources requires one of "matches" or "extension_ids"`;
295         context.logError(context.makeError(msg));
296         throw new Error(msg);
297       }
298     }
299     return value;
300   },
303 // Parses a regular expression, with support for the Python extended
304 // syntax that allows setting flags by including the string (?im)
305 function parsePattern(pattern) {
306   let flags = "";
307   let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
308   if (match) {
309     [, flags, pattern] = match;
310   }
311   return new RegExp(pattern, flags);
314 function getValueBaseType(value) {
315   let type = typeof value;
316   switch (type) {
317     case "object":
318       if (value === null) {
319         return "null";
320       }
321       if (Array.isArray(value)) {
322         return "array";
323       }
324       break;
326     case "number":
327       if (value % 1 === 0) {
328         return "integer";
329       }
330   }
331   return type;
334 // Methods of Context that are used by Schemas.normalize. These methods can be
335 // overridden at the construction of Context.
336 const CONTEXT_FOR_VALIDATION = ["checkLoadURL", "hasPermission", "logError"];
338 // Methods of Context that are used by Schemas.inject.
339 // Callers of Schemas.inject should implement all of these methods.
340 const CONTEXT_FOR_INJECTION = [
341   ...CONTEXT_FOR_VALIDATION,
342   "getImplementation",
343   "isPermissionRevokable",
344   "shouldInject",
347 // If the message is a function, call it and return the result.
348 // Otherwise, assume it's a string.
349 function forceString(msg) {
350   if (typeof msg === "function") {
351     return msg();
352   }
353   return msg;
357  * A context for schema validation and error reporting. This class is only used
358  * internally within Schemas.
359  */
360 class Context {
361   /**
362    * @param {object} params Provides the implementation of this class.
363    * @param {Array<string>} overridableMethods
364    */
365   constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) {
366     this.params = params;
368     if (typeof params.manifestVersion !== "number") {
369       throw new Error(
370         `Unexpected params.manifestVersion value: ${params.manifestVersion}`
371       );
372     }
374     this.path = [];
375     this.preprocessors = {
376       localize(value, context) {
377         return value;
378       },
379       ...params.preprocessors,
380     };
382     this.postprocessors = POSTPROCESSORS;
383     this.isChromeCompat = params.isChromeCompat ?? false;
384     this.manifestVersion = params.manifestVersion;
386     this.currentChoices = new Set();
387     this.choicePathIndex = 0;
389     for (let method of overridableMethods) {
390       if (method in params) {
391         this[method] = params[method].bind(params);
392       }
393     }
394   }
396   get choicePath() {
397     let path = this.path.slice(this.choicePathIndex);
398     return path.join(".");
399   }
401   get cloneScope() {
402     return this.params.cloneScope || undefined;
403   }
405   get url() {
406     return this.params.url;
407   }
409   get principal() {
410     return (
411       this.params.principal ||
412       Services.scriptSecurityManager.createNullPrincipal({})
413     );
414   }
416   /**
417    * Checks whether `url` may be loaded by the extension in this context.
418    *
419    * @param {string} url The URL that the extension wished to load.
420    * @returns {boolean} Whether the context may load `url`.
421    */
422   checkLoadURL(url) {
423     let ssm = Services.scriptSecurityManager;
424     try {
425       ssm.checkLoadURIWithPrincipal(
426         this.principal,
427         Services.io.newURI(url),
428         ssm.DISALLOW_INHERIT_PRINCIPAL
429       );
430     } catch (e) {
431       return false;
432     }
433     return true;
434   }
436   /**
437    * Checks whether this context has the given permission.
438    *
439    * @param {string} permission
440    *        The name of the permission to check.
441    *
442    * @returns {boolean} True if the context has the given permission.
443    */
444   hasPermission(permission) {
445     return false;
446   }
448   /**
449    * Checks whether the given permission can be dynamically revoked or
450    * granted.
451    *
452    * @param {string} permission
453    *        The name of the permission to check.
454    *
455    * @returns {boolean} True if the given permission is revokable.
456    */
457   isPermissionRevokable(permission) {
458     return false;
459   }
461   /**
462    * Returns an error result object with the given message, for return
463    * by Type normalization functions.
464    *
465    * If the context has a `currentTarget` value, this is prepended to
466    * the message to indicate the location of the error.
467    *
468    * @param {string | Function} errorMessage
469    *        The error message which will be displayed when this is the
470    *        only possible matching schema. If a function is passed, it
471    *        will be evaluated when the error string is first needed, and
472    *        must return a string.
473    * @param {string | Function} choicesMessage
474    *        The message describing the valid what constitutes a valid
475    *        value for this schema, which will be displayed when multiple
476    *        schema choices are available and none match.
477    *
478    *        A caller may pass `null` to prevent a choice from being
479    *        added, but this should *only* be done from code processing a
480    *        choices type.
481    * @param {boolean} [warning = false]
482    *        If true, make message prefixed `Warning`. If false, make message
483    *        prefixed `Error`
484    * @returns {object}
485    */
486   error(errorMessage, choicesMessage = undefined, warning = false) {
487     if (choicesMessage !== null) {
488       let { choicePath } = this;
489       if (choicePath) {
490         choicesMessage = `.${choicePath} must ${choicesMessage}`;
491       }
493       this.currentChoices.add(choicesMessage);
494     }
496     if (this.currentTarget) {
497       let { currentTarget } = this;
498       return {
499         error: () =>
500           `${
501             warning ? "Warning" : "Error"
502           } processing ${currentTarget}: ${forceString(errorMessage)}`,
503       };
504     }
505     return { error: errorMessage };
506   }
508   /**
509    * Creates an `Error` object belonging to the current unprivileged
510    * scope. If there is no unprivileged scope associated with this
511    * context, the message is returned as a string.
512    *
513    * If the context has a `currentTarget` value, this is prepended to
514    * the message, in the same way as for the `error` method.
515    *
516    * @param {string} message
517    * @param {object} [options]
518    * @param {boolean} [options.warning = false]
519    * @returns {Error}
520    */
521   makeError(message, { warning = false } = {}) {
522     let error = forceString(this.error(message, null, warning).error);
523     if (this.cloneScope) {
524       return new this.cloneScope.Error(error);
525     }
526     return error;
527   }
529   /**
530    * Logs the given error to the console. May be overridden to enable
531    * custom logging.
532    *
533    * @param {Error|string} error
534    */
535   logError(error) {
536     if (this.cloneScope) {
537       Cu.reportError(
538         // Error objects logged using Cu.reportError are not associated
539         // to the related innerWindowID. This results in a leaked docshell
540         // since consoleService cannot release the error object when the
541         // extension global is destroyed.
542         typeof error == "string" ? error : String(error),
543         // Report the error with the appropriate stack trace when the
544         // is related to an actual extension global (instead of being
545         // related to a manifest validation).
546         this.principal && ChromeUtils.getCallerLocation(this.principal)
547       );
548     } else {
549       Cu.reportError(error);
550     }
551   }
553   /**
554    * Logs a warning. An error might be thrown when we treat warnings as errors.
555    *
556    * @param {string} warningMessage
557    */
558   logWarning(warningMessage) {
559     let error = this.makeError(warningMessage, { warning: true });
560     this.logError(error);
562     if (lazy.treatWarningsAsErrors) {
563       // This pref is false by default, and true by default in tests to
564       // discourage the use of deprecated APIs in our unit tests.
565       // If a warning is an expected part of a test, temporarily set the pref
566       // to false, e.g. with the ExtensionTestUtils.failOnSchemaWarnings helper.
567       Services.console.logStringMessage(
568         "Treating warning as error because the preference " +
569           "extensions.webextensions.warnings-as-errors is set to true"
570       );
571       if (typeof error === "string") {
572         error = new Error(error);
573       }
574       throw error;
575     }
576   }
578   /**
579    * Returns the name of the value currently being normalized. For a
580    * nested object, this is usually approximately equivalent to the
581    * JavaScript property accessor for that property. Given:
582    *
583    *   { foo: { bar: [{ baz: x }] } }
584    *
585    * When processing the value for `x`, the currentTarget is
586    * 'foo.bar.0.baz'
587    */
588   get currentTarget() {
589     return this.path.join(".");
590   }
592   /**
593    * Executes the given callback, and returns an array of choice strings
594    * passed to {@see #error} during its execution.
595    *
596    * @param {Function} callback
597    * @returns {object}
598    *          An object with a `result` property containing the return
599    *          value of the callback, and a `choice` property containing
600    *          an array of choices.
601    */
602   withChoices(callback) {
603     let { currentChoices, choicePathIndex } = this;
605     let choices = new Set();
606     this.currentChoices = choices;
607     this.choicePathIndex = this.path.length;
609     try {
610       let result = callback();
612       return { result, choices };
613     } finally {
614       this.currentChoices = currentChoices;
615       this.choicePathIndex = choicePathIndex;
617       if (choices.size == 1) {
618         for (let choice of choices) {
619           currentChoices.add(choice);
620         }
621       } else if (choices.size) {
622         this.error(null, () => {
623           let array = Array.from(choices, forceString);
624           let n = array.length - 1;
625           array[n] = `or ${array[n]}`;
627           return `must either [${array.join(", ")}]`;
628         });
629       }
630     }
631   }
633   /**
634    * Appends the given component to the `currentTarget` path to indicate
635    * that it is being processed, calls the given callback function, and
636    * then restores the original path.
637    *
638    * This is used to identify the path of the property being processed
639    * when reporting type errors.
640    *
641    * @param {string} component
642    * @param {Function} callback
643    * @returns {*}
644    */
645   withPath(component, callback) {
646     this.path.push(component);
647     try {
648       return callback();
649     } finally {
650       this.path.pop();
651     }
652   }
654   matchManifestVersion(entry) {
655     let { manifestVersion } = this;
656     return (
657       manifestVersion >= entry.min_manifest_version &&
658       manifestVersion <= entry.max_manifest_version
659     );
660   }
664  * Represents a schema entry to be injected into an object. Handles the
665  * injection, revocation, and permissions of said entry.
667  * @param {InjectionContext} context
668  *        The injection context for the entry.
669  * @param {Entry} entry
670  *        The entry to inject.
671  * @param {object} parentObject
672  *        The object into which to inject this entry.
673  * @param {string} name
674  *        The property name at which to inject this entry.
675  * @param {Array<string>} path
676  *        The full path from the root entry to this entry.
677  * @param {Entry} parentEntry
678  *        The parent entry for the injected entry.
679  */
680 class InjectionEntry {
681   constructor(context, entry, parentObj, name, path, parentEntry) {
682     this.context = context;
683     this.entry = entry;
684     this.parentObj = parentObj;
685     this.name = name;
686     this.path = path;
687     this.parentEntry = parentEntry;
689     this.injected = null;
690     this.lazyInjected = null;
691   }
693   /**
694    * @property {Array<string>} allowedContexts
695    *        The list of allowed contexts into which the entry may be
696    *        injected.
697    */
698   get allowedContexts() {
699     let { allowedContexts } = this.entry;
700     if (allowedContexts.length) {
701       return allowedContexts;
702     }
703     return this.parentEntry.defaultContexts;
704   }
706   /**
707    * @property {boolean} isRevokable
708    *        Returns true if this entry may be dynamically injected or
709    *        revoked based on its permissions.
710    */
711   get isRevokable() {
712     return (
713       this.entry.permissions &&
714       this.entry.permissions.some(perm =>
715         this.context.isPermissionRevokable(perm)
716       )
717     );
718   }
720   /**
721    * @property {boolean} hasPermission
722    *        Returns true if the injection context currently has the
723    *        appropriate permissions to access this entry.
724    */
725   get hasPermission() {
726     return (
727       !this.entry.permissions ||
728       this.entry.permissions.some(perm => this.context.hasPermission(perm))
729     );
730   }
732   /**
733    * @property {boolean} shouldInject
734    *        Returns true if this entry should be injected in the given
735    *        context, without respect to permissions.
736    */
737   get shouldInject() {
738     return (
739       this.context.matchManifestVersion(this.entry) &&
740       this.context.shouldInject(
741         this.path.join("."),
742         this.name,
743         this.allowedContexts
744       )
745     );
746   }
748   /**
749    * Revokes this entry, removing its property from its parent object,
750    * and invalidating its wrappers.
751    */
752   revoke() {
753     if (this.lazyInjected) {
754       this.lazyInjected = false;
755     } else if (this.injected) {
756       if (this.injected.revoke) {
757         this.injected.revoke();
758       }
760       try {
761         let unwrapped = ChromeUtils.waiveXrays(this.parentObj);
762         delete unwrapped[this.name];
763       } catch (e) {
764         Cu.reportError(e);
765       }
767       let { value } = this.injected.descriptor;
768       if (value) {
769         this.context.revokeChildren(value);
770       }
772       this.injected = null;
773     }
774   }
776   /**
777    * Returns a property descriptor object for this entry, if it should
778    * be injected, or undefined if it should not.
779    *
780    * @returns {object?}
781    *        A property descriptor object, or undefined if the property
782    *        should be removed.
783    */
784   getDescriptor() {
785     this.lazyInjected = false;
787     if (this.injected) {
788       let path = [...this.path, this.name];
789       throw new Error(
790         `Attempting to re-inject already injected entry: ${path.join(".")}`
791       );
792     }
794     if (!this.shouldInject) {
795       return;
796     }
798     if (this.isRevokable) {
799       this.context.pendingEntries.add(this);
800     }
802     if (!this.hasPermission) {
803       return;
804     }
806     this.injected = this.entry.getDescriptor(this.path, this.context);
807     if (!this.injected) {
808       return undefined;
809     }
811     return this.injected.descriptor;
812   }
814   /**
815    * Injects a lazy property descriptor into the parent object which
816    * checks permissions and eligibility for injection the first time it
817    * is accessed.
818    */
819   lazyInject() {
820     if (this.lazyInjected || this.injected) {
821       let path = [...this.path, this.name];
822       throw new Error(
823         `Attempting to re-lazy-inject already injected entry: ${path.join(".")}`
824       );
825     }
827     this.lazyInjected = true;
828     exportLazyProperty(this.parentObj, this.name, () => {
829       if (this.lazyInjected) {
830         return this.getDescriptor();
831       }
832     });
833   }
835   /**
836    * Injects or revokes this entry if its current state does not match
837    * the context's current permissions.
838    */
839   permissionsChanged() {
840     if (this.injected) {
841       this.maybeRevoke();
842     } else {
843       this.maybeInject();
844     }
845   }
847   maybeInject() {
848     if (!this.injected && !this.lazyInjected) {
849       this.lazyInject();
850     }
851   }
853   maybeRevoke() {
854     if (this.injected && !this.hasPermission) {
855       this.revoke();
856     }
857   }
861  * Holds methods that run the actual implementation of the extension APIs. These
862  * methods are only called if the extension API invocation matches the signature
863  * as defined in the schema. Otherwise an error is reported to the context.
864  */
865 class InjectionContext extends Context {
866   constructor(params, schemaRoot) {
867     super(params, CONTEXT_FOR_INJECTION);
869     this.schemaRoot = schemaRoot;
871     this.pendingEntries = new Set();
872     this.children = new DefaultWeakMap(() => new Map());
874     this.injectedRoots = new Set();
876     if (params.setPermissionsChangedCallback) {
877       params.setPermissionsChangedCallback(this.permissionsChanged.bind(this));
878     }
879   }
881   /**
882    * Check whether the API should be injected.
883    *
884    * @abstract
885    * @param {string} namespace The namespace of the API. This may contain dots,
886    *     e.g. in the case of "devtools.inspectedWindow".
887    * @param {string?} name The name of the property in the namespace.
888    *     `null` if we are checking whether the namespace should be injected.
889    * @param {Array<string>} allowedContexts A list of additional contexts in
890    *      which this API should be available. May include any of:
891    *         "main" - The main chrome browser process.
892    *         "addon" - An addon process.
893    *         "content" - A content process.
894    * @returns {boolean} Whether the API should be injected.
895    */
896   shouldInject(namespace, name, allowedContexts) {
897     throw new Error("Not implemented");
898   }
900   /**
901    * Generate the implementation for `namespace`.`name`.
902    *
903    * @abstract
904    * @param {string} namespace The full path to the namespace of the API, minus
905    *     the name of the method or property. E.g. "storage.local".
906    * @param {string} name The name of the method, property or event.
907    * @returns {SchemaAPIInterface} The implementation of the API.
908    */
909   getImplementation(namespace, name) {
910     throw new Error("Not implemented");
911   }
913   /**
914    * Updates all injection entries which may need to be updated after a
915    * permission change, revoking or re-injecting them as necessary.
916    */
917   permissionsChanged() {
918     for (let entry of this.pendingEntries) {
919       try {
920         entry.permissionsChanged();
921       } catch (e) {
922         Cu.reportError(e);
923       }
924     }
925   }
927   /**
928    * Recursively revokes all child injection entries of the given
929    * object.
930    *
931    * @param {object} object
932    *        The object for which to invoke children.
933    */
934   revokeChildren(object) {
935     if (!this.children.has(object)) {
936       return;
937     }
939     let children = this.children.get(object);
940     for (let [name, entry] of children.entries()) {
941       try {
942         entry.revoke();
943       } catch (e) {
944         Cu.reportError(e);
945       }
946       children.delete(name);
948       // When we revoke children for an object, we consider that object
949       // dead. If the entry is ever reified again, a new object is
950       // created, with new child entries.
951       this.pendingEntries.delete(entry);
952     }
953     this.children.delete(object);
954   }
956   _getInjectionEntry(entry, dest, name, path, parentEntry) {
957     let injection = new InjectionEntry(
958       this,
959       entry,
960       dest,
961       name,
962       path,
963       parentEntry
964     );
966     this.children.get(dest).set(name, injection);
968     return injection;
969   }
971   /**
972    * Returns the property descriptor for the given entry.
973    *
974    * @param {Entry} entry
975    *        The entry instance to return a descriptor for.
976    * @param {object} dest
977    *        The object into which this entry is being injected.
978    * @param {string} name
979    *        The property name on the destination object where the entry
980    *        will be injected.
981    * @param {Array<string>} path
982    *        The full path from the root injection object to this entry.
983    * @param {Partial<Entry>} parentEntry
984    *        The parent entry for this entry.
985    *
986    * @returns {object?}
987    *        A property descriptor object, or null if the entry should
988    *        not be injected.
989    */
990   getDescriptor(entry, dest, name, path, parentEntry) {
991     let injection = this._getInjectionEntry(
992       entry,
993       dest,
994       name,
995       path,
996       parentEntry
997     );
999     return injection.getDescriptor();
1000   }
1002   /**
1003    * Lazily injects the given entry into the given object.
1004    *
1005    * @param {Entry} entry
1006    *        The entry instance to lazily inject.
1007    * @param {object} dest
1008    *        The object into which to inject this entry.
1009    * @param {string} name
1010    *        The property name at which to inject the entry.
1011    * @param {Array<string>} path
1012    *        The full path from the root injection object to this entry.
1013    * @param {Entry} parentEntry
1014    *        The parent entry for this entry.
1015    */
1016   injectInto(entry, dest, name, path, parentEntry) {
1017     let injection = this._getInjectionEntry(
1018       entry,
1019       dest,
1020       name,
1021       path,
1022       parentEntry
1023     );
1025     injection.lazyInject();
1026   }
1030  * The methods in this singleton represent the "format" specifier for
1031  * JSON Schema string types.
1033  * Each method either returns a normalized version of the original
1034  * value, or throws an error if the value is not valid for the given
1035  * format.
1036  */
1037 const FORMATS = {
1038   hostname(string, context) {
1039     // TODO bug 1797376: Despite the name, this format is NOT a "hostname",
1040     // but hostname + port and may fail with IPv6. Use canonicalDomain instead.
1041     let valid = true;
1043     try {
1044       valid = new URL(`http://${string}`).host === string;
1045     } catch (e) {
1046       valid = false;
1047     }
1049     if (!valid) {
1050       throw new Error(`Invalid hostname ${string}`);
1051     }
1053     return string;
1054   },
1056   canonicalDomain(string, context) {
1057     let valid;
1059     try {
1060       valid = new URL(`http://${string}`).hostname === string;
1061     } catch (e) {
1062       valid = false;
1063     }
1065     if (!valid) {
1066       // Require the input to be a canonical domain.
1067       // Rejects obvious non-domains such as URLs,
1068       // but also catches non-IDN (punycode) domains.
1069       throw new Error(`Invalid domain ${string}`);
1070     }
1072     return string;
1073   },
1075   url(string, context) {
1076     let url = new URL(string).href;
1078     if (!context.checkLoadURL(url)) {
1079       throw new Error(`Access denied for URL ${url}`);
1080     }
1081     return url;
1082   },
1084   origin(string, context) {
1085     let url;
1086     try {
1087       url = new URL(string);
1088     } catch (e) {
1089       throw new Error(`Invalid origin: ${string}`);
1090     }
1091     if (!/^https?:/.test(url.protocol)) {
1092       throw new Error(`Invalid origin must be http or https for URL ${string}`);
1093     }
1094     // url.origin is punycode so a direct check against string wont work.
1095     // url.href appends a slash even if not in the original string, we we
1096     // additionally check that string does not end in slash.
1097     if (string.endsWith("/") || url.href != new URL(url.origin).href) {
1098       throw new Error(
1099         `Invalid origin for URL ${string}, replace with origin ${url.origin}`
1100       );
1101     }
1102     if (!context.checkLoadURL(url.origin)) {
1103       throw new Error(`Access denied for URL ${url}`);
1104     }
1105     return url.origin;
1106   },
1108   relativeUrl(string, context) {
1109     if (!context.url) {
1110       // If there's no context URL, return relative URLs unresolved, and
1111       // skip security checks for them.
1112       try {
1113         new URL(string);
1114       } catch (e) {
1115         return string;
1116       }
1117     }
1119     let url = new URL(string, context.url).href;
1121     if (!context.checkLoadURL(url)) {
1122       throw new Error(`Access denied for URL ${url}`);
1123     }
1124     return url;
1125   },
1127   strictRelativeUrl(string, context) {
1128     void FORMATS.unresolvedRelativeUrl(string, context);
1129     return FORMATS.relativeUrl(string, context);
1130   },
1132   unresolvedRelativeUrl(string, context) {
1133     if (!string.startsWith("//")) {
1134       try {
1135         new URL(string);
1136       } catch (e) {
1137         return string;
1138       }
1139     }
1141     throw new SyntaxError(
1142       `String ${JSON.stringify(string)} must be a relative URL`
1143     );
1144   },
1146   homepageUrl(string, context) {
1147     // Pipes are used for separating homepages, but we only allow extensions to
1148     // set a single homepage. Encoding any pipes makes it one URL.
1149     return FORMATS.relativeUrl(
1150       string.replace(new RegExp("\\|", "g"), "%7C"),
1151       context
1152     );
1153   },
1155   imageDataOrStrictRelativeUrl(string, context) {
1156     // Do not accept a string which resolves as an absolute URL, or any
1157     // protocol-relative URL, except PNG or JPG data URLs
1158     if (
1159       !string.startsWith("data:image/png;base64,") &&
1160       !string.startsWith("data:image/jpeg;base64,")
1161     ) {
1162       try {
1163         return FORMATS.strictRelativeUrl(string, context);
1164       } catch (e) {
1165         throw new SyntaxError(
1166           `String ${JSON.stringify(
1167             string
1168           )} must be a relative or PNG or JPG data:image URL`
1169         );
1170       }
1171     }
1172     return string;
1173   },
1175   contentSecurityPolicy(string, context) {
1176     // Manifest V3 extension_pages allows WASM.  When sandbox is
1177     // implemented, or any other V3 or later directive, the flags
1178     // logic will need to be updated.
1179     let flags =
1180       context.manifestVersion < 3
1181         ? Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY
1182         : Ci.nsIAddonContentPolicy.CSP_ALLOW_WASM;
1183     let error = lazy.contentPolicyService.validateAddonCSP(string, flags);
1184     if (error != null) {
1185       // The CSP validation error is not reported as part of the "choices" error message,
1186       // we log the CSP validation error explicitly here to make it easier for the addon developers
1187       // to see and fix the extension CSP.
1188       context.logError(`Error processing ${context.currentTarget}: ${error}`);
1189       return null;
1190     }
1191     return string;
1192   },
1194   date(string, context) {
1195     // A valid ISO 8601 timestamp.
1196     const PATTERN =
1197       /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/;
1198     if (!PATTERN.test(string)) {
1199       throw new Error(`Invalid date string ${string}`);
1200     }
1201     // Our pattern just checks the format, we could still have invalid
1202     // values (e.g., month=99 or month=02 and day=31).  Let the Date
1203     // constructor do the dirty work of validating.
1204     if (isNaN(Date.parse(string))) {
1205       throw new Error(`Invalid date string ${string}`);
1206     }
1207     return string;
1208   },
1210   manifestShortcutKey(string, context) {
1211     if (lazy.ShortcutUtils.validate(string) == lazy.ShortcutUtils.IS_VALID) {
1212       return string;
1213     }
1214     let errorMessage =
1215       `Value "${string}" must consist of ` +
1216       `either a combination of one or two modifiers, including ` +
1217       `a mandatory primary modifier and a key, separated by '+', ` +
1218       `or a media key. For details see: ` +
1219       `https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`;
1220     throw new Error(errorMessage);
1221   },
1223   manifestShortcutKeyOrEmpty(string, context) {
1224     return string === "" ? "" : FORMATS.manifestShortcutKey(string, context);
1225   },
1227   versionString(string, context) {
1228     const parts = string.split(".");
1230     if (
1231       // We accept up to 4 numbers.
1232       parts.length > 4 ||
1233       // Non-zero values cannot start with 0 and we allow numbers up to 9 digits.
1234       parts.some(part => !/^(0|[1-9][0-9]{0,8})$/.test(part))
1235     ) {
1236       context.logWarning(
1237         `version must be a version string consisting of at most 4 integers ` +
1238           `of at most 9 digits without leading zeros, and separated with dots`
1239       );
1240     }
1242     // The idea is to only emit a warning when the version string does not
1243     // match the simple format we want to encourage developers to use. Given
1244     // the version is required, we always accept the value as is.
1245     return string;
1246   },
1249 // Schema files contain namespaces, and each namespace contains types,
1250 // properties, functions, and events. An Entry is a base class for
1251 // types, properties, functions, and events.
1252 class Entry {
1253   constructor(schema = {}) {
1254     /**
1255      * If set to any value which evaluates as true, this entry is
1256      * deprecated, and any access to it will result in a deprecation
1257      * warning being logged to the browser console.
1258      *
1259      * If the value is a string, it will be appended to the deprecation
1260      * message. If it contains the substring "${value}", it will be
1261      * replaced with a string representation of the value being
1262      * processed.
1263      *
1264      * If the value is any other truthy value, a generic deprecation
1265      * message will be emitted.
1266      */
1267     this.deprecated = false;
1268     if ("deprecated" in schema) {
1269       this.deprecated = schema.deprecated;
1270     }
1272     /**
1273      * @property {string} [preprocessor]
1274      * If set to a string value, and a preprocessor of the same is
1275      * defined in the validation context, it will be applied to this
1276      * value prior to any normalization.
1277      */
1278     this.preprocessor = schema.preprocess || null;
1280     /**
1281      * @property {string} [postprocessor]
1282      * If set to a string value, and a postprocessor of the same is
1283      * defined in the validation context, it will be applied to this
1284      * value after any normalization.
1285      */
1286     this.postprocessor = schema.postprocess || null;
1288     /**
1289      * @property {Array<string>} allowedContexts A list of allowed contexts
1290      * to consider before generating the API.
1291      * These are not parsed by the schema, but passed to `shouldInject`.
1292      */
1293     this.allowedContexts = schema.allowedContexts || [];
1295     this.min_manifest_version =
1296       schema.min_manifest_version ?? MIN_MANIFEST_VERSION;
1297     this.max_manifest_version =
1298       schema.max_manifest_version ?? MAX_MANIFEST_VERSION;
1299   }
1301   /**
1302    * Preprocess the given value with the preprocessor declared in
1303    * `preprocessor`.
1304    *
1305    * @param {*} value
1306    * @param {Context} context
1307    * @returns {*}
1308    */
1309   preprocess(value, context) {
1310     if (this.preprocessor) {
1311       return context.preprocessors[this.preprocessor](value, context);
1312     }
1313     return value;
1314   }
1316   /**
1317    * Postprocess the given result with the postprocessor declared in
1318    * `postprocessor`.
1319    *
1320    * @param {object} result
1321    * @param {Context} context
1322    * @returns {object}
1323    */
1324   postprocess(result, context) {
1325     if (result.error || !this.postprocessor) {
1326       return result;
1327     }
1329     let value = context.postprocessors[this.postprocessor](
1330       result.value,
1331       context
1332     );
1333     return { value };
1334   }
1336   /**
1337    * Logs a deprecation warning for this entry, based on the value of
1338    * its `deprecated` property.
1339    *
1340    * @param {Context} context
1341    * @param {any} [value]
1342    */
1343   logDeprecation(context, value = null) {
1344     let message = "This property is deprecated";
1345     if (typeof this.deprecated == "string") {
1346       message = this.deprecated;
1347       if (message.includes("${value}")) {
1348         try {
1349           value = JSON.stringify(value);
1350         } catch (e) {
1351           value = String(value);
1352         }
1353         message = message.replace(/\$\{value\}/g, () => value);
1354       }
1355     }
1357     context.logWarning(message);
1358   }
1360   /**
1361    * Checks whether the entry is deprecated and, if so, logs a
1362    * deprecation message.
1363    *
1364    * @param {Context} context
1365    * @param {any} [value]
1366    */
1367   checkDeprecated(context, value = null) {
1368     if (this.deprecated) {
1369       this.logDeprecation(context, value);
1370     }
1371   }
1373   /**
1374    * Returns an object containing property descriptor for use when
1375    * injecting this entry into an API object.
1376    *
1377    * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
1378    * @param {InjectionContext} context
1379    *
1380    * @returns {object?}
1381    *        An object containing a `descriptor` property, specifying the
1382    *        entry's property descriptor, and an optional `revoke`
1383    *        method, to be called when the entry is being revoked.
1384    */
1385   getDescriptor(path, context) {
1386     return undefined;
1387   }
1390 // Corresponds either to a type declared in the "types" section of the
1391 // schema or else to any type object used throughout the schema.
1392 class Type extends Entry {
1393   /**
1394    * @property {Array<string>} EXTRA_PROPERTIES
1395    *        An array of extra properties which may be present for
1396    *        schemas of this type.
1397    */
1398   static get EXTRA_PROPERTIES() {
1399     return [
1400       "description",
1401       "deprecated",
1402       "preprocess",
1403       "postprocess",
1404       "privileged",
1405       "allowedContexts",
1406       "min_manifest_version",
1407       "max_manifest_version",
1408     ];
1409   }
1411   /**
1412    * Parses the given schema object and returns an instance of this
1413    * class which corresponds to its properties.
1414    *
1415    * @param {SchemaRoot} root
1416    *        The root schema for this type.
1417    * @param {object} schema
1418    *        A JSON schema object which corresponds to a definition of
1419    *        this type.
1420    * @param {Array<string>} path
1421    *        The path to this schema object from the root schema,
1422    *        corresponding to the property names and array indices
1423    *        traversed during parsing in order to arrive at this schema
1424    *        object.
1425    * @param {Array<string>} [extraProperties]
1426    *        An array of extra property names which are valid for this
1427    *        schema in the current context.
1428    * @returns {Type}
1429    *        An instance of this type which corresponds to the given
1430    *        schema object.
1431    * @static
1432    */
1433   static parseSchema(root, schema, path, extraProperties = []) {
1434     this.checkSchemaProperties(schema, path, extraProperties);
1436     return new this(schema);
1437   }
1439   /**
1440    * Checks that all of the properties present in the given schema
1441    * object are valid properties for this type, and throws if invalid.
1442    *
1443    * @param {object} schema
1444    *        A JSON schema object.
1445    * @param {Array<string>} path
1446    *        The path to this schema object from the root schema,
1447    *        corresponding to the property names and array indices
1448    *        traversed during parsing in order to arrive at this schema
1449    *        object.
1450    * @param {Iterable<string>} [extra]
1451    *        An array of extra property names which are valid for this
1452    *        schema in the current context.
1453    * @throws {Error}
1454    *        An error describing the first invalid property found in the
1455    *        schema object.
1456    */
1457   static checkSchemaProperties(schema, path, extra = []) {
1458     if (DEBUG) {
1459       let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]);
1461       for (let prop of Object.keys(schema)) {
1462         if (!allowedSet.has(prop)) {
1463           throw new Error(
1464             `Internal error: Namespace ${path.join(".")} has ` +
1465               `invalid type property "${prop}" ` +
1466               `in type "${schema.id || JSON.stringify(schema)}"`
1467           );
1468         }
1469       }
1470     }
1471   }
1473   // Takes a value, checks that it has the correct type, and returns a
1474   // "normalized" version of the value. The normalized version will
1475   // include "nulls" in place of omitted optional properties. The
1476   // result of this function is either {error: "Some type error"} or
1477   // {value: <normalized-value>}.
1478   normalize(value, context) {
1479     return context.error("invalid type");
1480   }
1482   // Unlike normalize, this function does a shallow check to see if
1483   // |baseType| (one of the possible getValueBaseType results) is
1484   // valid for this type. It returns true or false. It's used to fill
1485   // in optional arguments to functions before actually type checking
1487   checkBaseType(baseType) {
1488     return false;
1489   }
1491   // Helper method that simply relies on checkBaseType to implement
1492   // normalize. Subclasses can choose to use it or not.
1493   normalizeBase(type, value, context) {
1494     if (this.checkBaseType(getValueBaseType(value))) {
1495       this.checkDeprecated(context, value);
1496       return { value: this.preprocess(value, context) };
1497     }
1499     let choice;
1500     if ("aeiou".includes(type[0])) {
1501       choice = `be an ${type} value`;
1502     } else {
1503       choice = `be a ${type} value`;
1504     }
1506     return context.error(
1507       () => `Expected ${type} instead of ${JSON.stringify(value)}`,
1508       choice
1509     );
1510   }
1513 // Type that allows any value.
1514 class AnyType extends Type {
1515   normalize(value, context) {
1516     this.checkDeprecated(context, value);
1517     return this.postprocess({ value }, context);
1518   }
1520   checkBaseType(baseType) {
1521     return true;
1522   }
1525 // An untagged union type.
1526 class ChoiceType extends Type {
1527   static get EXTRA_PROPERTIES() {
1528     return ["choices", ...super.EXTRA_PROPERTIES];
1529   }
1531   /** @type {(root, schema, path, extraProperties?: Iterable) => ChoiceType} */
1532   static parseSchema(root, schema, path, extraProperties = []) {
1533     this.checkSchemaProperties(schema, path, extraProperties);
1535     let choices = schema.choices.map(t => root.parseSchema(t, path));
1536     return new this(schema, choices);
1537   }
1539   constructor(schema, choices) {
1540     super(schema);
1541     this.choices = choices;
1542   }
1544   extend(type) {
1545     this.choices.push(...type.choices);
1547     return this;
1548   }
1550   normalize(value, context) {
1551     this.checkDeprecated(context, value);
1553     let error;
1554     let { choices, result } = context.withChoices(() => {
1555       for (let choice of this.choices) {
1556         // Ignore a possible choice if it is not supported by
1557         // the manifest version we are normalizing.
1558         if (!context.matchManifestVersion(choice)) {
1559           continue;
1560         }
1562         let r = choice.normalize(value, context);
1563         if (!r.error) {
1564           return r;
1565         }
1567         error = r;
1568       }
1569     });
1571     if (result) {
1572       return result;
1573     }
1574     if (choices.size <= 1) {
1575       return error;
1576     }
1578     choices = Array.from(choices, forceString);
1579     let n = choices.length - 1;
1580     choices[n] = `or ${choices[n]}`;
1582     let message;
1583     if (typeof value === "object") {
1584       message = () => `Value must either: ${choices.join(", ")}`;
1585     } else {
1586       message = () =>
1587         `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`;
1588     }
1590     return context.error(message, null);
1591   }
1593   checkBaseType(baseType) {
1594     return this.choices.some(t => t.checkBaseType(baseType));
1595   }
1597   getDescriptor(path, context) {
1598     // In StringType.getDescriptor, unlike any other Type, a descriptor is returned if
1599     // it is an enumeration.  Since we need versioned choices in some cases, here we
1600     // build a list of valid enumerations that will work for a given manifest version.
1601     if (
1602       !this.choices.length ||
1603       !this.choices.every(t => t.checkBaseType("string") && t.enumeration)
1604     ) {
1605       return;
1606     }
1608     let obj = Cu.createObjectIn(context.cloneScope);
1609     let descriptor = { value: obj };
1610     for (let choice of this.choices) {
1611       // Ignore a possible choice if it is not supported by
1612       // the manifest version we are normalizing.
1613       if (!context.matchManifestVersion(choice)) {
1614         continue;
1615       }
1616       let d = choice.getDescriptor(path, context);
1617       if (d) {
1618         Object.assign(obj, d.descriptor.value);
1619       }
1620     }
1622     return { descriptor };
1623   }
1626 // This is a reference to another type--essentially a typedef.
1627 class RefType extends Type {
1628   static get EXTRA_PROPERTIES() {
1629     return ["$ref", ...super.EXTRA_PROPERTIES];
1630   }
1632   /** @type {(root, schema, path, extraProperties?: Iterable) => RefType} */
1633   static parseSchema(root, schema, path, extraProperties = []) {
1634     this.checkSchemaProperties(schema, path, extraProperties);
1636     let ref = schema.$ref;
1637     let ns = path.join(".");
1638     if (ref.includes(".")) {
1639       [, ns, ref] = /^(.*)\.(.*?)$/.exec(ref);
1640     }
1641     return new this(root, schema, ns, ref);
1642   }
1644   // For a reference to a type named T declared in namespace NS,
1645   // namespaceName will be NS and reference will be T.
1646   constructor(root, schema, namespaceName, reference) {
1647     super(schema);
1648     this.root = root;
1649     this.namespaceName = namespaceName;
1650     this.reference = reference;
1651   }
1653   get targetType() {
1654     let ns = this.root.getNamespace(this.namespaceName);
1655     let type = ns.get(this.reference);
1656     if (!type) {
1657       throw new Error(`Internal error: Type ${this.reference} not found`);
1658     }
1659     return type;
1660   }
1662   normalize(value, context) {
1663     this.checkDeprecated(context, value);
1664     return this.targetType.normalize(value, context);
1665   }
1667   checkBaseType(baseType) {
1668     return this.targetType.checkBaseType(baseType);
1669   }
1672 class StringType extends Type {
1673   static get EXTRA_PROPERTIES() {
1674     return [
1675       "enum",
1676       "minLength",
1677       "maxLength",
1678       "pattern",
1679       "format",
1680       ...super.EXTRA_PROPERTIES,
1681     ];
1682   }
1684   static parseSchema(root, schema, path, extraProperties = []) {
1685     this.checkSchemaProperties(schema, path, extraProperties);
1687     let enumeration = schema.enum || null;
1688     if (enumeration) {
1689       // The "enum" property is either a list of strings that are
1690       // valid values or else a list of {name, description} objects,
1691       // where the .name values are the valid values.
1692       enumeration = enumeration.map(e => {
1693         if (typeof e == "object") {
1694           return e.name;
1695         }
1696         return e;
1697       });
1698     }
1700     let pattern = null;
1701     if (schema.pattern) {
1702       try {
1703         pattern = parsePattern(schema.pattern);
1704       } catch (e) {
1705         throw new Error(
1706           `Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}`
1707         );
1708       }
1709     }
1711     let format = null;
1712     if (schema.format) {
1713       if (!(schema.format in FORMATS)) {
1714         throw new Error(
1715           `Internal error: Invalid string format ${schema.format}`
1716         );
1717       }
1718       format = FORMATS[schema.format];
1719     }
1720     return new this(
1721       schema,
1722       schema.id || undefined,
1723       enumeration,
1724       schema.minLength || 0,
1725       schema.maxLength || Infinity,
1726       pattern,
1727       format
1728     );
1729   }
1731   constructor(
1732     schema,
1733     name,
1734     enumeration,
1735     minLength,
1736     maxLength,
1737     pattern,
1738     format
1739   ) {
1740     super(schema);
1741     this.name = name;
1742     this.enumeration = enumeration;
1743     this.minLength = minLength;
1744     this.maxLength = maxLength;
1745     this.pattern = pattern;
1746     this.format = format;
1747   }
1749   normalize(value, context) {
1750     let r = this.normalizeBase("string", value, context);
1751     if (r.error) {
1752       return r;
1753     }
1754     value = r.value;
1756     if (this.enumeration) {
1757       if (this.enumeration.includes(value)) {
1758         return this.postprocess({ value }, context);
1759       }
1761       let choices = this.enumeration.map(JSON.stringify).join(", ");
1763       return context.error(
1764         () => `Invalid enumeration value ${JSON.stringify(value)}`,
1765         `be one of [${choices}]`
1766       );
1767     }
1769     if (value.length < this.minLength) {
1770       return context.error(
1771         () =>
1772           `String ${JSON.stringify(value)} is too short (must be ${
1773             this.minLength
1774           })`,
1775         `be longer than ${this.minLength}`
1776       );
1777     }
1778     if (value.length > this.maxLength) {
1779       return context.error(
1780         () =>
1781           `String ${JSON.stringify(value)} is too long (must be ${
1782             this.maxLength
1783           })`,
1784         `be shorter than ${this.maxLength}`
1785       );
1786     }
1788     if (this.pattern && !this.pattern.test(value)) {
1789       return context.error(
1790         () => `String ${JSON.stringify(value)} must match ${this.pattern}`,
1791         `match the pattern ${this.pattern.toSource()}`
1792       );
1793     }
1795     if (this.format) {
1796       try {
1797         r.value = this.format(r.value, context);
1798       } catch (e) {
1799         return context.error(
1800           String(e),
1801           `match the format "${this.format.name}"`
1802         );
1803       }
1804     }
1806     return r;
1807   }
1809   checkBaseType(baseType) {
1810     return baseType == "string";
1811   }
1813   getDescriptor(path, context) {
1814     if (this.enumeration) {
1815       let obj = Cu.createObjectIn(context.cloneScope);
1817       for (let e of this.enumeration) {
1818         obj[e.toUpperCase()] = e;
1819       }
1821       return {
1822         descriptor: { value: obj },
1823       };
1824     }
1825   }
1828 class NullType extends Type {
1829   normalize(value, context) {
1830     return this.normalizeBase("null", value, context);
1831   }
1833   checkBaseType(baseType) {
1834     return baseType == "null";
1835   }
1838 let FunctionEntry;
1839 let Event;
1840 let SubModuleType;
1842 class ObjectType extends Type {
1843   static get EXTRA_PROPERTIES() {
1844     return [
1845       "properties",
1846       "patternProperties",
1847       "$import",
1848       ...super.EXTRA_PROPERTIES,
1849     ];
1850   }
1852   static parseSchema(root, schema, path, extraProperties = []) {
1853     if ("functions" in schema) {
1854       return SubModuleType.parseSchema(root, schema, path, extraProperties);
1855     }
1857     if (DEBUG && !("$extend" in schema)) {
1858       // Only allow extending "properties" and "patternProperties".
1859       extraProperties = [
1860         "additionalProperties",
1861         "isInstanceOf",
1862         ...extraProperties,
1863       ];
1864     }
1865     this.checkSchemaProperties(schema, path, extraProperties);
1867     let imported = null;
1868     if ("$import" in schema) {
1869       let importPath = schema.$import;
1870       let idx = importPath.indexOf(".");
1871       if (idx === -1) {
1872         imported = [path[0], importPath];
1873       } else {
1874         imported = [importPath.slice(0, idx), importPath.slice(idx + 1)];
1875       }
1876     }
1878     let parseProperty = (schema, extraProps = []) => {
1879       return {
1880         type: root.parseSchema(
1881           schema,
1882           path,
1883           DEBUG && [
1884             "unsupported",
1885             "onError",
1886             "permissions",
1887             "default",
1888             ...extraProps,
1889           ]
1890         ),
1891         optional: schema.optional || false,
1892         unsupported: schema.unsupported || false,
1893         onError: schema.onError || null,
1894         default: schema.default === undefined ? null : schema.default,
1895       };
1896     };
1898     // Parse explicit "properties" object.
1899     let properties = Object.create(null);
1900     for (let propName of Object.keys(schema.properties || {})) {
1901       properties[propName] = parseProperty(schema.properties[propName], [
1902         "optional",
1903       ]);
1904     }
1906     // Parse regexp properties from "patternProperties" object.
1907     let patternProperties = [];
1908     for (let propName of Object.keys(schema.patternProperties || {})) {
1909       let pattern;
1910       try {
1911         pattern = parsePattern(propName);
1912       } catch (e) {
1913         throw new Error(
1914           `Internal error: Invalid property pattern ${JSON.stringify(propName)}`
1915         );
1916       }
1918       patternProperties.push({
1919         pattern,
1920         type: parseProperty(schema.patternProperties[propName]),
1921       });
1922     }
1924     // Parse "additionalProperties" schema.
1925     let additionalProperties = null;
1926     if (schema.additionalProperties) {
1927       let type = schema.additionalProperties;
1928       if (type === true) {
1929         type = { type: "any" };
1930       }
1932       additionalProperties = root.parseSchema(type, path);
1933     }
1935     return new this(
1936       schema,
1937       properties,
1938       additionalProperties,
1939       patternProperties,
1940       schema.isInstanceOf || null,
1941       imported
1942     );
1943   }
1945   constructor(
1946     schema,
1947     properties,
1948     additionalProperties,
1949     patternProperties,
1950     isInstanceOf,
1951     imported
1952   ) {
1953     super(schema);
1954     this.properties = properties;
1955     this.additionalProperties = additionalProperties;
1956     this.patternProperties = patternProperties;
1957     this.isInstanceOf = isInstanceOf;
1959     if (imported) {
1960       let [ns, path] = imported;
1961       ns = Schemas.getNamespace(ns);
1962       let importedType = ns.get(path);
1963       if (!importedType) {
1964         throw new Error(`Internal error: imported type ${path} not found`);
1965       }
1967       if (DEBUG && !(importedType instanceof ObjectType)) {
1968         throw new Error(
1969           `Internal error: cannot import non-object type ${path}`
1970         );
1971       }
1973       this.properties = Object.assign(
1974         {},
1975         importedType.properties,
1976         this.properties
1977       );
1978       this.patternProperties = [
1979         ...importedType.patternProperties,
1980         ...this.patternProperties,
1981       ];
1982       this.additionalProperties =
1983         importedType.additionalProperties || this.additionalProperties;
1984     }
1985   }
1987   extend(type) {
1988     for (let key of Object.keys(type.properties)) {
1989       if (key in this.properties) {
1990         throw new Error(
1991           `InternalError: Attempt to extend an object with conflicting property "${key}"`
1992         );
1993       }
1994       this.properties[key] = type.properties[key];
1995     }
1997     this.patternProperties.push(...type.patternProperties);
1999     return this;
2000   }
2002   checkBaseType(baseType) {
2003     return baseType == "object";
2004   }
2006   /**
2007    * Extracts the enumerable properties of the given object, including
2008    * function properties which would normally be omitted by X-ray
2009    * wrappers.
2010    *
2011    * @param {object} value
2012    * @param {Context} context
2013    *        The current parse context.
2014    * @returns {object}
2015    *        An object with an `error` or `value` property.
2016    */
2017   extractProperties(value, context) {
2018     // |value| should be a JS Xray wrapping an object in the
2019     // extension compartment. This works well except when we need to
2020     // access callable properties on |value| since JS Xrays don't
2021     // support those. To work around the problem, we verify that
2022     // |value| is a plain JS object (i.e., not anything scary like a
2023     // Proxy). Then we copy the properties out of it into a normal
2024     // object using a waiver wrapper.
2026     let klass = ChromeUtils.getClassName(value, true);
2027     if (klass != "Object") {
2028       throw context.error(
2029         `Expected a plain JavaScript object, got a ${klass}`,
2030         `be a plain JavaScript object`
2031       );
2032     }
2034     return ChromeUtils.shallowClone(value);
2035   }
2037   checkProperty(context, prop, propType, result, properties, remainingProps) {
2038     let { type, optional, unsupported, onError } = propType;
2039     let error = null;
2041     if (!context.matchManifestVersion(type)) {
2042       if (prop in properties) {
2043         error = context.error(
2044           `Property "${prop}" is unsupported in Manifest Version ${context.manifestVersion}`,
2045           `not contain an unsupported "${prop}" property`
2046         );
2048         context.logWarning(forceString(error.error));
2049         if (this.additionalProperties) {
2050           // When `additionalProperties` is set to UnrecognizedProperty, the
2051           // caller (i.e. ObjectType's normalize method) assigns the original
2052           // value to `result[prop]`. Erase the property now to prevent
2053           // `result[prop]` from becoming anything other than `undefined.
2054           //
2055           // A warning was already logged above, so we do not need to also log
2056           // "An unexpected property was found in the WebExtension manifest."
2057           remainingProps.delete(prop);
2058         }
2059         // When `additionalProperties` is not set, ObjectType's normalize method
2060         // will return an error because prop is still in remainingProps.
2061         return;
2062       }
2063     } else if (unsupported) {
2064       if (prop in properties) {
2065         error = context.error(
2066           `Property "${prop}" is unsupported by Firefox`,
2067           `not contain an unsupported "${prop}" property`
2068         );
2069       }
2070     } else if (prop in properties) {
2071       if (
2072         optional &&
2073         (properties[prop] === null || properties[prop] === undefined)
2074       ) {
2075         result[prop] = propType.default;
2076       } else {
2077         let r = context.withPath(prop, () =>
2078           type.normalize(properties[prop], context)
2079         );
2080         if (r.error) {
2081           error = r;
2082         } else {
2083           result[prop] = r.value;
2084           properties[prop] = r.value;
2085         }
2086       }
2087       remainingProps.delete(prop);
2088     } else if (!optional) {
2089       error = context.error(
2090         `Property "${prop}" is required`,
2091         `contain the required "${prop}" property`
2092       );
2093     } else if (optional !== "omit-key-if-missing") {
2094       result[prop] = propType.default;
2095     }
2097     if (error) {
2098       if (onError == "warn") {
2099         context.logWarning(forceString(error.error));
2100       } else if (onError != "ignore") {
2101         throw error;
2102       }
2104       result[prop] = propType.default;
2105     }
2106   }
2108   normalize(value, context) {
2109     try {
2110       let v = this.normalizeBase("object", value, context);
2111       if (v.error) {
2112         return v;
2113       }
2114       value = v.value;
2116       if (this.isInstanceOf) {
2117         if (DEBUG) {
2118           if (
2119             Object.keys(this.properties).length ||
2120             this.patternProperties.length ||
2121             !(this.additionalProperties instanceof AnyType)
2122           ) {
2123             throw new Error(
2124               "InternalError: isInstanceOf can only be used " +
2125                 "with objects that are otherwise unrestricted"
2126             );
2127           }
2128         }
2130         if (
2131           ChromeUtils.getClassName(value) !== this.isInstanceOf &&
2132           (this.isInstanceOf !== "Element" || value.nodeType !== 1)
2133         ) {
2134           return context.error(
2135             `Object must be an instance of ${this.isInstanceOf}`,
2136             `be an instance of ${this.isInstanceOf}`
2137           );
2138         }
2140         // This is kind of a hack, but we can't normalize things that
2141         // aren't JSON, so we just return them.
2142         return this.postprocess({ value }, context);
2143       }
2145       let properties = this.extractProperties(value, context);
2146       let remainingProps = new Set(Object.keys(properties));
2148       let result = {};
2149       for (let prop of Object.keys(this.properties)) {
2150         this.checkProperty(
2151           context,
2152           prop,
2153           this.properties[prop],
2154           result,
2155           properties,
2156           remainingProps
2157         );
2158       }
2160       for (let prop of Object.keys(properties)) {
2161         for (let { pattern, type } of this.patternProperties) {
2162           if (pattern.test(prop)) {
2163             this.checkProperty(
2164               context,
2165               prop,
2166               type,
2167               result,
2168               properties,
2169               remainingProps
2170             );
2171           }
2172         }
2173       }
2175       if (this.additionalProperties) {
2176         for (let prop of remainingProps) {
2177           let r = context.withPath(prop, () =>
2178             this.additionalProperties.normalize(properties[prop], context)
2179           );
2180           if (r.error) {
2181             return r;
2182           }
2183           result[prop] = r.value;
2184         }
2185       } else if (remainingProps.size == 1) {
2186         return context.error(
2187           `Unexpected property "${[...remainingProps]}"`,
2188           `not contain an unexpected "${[...remainingProps]}" property`
2189         );
2190       } else if (remainingProps.size) {
2191         let props = [...remainingProps].sort().join(", ");
2192         return context.error(
2193           `Unexpected properties: ${props}`,
2194           `not contain the unexpected properties [${props}]`
2195         );
2196       }
2198       return this.postprocess({ value: result }, context);
2199     } catch (e) {
2200       if (e.error) {
2201         return e;
2202       }
2203       throw e;
2204     }
2205   }
2208 // This type is just a placeholder to be referred to by
2209 // SubModuleProperty. No value is ever expected to have this type.
2210 SubModuleType = class SubModuleType extends Type {
2211   static get EXTRA_PROPERTIES() {
2212     return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
2213   }
2215   static parseSchema(root, schema, path, extraProperties = []) {
2216     this.checkSchemaProperties(schema, path, extraProperties);
2218     // The path we pass in here is only used for error messages.
2219     path = [...path, schema.id];
2220     let functions = schema.functions
2221       .filter(fun => !fun.unsupported)
2222       .map(fun => FunctionEntry.parseSchema(root, fun, path));
2224     let events = [];
2226     if (schema.events) {
2227       events = schema.events
2228         .filter(event => !event.unsupported)
2229         .map(event => Event.parseSchema(root, event, path));
2230     }
2232     return new this(schema, functions, events);
2233   }
2235   constructor(schema, functions, events) {
2236     // schema contains properties such as min/max_manifest_version needed
2237     // in the base class so that the Context class can version compare
2238     // any entries against the manifest version.
2239     super(schema);
2240     this.functions = functions;
2241     this.events = events;
2242   }
2245 class NumberType extends Type {
2246   normalize(value, context) {
2247     let r = this.normalizeBase("number", value, context);
2248     if (r.error) {
2249       return r;
2250     }
2252     if (isNaN(r.value) || !Number.isFinite(r.value)) {
2253       return context.error(
2254         "NaN and infinity are not valid",
2255         "be a finite number"
2256       );
2257     }
2259     return r;
2260   }
2262   checkBaseType(baseType) {
2263     return baseType == "number" || baseType == "integer";
2264   }
2267 class IntegerType extends Type {
2268   static get EXTRA_PROPERTIES() {
2269     return ["minimum", "maximum", ...super.EXTRA_PROPERTIES];
2270   }
2272   static parseSchema(root, schema, path, extraProperties = []) {
2273     this.checkSchemaProperties(schema, path, extraProperties);
2275     let { minimum = -Infinity, maximum = Infinity } = schema;
2276     return new this(schema, minimum, maximum);
2277   }
2279   constructor(schema, minimum, maximum) {
2280     super(schema);
2281     this.minimum = minimum;
2282     this.maximum = maximum;
2283   }
2285   normalize(value, context) {
2286     let r = this.normalizeBase("integer", value, context);
2287     if (r.error) {
2288       return r;
2289     }
2290     value = r.value;
2292     // Ensure it's between -2**31 and 2**31-1
2293     if (!Number.isSafeInteger(value)) {
2294       return context.error(
2295         "Integer is out of range",
2296         "be a valid 32 bit signed integer"
2297       );
2298     }
2300     if (value < this.minimum) {
2301       return context.error(
2302         `Integer ${value} is too small (must be at least ${this.minimum})`,
2303         `be at least ${this.minimum}`
2304       );
2305     }
2306     if (value > this.maximum) {
2307       return context.error(
2308         `Integer ${value} is too big (must be at most ${this.maximum})`,
2309         `be no greater than ${this.maximum}`
2310       );
2311     }
2313     return this.postprocess(r, context);
2314   }
2316   checkBaseType(baseType) {
2317     return baseType == "integer";
2318   }
2321 class BooleanType extends Type {
2322   static get EXTRA_PROPERTIES() {
2323     return ["enum", ...super.EXTRA_PROPERTIES];
2324   }
2326   static parseSchema(root, schema, path, extraProperties = []) {
2327     this.checkSchemaProperties(schema, path, extraProperties);
2328     let enumeration = schema.enum || null;
2329     return new this(schema, enumeration);
2330   }
2332   constructor(schema, enumeration) {
2333     super(schema);
2334     this.enumeration = enumeration;
2335   }
2337   normalize(value, context) {
2338     if (!this.checkBaseType(getValueBaseType(value))) {
2339       return context.error(
2340         () => `Expected boolean instead of ${JSON.stringify(value)}`,
2341         `be a boolean`
2342       );
2343     }
2344     value = this.preprocess(value, context);
2345     if (this.enumeration && !this.enumeration.includes(value)) {
2346       return context.error(
2347         () => `Invalid value ${JSON.stringify(value)}`,
2348         `be ${this.enumeration}`
2349       );
2350     }
2351     this.checkDeprecated(context, value);
2352     return { value };
2353   }
2355   checkBaseType(baseType) {
2356     return baseType == "boolean";
2357   }
2360 class ArrayType extends Type {
2361   static get EXTRA_PROPERTIES() {
2362     return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
2363   }
2365   static parseSchema(root, schema, path, extraProperties = []) {
2366     this.checkSchemaProperties(schema, path, extraProperties);
2368     let items = root.parseSchema(schema.items, path, ["onError"]);
2370     return new this(
2371       schema,
2372       items,
2373       schema.minItems || 0,
2374       schema.maxItems || Infinity
2375     );
2376   }
2378   constructor(schema, itemType, minItems, maxItems) {
2379     super(schema);
2380     this.itemType = itemType;
2381     this.minItems = minItems;
2382     this.maxItems = maxItems;
2383     this.onError = schema.items.onError || null;
2384   }
2386   normalize(value, context) {
2387     let v = this.normalizeBase("array", value, context);
2388     if (v.error) {
2389       return v;
2390     }
2391     value = v.value;
2393     let result = [];
2394     for (let [i, element] of value.entries()) {
2395       element = context.withPath(String(i), () =>
2396         this.itemType.normalize(element, context)
2397       );
2398       if (element.error) {
2399         if (this.onError == "warn") {
2400           context.logWarning(forceString(element.error));
2401         } else if (this.onError != "ignore") {
2402           return element;
2403         }
2404         continue;
2405       }
2406       result.push(element.value);
2407     }
2409     if (result.length < this.minItems) {
2410       return context.error(
2411         `Array requires at least ${this.minItems} items; you have ${result.length}`,
2412         `have at least ${this.minItems} items`
2413       );
2414     }
2416     if (result.length > this.maxItems) {
2417       return context.error(
2418         `Array requires at most ${this.maxItems} items; you have ${result.length}`,
2419         `have at most ${this.maxItems} items`
2420       );
2421     }
2423     return this.postprocess({ value: result }, context);
2424   }
2426   checkBaseType(baseType) {
2427     return baseType == "array";
2428   }
2431 class FunctionType extends Type {
2432   static get EXTRA_PROPERTIES() {
2433     return [
2434       "parameters",
2435       "async",
2436       "returns",
2437       "requireUserInput",
2438       ...super.EXTRA_PROPERTIES,
2439     ];
2440   }
2442   static parseSchema(root, schema, path, extraProperties = []) {
2443     this.checkSchemaProperties(schema, path, extraProperties);
2445     let isAsync = !!schema.async;
2446     let isExpectingCallback = typeof schema.async === "string";
2447     let parameters = null;
2448     if ("parameters" in schema) {
2449       parameters = [];
2450       for (let param of schema.parameters) {
2451         // Callbacks default to optional for now, because of promise
2452         // handling.
2453         let isCallback = isAsync && param.name == schema.async;
2454         if (isCallback) {
2455           isExpectingCallback = false;
2456         }
2458         parameters.push({
2459           type: root.parseSchema(param, path, ["name", "optional", "default"]),
2460           name: param.name,
2461           optional: param.optional == null ? isCallback : param.optional,
2462           default: param.default == undefined ? null : param.default,
2463         });
2464       }
2465     }
2466     let hasAsyncCallback = false;
2467     if (isAsync) {
2468       hasAsyncCallback =
2469         parameters &&
2470         parameters.length &&
2471         parameters[parameters.length - 1].name == schema.async;
2472     }
2474     if (DEBUG) {
2475       if (isExpectingCallback) {
2476         throw new Error(
2477           `Internal error: Expected a callback parameter ` +
2478             `with name ${schema.async}`
2479         );
2480       }
2482       if (isAsync && schema.returns) {
2483         throw new Error(
2484           "Internal error: Async functions must not have return values."
2485         );
2486       }
2487       if (
2488         isAsync &&
2489         schema.allowAmbiguousOptionalArguments &&
2490         !hasAsyncCallback
2491       ) {
2492         throw new Error(
2493           "Internal error: Async functions with ambiguous " +
2494             "arguments must declare the callback as the last parameter"
2495         );
2496       }
2497     }
2499     return new this(
2500       schema,
2501       parameters,
2502       isAsync,
2503       hasAsyncCallback,
2504       !!schema.requireUserInput
2505     );
2506   }
2508   constructor(schema, parameters, isAsync, hasAsyncCallback, requireUserInput) {
2509     super(schema);
2510     this.parameters = parameters;
2511     this.isAsync = isAsync;
2512     this.hasAsyncCallback = hasAsyncCallback;
2513     this.requireUserInput = requireUserInput;
2514   }
2516   normalize(value, context) {
2517     return this.normalizeBase("function", value, context);
2518   }
2520   checkBaseType(baseType) {
2521     return baseType == "function";
2522   }
2525 // Represents a "property" defined in a schema namespace with a
2526 // particular value. Essentially this is a constant.
2527 class ValueProperty extends Entry {
2528   constructor(schema, name, value) {
2529     super(schema);
2530     this.name = name;
2531     this.value = value;
2532   }
2534   getDescriptor(path, context) {
2535     // Prevent injection if not a supported version.
2536     if (!context.matchManifestVersion(this)) {
2537       return;
2538     }
2540     return {
2541       descriptor: { value: this.value },
2542     };
2543   }
2546 // Represents a "property" defined in a schema namespace that is not a
2547 // constant.
2548 class TypeProperty extends Entry {
2549   unsupported = false;
2551   constructor(schema, path, name, type, writable, permissions) {
2552     super(schema);
2553     this.path = path;
2554     this.name = name;
2555     this.type = type;
2556     this.writable = writable;
2557     this.permissions = permissions;
2558   }
2560   throwError(context, msg) {
2561     throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2562   }
2564   getDescriptor(path, context) {
2565     if (this.unsupported || !context.matchManifestVersion(this)) {
2566       return;
2567     }
2569     let apiImpl = context.getImplementation(path.join("."), this.name);
2571     let getStub = () => {
2572       this.checkDeprecated(context);
2573       return apiImpl.getProperty();
2574     };
2576     let descriptor = {
2577       get: Cu.exportFunction(getStub, context.cloneScope),
2578     };
2580     if (this.writable) {
2581       let setStub = value => {
2582         let normalized = this.type.normalize(value, context);
2583         if (normalized.error) {
2584           this.throwError(context, forceString(normalized.error));
2585         }
2587         apiImpl.setProperty(normalized.value);
2588       };
2590       descriptor.set = Cu.exportFunction(setStub, context.cloneScope);
2591     }
2593     return {
2594       descriptor,
2595       revoke() {
2596         apiImpl.revoke();
2597         apiImpl = null;
2598       },
2599     };
2600   }
2603 class SubModuleProperty extends Entry {
2604   // A SubModuleProperty represents a tree of objects and properties
2605   // to expose to an extension. Currently we support only a limited
2606   // form of sub-module properties, where "$ref" points to a
2607   // SubModuleType containing a list of functions and "properties" is
2608   // a list of additional simple properties.
2609   //
2610   // name: Name of the property stuff is being added to.
2611   // namespaceName: Namespace in which the property lives.
2612   // reference: Name of the type defining the functions to add to the property.
2613   // properties: Additional properties to add to the module (unsupported).
2614   constructor(root, schema, path, name, reference, properties, permissions) {
2615     super(schema);
2616     this.root = root;
2617     this.name = name;
2618     this.path = path;
2619     this.namespaceName = path.join(".");
2620     this.reference = reference;
2621     this.properties = properties;
2622     this.permissions = permissions;
2623   }
2625   get targetType() {
2626     let ns = this.root.getNamespace(this.namespaceName);
2627     let type = ns.get(this.reference);
2628     if (!type && this.reference.includes(".")) {
2629       let [namespaceName, ref] = this.reference.split(".");
2630       ns = this.root.getNamespace(namespaceName);
2631       type = ns.get(ref);
2632     }
2633     return type;
2634   }
2636   getDescriptor(path, context) {
2637     let obj = Cu.createObjectIn(context.cloneScope);
2639     let ns = this.root.getNamespace(this.namespaceName);
2640     let type = this.targetType;
2642     // Prevent injection if not a supported version.
2643     if (!context.matchManifestVersion(type)) {
2644       return;
2645     }
2647     if (DEBUG) {
2648       if (!type || !(type instanceof SubModuleType)) {
2649         throw new Error(
2650           `Internal error: ${this.namespaceName}.${this.reference} ` +
2651             `is not a sub-module`
2652         );
2653       }
2654     }
2655     let subpath = [...path, this.name];
2657     let functions = type.functions;
2658     for (let fun of functions) {
2659       context.injectInto(fun, obj, fun.name, subpath, ns);
2660     }
2662     let events = type.events;
2663     for (let event of events) {
2664       context.injectInto(event, obj, event.name, subpath, ns);
2665     }
2667     // TODO: Inject this.properties.
2669     return {
2670       descriptor: { value: obj },
2671       revoke() {
2672         let unwrapped = ChromeUtils.waiveXrays(obj);
2673         for (let fun of functions) {
2674           try {
2675             delete unwrapped[fun.name];
2676           } catch (e) {
2677             Cu.reportError(e);
2678           }
2679         }
2680       },
2681     };
2682   }
2685 // This class is a base class for FunctionEntrys and Events. It takes
2686 // care of validating parameter lists (i.e., handling of optional
2687 // parameters and parameter type checking).
2688 class CallEntry extends Entry {
2689   hasAsyncCallback = false;
2691   constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) {
2692     super(schema);
2693     this.path = path;
2694     this.name = name;
2695     this.parameters = parameters;
2696     this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments;
2697   }
2699   throwError(context, msg) {
2700     throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2701   }
2703   checkParameters(args, context) {
2704     let fixedArgs = [];
2706     // First we create a new array, fixedArgs, that is the same as
2707     // |args| but with default values in place of omitted optional parameters.
2708     let check = (parameterIndex, argIndex) => {
2709       if (parameterIndex == this.parameters.length) {
2710         if (argIndex == args.length) {
2711           return true;
2712         }
2713         return false;
2714       }
2716       let parameter = this.parameters[parameterIndex];
2717       if (parameter.optional) {
2718         // Try skipping it.
2719         fixedArgs[parameterIndex] = parameter.default;
2720         if (check(parameterIndex + 1, argIndex)) {
2721           return true;
2722         }
2723       }
2725       if (argIndex == args.length) {
2726         return false;
2727       }
2729       let arg = args[argIndex];
2730       if (!parameter.type.checkBaseType(getValueBaseType(arg))) {
2731         // For Chrome compatibility, use the default value if null or undefined
2732         // is explicitly passed but is not a valid argument in this position.
2733         if (parameter.optional && (arg === null || arg === undefined)) {
2734           fixedArgs[parameterIndex] = Cu.cloneInto(parameter.default, {});
2735         } else {
2736           return false;
2737         }
2738       } else {
2739         fixedArgs[parameterIndex] = arg;
2740       }
2742       return check(parameterIndex + 1, argIndex + 1);
2743     };
2745     if (this.allowAmbiguousOptionalArguments) {
2746       // When this option is set, it's up to the implementation to
2747       // parse arguments.
2748       // The last argument for asynchronous methods is either a function or null.
2749       // This is specifically done for runtime.sendMessage.
2750       if (this.hasAsyncCallback && typeof args[args.length - 1] != "function") {
2751         args.push(null);
2752       }
2753       return args;
2754     }
2755     let success = check(0, 0);
2756     if (!success) {
2757       this.throwError(context, "Incorrect argument types");
2758     }
2760     // Now we normalize (and fully type check) all non-omitted arguments.
2761     fixedArgs = fixedArgs.map((arg, parameterIndex) => {
2762       if (arg === null) {
2763         return null;
2764       }
2765       let parameter = this.parameters[parameterIndex];
2766       let r = parameter.type.normalize(arg, context);
2767       if (r.error) {
2768         this.throwError(
2769           context,
2770           `Type error for parameter ${parameter.name} (${forceString(r.error)})`
2771         );
2772       }
2773       return r.value;
2774     });
2776     return fixedArgs;
2777   }
2780 // Represents a "function" defined in a schema namespace.
2781 FunctionEntry = class FunctionEntry extends CallEntry {
2782   static parseSchema(root, schema, path) {
2783     // When not in DEBUG mode, we just need to know *if* this returns.
2784     /** @type {boolean|object} */
2785     let returns = !!schema.returns;
2786     if (DEBUG && "returns" in schema) {
2787       returns = {
2788         type: root.parseSchema(schema.returns, path, ["optional", "name"]),
2789         optional: schema.returns.optional || false,
2790         name: "result",
2791       };
2792     }
2794     return new this(
2795       schema,
2796       path,
2797       schema.name,
2798       root.parseSchema(schema, path, [
2799         "name",
2800         "unsupported",
2801         "returns",
2802         "permissions",
2803         "allowAmbiguousOptionalArguments",
2804         "allowCrossOriginArguments",
2805       ]),
2806       schema.unsupported || false,
2807       schema.allowAmbiguousOptionalArguments || false,
2808       schema.allowCrossOriginArguments || false,
2809       returns,
2810       schema.permissions || null
2811     );
2812   }
2814   constructor(
2815     schema,
2816     path,
2817     name,
2818     type,
2819     unsupported,
2820     allowAmbiguousOptionalArguments,
2821     allowCrossOriginArguments,
2822     returns,
2823     permissions
2824   ) {
2825     super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
2826     this.unsupported = unsupported;
2827     this.returns = returns;
2828     this.permissions = permissions;
2829     this.allowCrossOriginArguments = allowCrossOriginArguments;
2831     this.isAsync = type.isAsync;
2832     this.hasAsyncCallback = type.hasAsyncCallback;
2833     this.requireUserInput = type.requireUserInput;
2834   }
2836   checkValue({ type, optional, name }, value, context) {
2837     if (optional && value == null) {
2838       return;
2839     }
2840     if (
2841       type.reference === "ExtensionPanel" ||
2842       type.reference === "ExtensionSidebarPane" ||
2843       type.reference === "Port"
2844     ) {
2845       // TODO: We currently treat objects with functions as SubModuleType,
2846       // which is just wrong, and a bigger yak.  Skipping for now.
2847       return;
2848     }
2849     const { error } = type.normalize(value, context);
2850     if (error) {
2851       this.throwError(
2852         context,
2853         `Type error for ${name} value (${forceString(error)})`
2854       );
2855     }
2856   }
2858   checkCallback(args, context) {
2859     const callback = this.parameters[this.parameters.length - 1];
2860     for (const [i, param] of callback.type.parameters.entries()) {
2861       this.checkValue(param, args[i], context);
2862     }
2863   }
2865   getDescriptor(path, context) {
2866     let apiImpl = context.getImplementation(path.join("."), this.name);
2868     let stub;
2869     if (this.isAsync) {
2870       stub = (...args) => {
2871         this.checkDeprecated(context);
2872         let actuals = this.checkParameters(args, context);
2873         let callback = null;
2874         if (this.hasAsyncCallback) {
2875           callback = actuals.pop();
2876         }
2877         if (callback === null && context.isChromeCompat) {
2878           // We pass an empty stub function as a default callback for
2879           // the `chrome` API, so promise objects are not returned,
2880           // and lastError values are reported immediately.
2881           callback = () => {};
2882         }
2883         if (DEBUG && this.hasAsyncCallback && callback) {
2884           let original = callback;
2885           callback = (...args) => {
2886             this.checkCallback(args, context);
2887             original(...args);
2888           };
2889         }
2890         let result = apiImpl.callAsyncFunction(
2891           actuals,
2892           callback,
2893           this.requireUserInput
2894         );
2895         if (DEBUG && this.hasAsyncCallback && !callback) {
2896           return result.then(result => {
2897             this.checkCallback([result], context);
2898             return result;
2899           });
2900         }
2901         return result;
2902       };
2903     } else if (!this.returns) {
2904       stub = (...args) => {
2905         this.checkDeprecated(context);
2906         let actuals = this.checkParameters(args, context);
2907         return apiImpl.callFunctionNoReturn(actuals);
2908       };
2909     } else {
2910       stub = (...args) => {
2911         this.checkDeprecated(context);
2912         let actuals = this.checkParameters(args, context);
2913         let result = apiImpl.callFunction(actuals);
2914         if (DEBUG && this.returns) {
2915           this.checkValue(this.returns, result, context);
2916         }
2917         return result;
2918       };
2919     }
2921     return {
2922       descriptor: {
2923         value: Cu.exportFunction(stub, context.cloneScope, {
2924           allowCrossOriginArguments: this.allowCrossOriginArguments,
2925         }),
2926       },
2927       revoke() {
2928         apiImpl.revoke();
2929         apiImpl = null;
2930       },
2931     };
2932   }
2935 // Represents an "event" defined in a schema namespace.
2937 // TODO Bug 1369722: we should be able to remove the eslint-disable-line that follows
2938 // once Bug 1369722 has been fixed.
2939 // eslint-disable-next-line no-global-assign
2940 Event = class Event extends CallEntry {
2941   static parseSchema(root, event, path) {
2942     let extraParameters = Array.from(event.extraParameters || [], param => ({
2943       type: root.parseSchema(param, path, ["name", "optional", "default"]),
2944       name: param.name,
2945       optional: param.optional || false,
2946       default: param.default == undefined ? null : param.default,
2947     }));
2949     let extraProperties = [
2950       "name",
2951       "unsupported",
2952       "permissions",
2953       "extraParameters",
2954       // We ignore these properties for now.
2955       "returns",
2956       "filters",
2957     ];
2959     return new this(
2960       event,
2961       path,
2962       event.name,
2963       root.parseSchema(event, path, extraProperties),
2964       extraParameters,
2965       event.unsupported || false,
2966       event.permissions || null
2967     );
2968   }
2970   constructor(
2971     schema,
2972     path,
2973     name,
2974     type,
2975     extraParameters,
2976     unsupported,
2977     permissions
2978   ) {
2979     super(schema, path, name, extraParameters);
2980     this.type = type;
2981     this.unsupported = unsupported;
2982     this.permissions = permissions;
2983   }
2985   checkListener(listener, context) {
2986     let r = this.type.normalize(listener, context);
2987     if (r.error) {
2988       this.throwError(context, "Invalid listener");
2989     }
2990     return r.value;
2991   }
2993   getDescriptor(path, context) {
2994     let apiImpl = context.getImplementation(path.join("."), this.name);
2996     let addStub = (listener, ...args) => {
2997       listener = this.checkListener(listener, context);
2998       let actuals = this.checkParameters(args, context);
2999       apiImpl.addListener(listener, actuals);
3000     };
3002     let removeStub = listener => {
3003       listener = this.checkListener(listener, context);
3004       apiImpl.removeListener(listener);
3005     };
3007     let hasStub = listener => {
3008       listener = this.checkListener(listener, context);
3009       return apiImpl.hasListener(listener);
3010     };
3012     let obj = Cu.createObjectIn(context.cloneScope);
3014     Cu.exportFunction(addStub, obj, { defineAs: "addListener" });
3015     Cu.exportFunction(removeStub, obj, { defineAs: "removeListener" });
3016     Cu.exportFunction(hasStub, obj, { defineAs: "hasListener" });
3018     return {
3019       descriptor: { value: obj },
3020       revoke() {
3021         apiImpl.revoke();
3022         apiImpl = null;
3024         let unwrapped = ChromeUtils.waiveXrays(obj);
3025         delete unwrapped.addListener;
3026         delete unwrapped.removeListener;
3027         delete unwrapped.hasListener;
3028       },
3029     };
3030   }
3033 const TYPES = Object.freeze(
3034   Object.assign(Object.create(null), {
3035     any: AnyType,
3036     array: ArrayType,
3037     boolean: BooleanType,
3038     function: FunctionType,
3039     integer: IntegerType,
3040     null: NullType,
3041     number: NumberType,
3042     object: ObjectType,
3043     string: StringType,
3044   })
3047 const LOADERS = {
3048   events: "loadEvent",
3049   functions: "loadFunction",
3050   properties: "loadProperty",
3051   types: "loadType",
3054 class Namespace extends Map {
3055   constructor(root, name, path) {
3056     super();
3058     this.root = root;
3060     this._lazySchemas = [];
3061     this.initialized = false;
3063     this.name = name;
3064     this.path = name ? [...path, name] : [...path];
3066     this.superNamespace = null;
3068     this.min_manifest_version = MIN_MANIFEST_VERSION;
3069     this.max_manifest_version = MAX_MANIFEST_VERSION;
3071     this.permissions = null;
3072     this.allowedContexts = [];
3073     this.defaultContexts = [];
3074   }
3076   /**
3077    * Adds a JSON Schema object to the set of schemas that represent this
3078    * namespace.
3079    *
3080    * @param {object} schema
3081    *        A JSON schema object which partially describes this
3082    *        namespace.
3083    */
3084   addSchema(schema) {
3085     this._lazySchemas.push(schema);
3087     for (let prop of [
3088       "permissions",
3089       "allowedContexts",
3090       "defaultContexts",
3091       "min_manifest_version",
3092       "max_manifest_version",
3093     ]) {
3094       if (schema[prop]) {
3095         this[prop] = schema[prop];
3096       }
3097     }
3099     if (schema.$import) {
3100       this.superNamespace = this.root.getNamespace(schema.$import);
3101     }
3102   }
3104   /**
3105    * Initializes the keys of this namespace based on the schema objects
3106    * added via previous `addSchema` calls.
3107    */
3108   init() {
3109     if (this.initialized) {
3110       return;
3111     }
3113     if (this.superNamespace) {
3114       this._lazySchemas.unshift(...this.superNamespace._lazySchemas);
3115     }
3117     // Keep in sync with LOADERS above.
3118     this.types = new DefaultMap(() => []);
3119     this.properties = new DefaultMap(() => []);
3120     this.functions = new DefaultMap(() => []);
3121     this.events = new DefaultMap(() => []);
3123     for (let schema of this._lazySchemas) {
3124       for (let type of schema.types || []) {
3125         if (!type.unsupported) {
3126           this.types.get(type.$extend || type.id).push(type);
3127         }
3128       }
3130       for (let [name, prop] of Object.entries(schema.properties || {})) {
3131         if (!prop.unsupported) {
3132           this.properties.get(name).push(prop);
3133         }
3134       }
3136       for (let fun of schema.functions || []) {
3137         if (!fun.unsupported) {
3138           this.functions.get(fun.name).push(fun);
3139         }
3140       }
3142       for (let event of schema.events || []) {
3143         if (!event.unsupported) {
3144           this.events.get(event.name).push(event);
3145         }
3146       }
3147     }
3149     // For each type of top-level property in the schema object, iterate
3150     // over all properties of that type, and create a temporary key for
3151     // each property pointing to its type. Those temporary properties
3152     // are later used to instantiate an Entry object based on the actual
3153     // schema object.
3154     for (let type of Object.keys(LOADERS)) {
3155       for (let key of this[type].keys()) {
3156         this.set(key, type);
3157       }
3158     }
3160     this.initialized = true;
3162     if (DEBUG) {
3163       for (let key of this.keys()) {
3164         this.get(key);
3165       }
3166     }
3167   }
3169   /**
3170    * Initializes the value of a given key, by parsing the schema object
3171    * associated with it and replacing its temporary value with an `Entry`
3172    * instance.
3173    *
3174    * @param {string} key
3175    *        The name of the property to initialize.
3176    * @param {string} type
3177    *        The type of property the key represents. Must have a
3178    *        corresponding entry in the `LOADERS` object, pointing to the
3179    *        initialization method for that type.
3180    *
3181    * @returns {Entry}
3182    */
3183   initKey(key, type) {
3184     let loader = LOADERS[type];
3186     for (let schema of this[type].get(key)) {
3187       this.set(key, this[loader](key, schema));
3188     }
3190     return this.get(key);
3191   }
3193   loadType(name, type) {
3194     if ("$extend" in type) {
3195       return this.extendType(type);
3196     }
3197     return this.root.parseSchema(type, this.path, ["id"]);
3198   }
3200   extendType(type) {
3201     let targetType = this.get(type.$extend);
3203     // Only allow extending object and choices types for now.
3204     if (targetType instanceof ObjectType) {
3205       type.type = "object";
3206     } else if (DEBUG) {
3207       if (!targetType) {
3208         throw new Error(
3209           `Internal error: Attempt to extend a nonexistent type ${type.$extend}`
3210         );
3211       } else if (!(targetType instanceof ChoiceType)) {
3212         throw new Error(
3213           `Internal error: Attempt to extend a non-extensible type ${type.$extend}`
3214         );
3215       }
3216     }
3218     let parsed = this.root.parseSchema(type, this.path, ["$extend"]);
3220     if (DEBUG && parsed.constructor !== targetType.constructor) {
3221       throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
3222     }
3224     targetType.extend(parsed);
3226     return targetType;
3227   }
3229   loadProperty(name, prop) {
3230     if ("$ref" in prop) {
3231       if (!prop.unsupported) {
3232         return new SubModuleProperty(
3233           this.root,
3234           prop,
3235           this.path,
3236           name,
3237           prop.$ref,
3238           prop.properties || {},
3239           prop.permissions || null
3240         );
3241       }
3242     } else if ("value" in prop) {
3243       return new ValueProperty(prop, name, prop.value);
3244     } else {
3245       // We ignore the "optional" attribute on properties since we
3246       // don't inject anything here anyway.
3247       let type = this.root.parseSchema(
3248         prop,
3249         [this.name],
3250         ["optional", "permissions", "writable"]
3251       );
3252       return new TypeProperty(
3253         prop,
3254         this.path,
3255         name,
3256         type,
3257         prop.writable || false,
3258         prop.permissions || null
3259       );
3260     }
3261   }
3263   loadFunction(name, fun) {
3264     return FunctionEntry.parseSchema(this.root, fun, this.path);
3265   }
3267   loadEvent(name, event) {
3268     return Event.parseSchema(this.root, event, this.path);
3269   }
3271   /**
3272    * Injects the properties of this namespace into the given object.
3273    *
3274    * @param {object} dest
3275    *        The object into which to inject the namespace properties.
3276    * @param {InjectionContext} context
3277    *        The injection context with which to inject the properties.
3278    */
3279   injectInto(dest, context) {
3280     for (let name of this.keys()) {
3281       // If the entry does not match the manifest version do not
3282       // inject the property.  This prevents the item from being
3283       // enumerable in the namespace object.  We cannot accomplish
3284       // this inside exportLazyProperty, it specifically injects
3285       // an enumerable object.
3286       let entry = this.get(name);
3287       if (!context.matchManifestVersion(entry)) {
3288         continue;
3289       }
3290       exportLazyProperty(dest, name, () => {
3291         let entry = this.get(name);
3293         return context.getDescriptor(entry, dest, name, this.path, this);
3294       });
3295     }
3296   }
3298   getDescriptor(path, context) {
3299     let obj = Cu.createObjectIn(context.cloneScope);
3301     let ns = context.schemaRoot.getNamespace(this.path.join("."));
3302     ns.injectInto(obj, context);
3304     // Only inject the namespace object if it isn't empty.
3305     if (Object.keys(obj).length) {
3306       return {
3307         descriptor: { value: obj },
3308       };
3309     }
3310   }
3312   keys() {
3313     this.init();
3314     return super.keys();
3315   }
3317   /** @returns {Generator<[string, Entry]>} */
3318   *entries() {
3319     for (let key of this.keys()) {
3320       yield [key, this.get(key)];
3321     }
3322   }
3324   get(key) {
3325     this.init();
3326     let value = super.get(key);
3328     // The initial values of lazily-initialized schema properties are
3329     // strings, pointing to the type of property, corresponding to one
3330     // of the entries in the `LOADERS` object.
3331     if (typeof value === "string") {
3332       value = this.initKey(key, value);
3333     }
3335     return value;
3336   }
3338   /**
3339    * Returns a Namespace object for the given namespace name. If a
3340    * namespace object with this name does not already exist, it is
3341    * created. If the name contains any '.' characters, namespaces are
3342    * recursively created, for each dot-separated component.
3343    *
3344    * @param {string} name
3345    *        The name of the sub-namespace to retrieve.
3346    * @param {boolean} [create = true]
3347    *        If true, create any intermediate namespaces which don't
3348    *        exist.
3349    *
3350    * @returns {Namespace}
3351    */
3352   getNamespace(name, create = true) {
3353     let subName;
3355     let idx = name.indexOf(".");
3356     if (idx > 0) {
3357       subName = name.slice(idx + 1);
3358       name = name.slice(0, idx);
3359     }
3361     let ns = super.get(name);
3362     if (!ns) {
3363       if (!create) {
3364         return null;
3365       }
3366       ns = new Namespace(this.root, name, this.path);
3367       this.set(name, ns);
3368     }
3370     if (subName) {
3371       return ns.getNamespace(subName);
3372     }
3373     return ns;
3374   }
3376   getOwnNamespace(name) {
3377     return this.getNamespace(name);
3378   }
3380   has(key) {
3381     this.init();
3382     return super.has(key);
3383   }
3387  * A namespace which combines the children of an arbitrary number of
3388  * sub-namespaces.
3389  */
3390 class Namespaces extends Namespace {
3391   constructor(root, name, path, namespaces) {
3392     super(root, name, path);
3394     this.namespaces = namespaces;
3395   }
3397   injectInto(obj, context) {
3398     for (let ns of this.namespaces) {
3399       ns.injectInto(obj, context);
3400     }
3401   }
3405  * A root schema which combines the contents of an arbitrary number of base
3406  * schema roots.
3407  */
3408 class SchemaRoots extends Namespaces {
3409   constructor(root, bases) {
3410     bases = bases.map(base => base.rootSchema || base);
3412     super(null, "", [], bases);
3414     this.root = root;
3415     this.bases = bases;
3416     this._namespaces = new Map();
3417   }
3419   _getNamespace(name, create) {
3420     let results = [];
3421     for (let root of this.bases) {
3422       let ns = root.getNamespace(name, create);
3423       if (ns) {
3424         results.push(ns);
3425       }
3426     }
3428     if (results.length == 1) {
3429       return results[0];
3430     }
3432     if (results.length) {
3433       return new Namespaces(this.root, name, name.split("."), results);
3434     }
3435     return null;
3436   }
3438   getNamespace(name, create) {
3439     let ns = this._namespaces.get(name);
3440     if (!ns) {
3441       ns = this._getNamespace(name, create);
3442       if (ns) {
3443         this._namespaces.set(name, ns);
3444       }
3445     }
3446     return ns;
3447   }
3449   *getNamespaces(name) {
3450     for (let root of this.bases) {
3451       yield* root.getNamespaces(name);
3452     }
3453   }
3457  * A root schema namespace containing schema data which is isolated from data in
3458  * other schema roots. May extend a base namespace, in which case schemas in
3459  * this root may refer to types in a base, but not vice versa.
3461  * @param {SchemaRoot|Array<SchemaRoot>|null} base
3462  *        A base schema root (or roots) from which to derive, or null.
3463  * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON
3464  *        A map of schema URLs and corresponding JSON blobs from which to
3465  *        populate this root namespace.
3466  */
3467 export class SchemaRoot extends Namespace {
3468   constructor(base, schemaJSON) {
3469     super(null, "", []);
3471     if (Array.isArray(base)) {
3472       base = new SchemaRoots(this, base);
3473     }
3475     this.root = this;
3476     this.base = base;
3477     this.schemaJSON = schemaJSON;
3478   }
3480   *getNamespaces(path) {
3481     let name = path.join(".");
3483     let ns = this.getNamespace(name, false);
3484     if (ns) {
3485       yield ns;
3486     }
3488     if (this.base) {
3489       yield* this.base.getNamespaces(name);
3490     }
3491   }
3493   /**
3494    * Returns the sub-namespace with the given name. If the given namespace
3495    * doesn't already exist, attempts to find it in the base SchemaRoot before
3496    * creating a new empty namespace.
3497    *
3498    * @param {string} name
3499    *        The namespace to retrieve.
3500    * @param {boolean} [create = true]
3501    *        If true, an empty namespace should be created if one does not
3502    *        already exist.
3503    * @returns {Namespace|null}
3504    */
3505   getNamespace(name, create = true) {
3506     let ns = super.getNamespace(name, false);
3507     if (ns) {
3508       return ns;
3509     }
3511     ns = this.base && this.base.getNamespace(name, false);
3512     if (ns) {
3513       return ns;
3514     }
3515     return create && super.getNamespace(name, create);
3516   }
3518   /**
3519    * Like getNamespace, but does not take the base SchemaRoot into account.
3520    *
3521    * @param {string} name
3522    *        The namespace to retrieve.
3523    * @returns {Namespace}
3524    */
3525   getOwnNamespace(name) {
3526     return super.getNamespace(name);
3527   }
3529   parseSchema(schema, path, extraProperties = []) {
3530     let allowedProperties = DEBUG && new Set(extraProperties);
3532     if ("choices" in schema) {
3533       return ChoiceType.parseSchema(this, schema, path, allowedProperties);
3534     } else if ("$ref" in schema) {
3535       return RefType.parseSchema(this, schema, path, allowedProperties);
3536     }
3538     let type = TYPES[schema.type];
3540     if (DEBUG) {
3541       allowedProperties.add("type");
3543       if (!("type" in schema)) {
3544         throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
3545       }
3547       if (!type) {
3548         throw new Error(`Unexpected type ${schema.type}`);
3549       }
3550     }
3552     return type.parseSchema(this, schema, path, allowedProperties);
3553   }
3555   parseSchemas() {
3556     for (let [key, schema] of this.schemaJSON.entries()) {
3557       try {
3558         if (typeof schema.deserialize === "function") {
3559           schema = schema.deserialize(globalThis, isParentProcess);
3561           // If we're in the parent process, we need to keep the
3562           // StructuredCloneHolder blob around in order to send to future child
3563           // processes. If we're in a child, we have no further use for it, so
3564           // just store the deserialized schema data in its place.
3565           if (!isParentProcess) {
3566             this.schemaJSON.set(key, schema);
3567           }
3568         }
3570         this.loadSchema(schema);
3571       } catch (e) {
3572         Cu.reportError(e);
3573       }
3574     }
3575   }
3577   loadSchema(json) {
3578     for (let namespace of json) {
3579       this.getOwnNamespace(namespace.namespace).addSchema(namespace);
3580     }
3581   }
3583   /**
3584    * Checks whether a given object has the necessary permissions to
3585    * expose the given namespace.
3586    *
3587    * @param {string} namespace
3588    *        The top-level namespace to check permissions for.
3589    * @param {object} wrapperFuncs
3590    *        Wrapper functions for the given context.
3591    * @param {Function} wrapperFuncs.hasPermission
3592    *        A function which, when given a string argument, returns true
3593    *        if the context has the given permission.
3594    * @returns {boolean}
3595    *        True if the context has permission for the given namespace.
3596    */
3597   checkPermissions(namespace, wrapperFuncs) {
3598     let ns = this.getNamespace(namespace);
3599     if (ns && ns.permissions) {
3600       return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
3601     }
3602     return true;
3603   }
3605   /**
3606    * Inject registered extension APIs into `dest`.
3607    *
3608    * @param {object} dest The root namespace for the APIs.
3609    *     This object is usually exposed to extensions as "chrome" or "browser".
3610    * @param {object} wrapperFuncs An implementation of the InjectionContext
3611    *     interface, which runs the actual functionality of the generated API.
3612    */
3613   inject(dest, wrapperFuncs) {
3614     let context = new InjectionContext(wrapperFuncs, this);
3616     this.injectInto(dest, context);
3617   }
3619   injectInto(dest, context) {
3620     // For schema graphs where multiple schema roots have the same base, don't
3621     // inject it more than once.
3623     if (!context.injectedRoots.has(this)) {
3624       context.injectedRoots.add(this);
3625       if (this.base) {
3626         this.base.injectInto(dest, context);
3627       }
3628       super.injectInto(dest, context);
3629     }
3630   }
3632   /**
3633    * Normalize `obj` according to the loaded schema for `typeName`.
3634    *
3635    * @param {object} obj The object to normalize against the schema.
3636    * @param {string} typeName The name in the format namespace.propertyname
3637    * @param {object} context An implementation of Context. Any validation errors
3638    *     are reported to the given context.
3639    * @returns {object} The normalized object.
3640    */
3641   normalize(obj, typeName, context) {
3642     let [namespaceName, prop] = typeName.split(".");
3643     let ns = this.getNamespace(namespaceName);
3644     let type = ns.get(prop);
3646     let result = type.normalize(obj, new Context(context));
3647     if (result.error) {
3648       return { error: forceString(result.error) };
3649     }
3650     return result;
3651   }
3654 export var Schemas = {
3655   initialized: false,
3657   REVOKE: Symbol("@@revoke"),
3659   // Maps a schema URL to the JSON contained in that schema file. This
3660   // is useful for sending the JSON across processes.
3661   schemaJSON: new Map(),
3663   // A map of schema JSON which should be available in all content processes.
3664   contentSchemaJSON: new Map(),
3666   // A map of schema JSON which should only be available to extension processes.
3667   privilegedSchemaJSON: new Map(),
3669   _rootSchema: null,
3671   // A weakmap for the validation Context class instances given an extension
3672   // context (keyed by the extensin context instance).
3673   // This is used instead of the InjectionContext for webIDL API validation
3674   // and normalization (see Schemas.checkParameters).
3675   paramsValidationContexts: new DefaultWeakMap(
3676     extContext => new Context(extContext)
3677   ),
3679   get rootSchema() {
3680     if (!this.initialized) {
3681       this.init();
3682     }
3683     if (!this._rootSchema) {
3684       this._rootSchema = new SchemaRoot(null, this.schemaJSON);
3685       this._rootSchema.parseSchemas();
3686     }
3687     return this._rootSchema;
3688   },
3690   getNamespace(name) {
3691     return this.rootSchema.getNamespace(name);
3692   },
3694   init() {
3695     if (this.initialized) {
3696       return;
3697     }
3698     this.initialized = true;
3700     if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
3701       let addSchemas = schemas => {
3702         for (let [key, value] of schemas.entries()) {
3703           this.schemaJSON.set(key, value);
3704         }
3705       };
3707       if (WebExtensionPolicy.isExtensionProcess || DEBUG) {
3708         addSchemas(Services.cpmm.sharedData.get(KEY_PRIVILEGED_SCHEMAS));
3709       }
3711       let schemas = Services.cpmm.sharedData.get(KEY_CONTENT_SCHEMAS);
3712       if (schemas) {
3713         addSchemas(schemas);
3714       }
3715     }
3716   },
3718   _loadCachedSchemasPromise: null,
3719   loadCachedSchemas() {
3720     if (!this._loadCachedSchemasPromise) {
3721       this._loadCachedSchemasPromise = lazy.StartupCache.schemas
3722         .getAll()
3723         .then(results => {
3724           return results;
3725         });
3726     }
3728     return this._loadCachedSchemasPromise;
3729   },
3731   addSchema(url, schema, content = false) {
3732     this.schemaJSON.set(url, schema);
3734     if (content) {
3735       this.contentSchemaJSON.set(url, schema);
3736     } else {
3737       this.privilegedSchemaJSON.set(url, schema);
3738     }
3740     if (this._rootSchema) {
3741       throw new Error("Schema loaded after root schema populated");
3742     }
3743   },
3745   updateSharedSchemas() {
3746     let { sharedData } = Services.ppmm;
3748     sharedData.set(KEY_CONTENT_SCHEMAS, this.contentSchemaJSON);
3749     sharedData.set(KEY_PRIVILEGED_SCHEMAS, this.privilegedSchemaJSON);
3750   },
3752   fetch(url) {
3753     return readJSONAndBlobbify(url);
3754   },
3756   processSchema(json) {
3757     return blobbify(json);
3758   },
3760   async load(url, content = false) {
3761     if (!isParentProcess) {
3762       return;
3763     }
3765     const startTime = Cu.now();
3766     let schemaCache = await this.loadCachedSchemas();
3767     const fromCache = schemaCache.has(url);
3769     let blob =
3770       schemaCache.get(url) ||
3771       (await lazy.StartupCache.schemas.get(url, readJSONAndBlobbify));
3773     if (!this.schemaJSON.has(url)) {
3774       this.addSchema(url, blob, content);
3775     }
3777     ChromeUtils.addProfilerMarker(
3778       "ExtensionSchemas",
3779       { startTime },
3780       `load ${url}, from cache: ${fromCache}`
3781     );
3782   },
3784   /**
3785    * Checks whether a given object has the necessary permissions to
3786    * expose the given namespace.
3787    *
3788    * @param {string} namespace
3789    *        The top-level namespace to check permissions for.
3790    * @param {object} wrapperFuncs
3791    *        Wrapper functions for the given context.
3792    * @param {Function} wrapperFuncs.hasPermission
3793    *        A function which, when given a string argument, returns true
3794    *        if the context has the given permission.
3795    * @returns {boolean}
3796    *        True if the context has permission for the given namespace.
3797    */
3798   checkPermissions(namespace, wrapperFuncs) {
3799     return this.rootSchema.checkPermissions(namespace, wrapperFuncs);
3800   },
3802   /**
3803    * Returns a sorted array of permission names for the given permission types.
3804    *
3805    * @param {Array} types An array of permission types, defaults to all permissions.
3806    * @returns {Array} sorted array of permission names
3807    */
3808   getPermissionNames(
3809     types = [
3810       "Permission",
3811       "OptionalPermission",
3812       "PermissionNoPrompt",
3813       "OptionalPermissionNoPrompt",
3814       "PermissionPrivileged",
3815     ]
3816   ) {
3817     const ns = this.getNamespace("manifest");
3818     let names = [];
3819     for (let typeName of types) {
3820       for (let choice of ns
3821         .get(typeName)
3822         .choices.filter(choice => choice.enumeration)) {
3823         names = names.concat(choice.enumeration);
3824       }
3825     }
3826     return names.sort();
3827   },
3829   exportLazyGetter,
3831   /**
3832    * Inject registered extension APIs into `dest`.
3833    *
3834    * @param {object} dest The root namespace for the APIs.
3835    *     This object is usually exposed to extensions as "chrome" or "browser".
3836    * @param {object} wrapperFuncs An implementation of the InjectionContext
3837    *     interface, which runs the actual functionality of the generated API.
3838    */
3839   inject(dest, wrapperFuncs) {
3840     this.rootSchema.inject(dest, wrapperFuncs);
3841   },
3843   /**
3844    * Normalize `obj` according to the loaded schema for `typeName`.
3845    *
3846    * @param {object} obj The object to normalize against the schema.
3847    * @param {string} typeName The name in the format namespace.propertyname
3848    * @param {object} context An implementation of Context. Any validation errors
3849    *     are reported to the given context.
3850    * @returns {object} The normalized object.
3851    */
3852   normalize(obj, typeName, context) {
3853     return this.rootSchema.normalize(obj, typeName, context);
3854   },
3856   /**
3857    * Validate and normalize the arguments for an API request originated
3858    * from the webIDL API bindings.
3859    *
3860    * This provides for calls originating through WebIDL the parameters
3861    * validation and normalization guarantees that the ext-APINAMESPACE.js
3862    * scripts expects (what InjectionContext does for the regular bindings).
3863    *
3864    * @param {object}                   extContext
3865    * @param {mozIExtensionAPIRequest } apiRequest
3866    *
3867    * @returns {Array<any>} Normalized arguments array.
3868    */
3869   checkWebIDLRequestParameters(extContext, apiRequest) {
3870     const getSchemaForProperty = (schemaObj, propName, schemaPath) => {
3871       if (schemaObj instanceof Namespace) {
3872         return schemaObj?.get(propName);
3873       } else if (schemaObj instanceof SubModuleProperty) {
3874         for (const fun of schemaObj.targetType.functions) {
3875           if (fun.name === propName) {
3876             return fun;
3877           }
3878         }
3880         for (const fun of schemaObj.targetType.events) {
3881           if (fun.name === propName) {
3882             return fun;
3883           }
3884         }
3885       } else if (schemaObj instanceof Event) {
3886         return schemaObj;
3887       }
3889       const schemaPathType = schemaObj?.constructor.name;
3890       throw new Error(
3891         `API Schema for "${propName}" not found in ${schemaPath} (${schemaPath} type is ${schemaPathType})`
3892       );
3893     };
3894     const { requestType, apiNamespace, apiName } = apiRequest;
3896     let [ns, ...rest] = (
3897       ["addListener", "removeListener"].includes(requestType)
3898         ? `${apiNamespace}.${apiName}.${requestType}`
3899         : `${apiNamespace}.${apiName}`
3900     ).split(".");
3901     let apiSchema = this.getNamespace(ns);
3903     // Keep track of the current schema path, populated while navigating the nested API schema
3904     // data and then used to include the full path to the API schema that is hitting unexpected
3905     // errors due to schema data not found or an unexpected schema type.
3906     let schemaPath = [ns];
3908     while (rest.length) {
3909       // Nested property as namespace (e.g. used for proxy.settings requests).
3910       if (!apiSchema) {
3911         throw new Error(`API Schema not found for ${schemaPath.join(".")}`);
3912       }
3914       let [propName, ...newRest] = rest;
3915       rest = newRest;
3917       apiSchema = getSchemaForProperty(
3918         apiSchema,
3919         propName,
3920         schemaPath.join(".")
3921       );
3922       schemaPath.push(propName);
3923     }
3925     if (!apiSchema) {
3926       throw new Error(`API Schema not found for ${schemaPath.join(".")}`);
3927     }
3929     if (!apiSchema.checkParameters) {
3930       throw new Error(
3931         `Unexpected API Schema type for ${schemaPath.join(
3932           "."
3933         )} (${schemaPath.join(".")} type is ${apiSchema.constructor.name})`
3934       );
3935     }
3937     return apiSchema.checkParameters(
3938       apiRequest.args,
3939       this.paramsValidationContexts.get(extContext)
3940     );
3941   },