Bug 1733104 - Implement runtime.getFrameId available from content scripts r=robwu
[gecko.git] / toolkit / components / extensions / Schemas.jsm
blob71fe1e29965d88d09a31caf8f521f63ba5955d5e
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/. */
6 "use strict";
8 const global = this;
10 const { AppConstants } = ChromeUtils.import(
11   "resource://gre/modules/AppConstants.jsm"
13 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14 const { XPCOMUtils } = ChromeUtils.import(
15   "resource://gre/modules/XPCOMUtils.jsm"
18 XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
20 const { ExtensionUtils } = ChromeUtils.import(
21   "resource://gre/modules/ExtensionUtils.jsm"
23 var { DefaultMap, DefaultWeakMap } = ExtensionUtils;
25 ChromeUtils.defineModuleGetter(
26   this,
27   "ExtensionParent",
28   "resource://gre/modules/ExtensionParent.jsm"
30 ChromeUtils.defineModuleGetter(
31   this,
32   "NetUtil",
33   "resource://gre/modules/NetUtil.jsm"
35 ChromeUtils.defineModuleGetter(
36   this,
37   "ShortcutUtils",
38   "resource://gre/modules/ShortcutUtils.jsm"
40 XPCOMUtils.defineLazyServiceGetter(
41   this,
42   "contentPolicyService",
43   "@mozilla.org/addons/content-policy;1",
44   "nsIAddonContentPolicy"
47 XPCOMUtils.defineLazyGetter(
48   this,
49   "StartupCache",
50   () => ExtensionParent.StartupCache
53 XPCOMUtils.defineLazyPreferenceGetter(
54   this,
55   "treatWarningsAsErrors",
56   "extensions.webextensions.warnings-as-errors",
57   false
60 var EXPORTED_SYMBOLS = ["SchemaRoot", "Schemas"];
62 const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content";
63 const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged";
65 const MIN_MANIFEST_VERSION = 2;
66 const MAX_MANIFEST_VERSION = 3;
68 const { DEBUG } = AppConstants;
70 const isParentProcess =
71   Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
73 function readJSON(url) {
74   return new Promise((resolve, reject) => {
75     NetUtil.asyncFetch(
76       { uri: url, loadUsingSystemPrincipal: true },
77       (inputStream, status) => {
78         if (!Components.isSuccessCode(status)) {
79           // Convert status code to a string
80           let e = Components.Exception("", status);
81           reject(new Error(`Error while loading '${url}' (${e.name})`));
82           return;
83         }
84         try {
85           let text = NetUtil.readInputStreamToString(
86             inputStream,
87             inputStream.available()
88           );
90           // Chrome JSON files include a license comment that we need to
91           // strip off for this to be valid JSON. As a hack, we just
92           // look for the first '[' character, which signals the start
93           // of the JSON content.
94           let index = text.indexOf("[");
95           text = text.slice(index);
97           resolve(JSON.parse(text));
98         } catch (e) {
99           reject(e);
100         }
101       }
102     );
103   });
106 function stripDescriptions(json, stripThis = true) {
107   if (Array.isArray(json)) {
108     for (let i = 0; i < json.length; i++) {
109       if (typeof json[i] === "object" && json[i] !== null) {
110         json[i] = stripDescriptions(json[i]);
111       }
112     }
113     return json;
114   }
116   let result = {};
118   // Objects are handled much more efficiently, both in terms of memory and
119   // CPU, if they have the same shape as other objects that serve the same
120   // purpose. So, normalize the order of properties to increase the chances
121   // that the majority of schema objects wind up in large shape groups.
122   for (let key of Object.keys(json).sort()) {
123     if (stripThis && key === "description" && typeof json[key] === "string") {
124       continue;
125     }
127     if (typeof json[key] === "object" && json[key] !== null) {
128       result[key] = stripDescriptions(json[key], key !== "properties");
129     } else {
130       result[key] = json[key];
131     }
132   }
134   return result;
137 function blobbify(json) {
138   // We don't actually use descriptions at runtime, and they make up about a
139   // third of the size of our structured clone data, so strip them before
140   // blobbifying.
141   json = stripDescriptions(json);
143   return new StructuredCloneHolder(json);
146 async function readJSONAndBlobbify(url) {
147   let json = await readJSON(url);
149   return blobbify(json);
153  * Defines a lazy getter for the given property on the given object. Any
154  * security wrappers are waived on the object before the property is
155  * defined, and the getter and setter methods are wrapped for the target
156  * scope.
158  * The given getter function is guaranteed to be called only once, even
159  * if the target scope retrieves the wrapped getter from the property
160  * descriptor and calls it directly.
162  * @param {object} object
163  *        The object on which to define the getter.
164  * @param {string|Symbol} prop
165  *        The property name for which to define the getter.
166  * @param {function} getter
167  *        The function to call in order to generate the final property
168  *        value.
169  */
170 function exportLazyGetter(object, prop, getter) {
171   object = ChromeUtils.waiveXrays(object);
173   let redefine = value => {
174     if (value === undefined) {
175       delete object[prop];
176     } else {
177       Object.defineProperty(object, prop, {
178         enumerable: true,
179         configurable: true,
180         writable: true,
181         value,
182       });
183     }
185     getter = null;
187     return value;
188   };
190   Object.defineProperty(object, prop, {
191     enumerable: true,
192     configurable: true,
194     get: Cu.exportFunction(function() {
195       return redefine(getter.call(this));
196     }, object),
198     set: Cu.exportFunction(value => {
199       redefine(value);
200     }, object),
201   });
205  * Defines a lazily-instantiated property descriptor on the given
206  * object. Any security wrappers are waived on the object before the
207  * property is defined.
209  * The given getter function is guaranteed to be called only once, even
210  * if the target scope retrieves the wrapped getter from the property
211  * descriptor and calls it directly.
213  * @param {object} object
214  *        The object on which to define the getter.
215  * @param {string|Symbol} prop
216  *        The property name for which to define the getter.
217  * @param {function} getter
218  *        The function to call in order to generate the final property
219  *        descriptor object. This will be called, and the property
220  *        descriptor installed on the object, the first time the
221  *        property is written or read. The function may return
222  *        undefined, which will cause the property to be deleted.
223  */
224 function exportLazyProperty(object, prop, getter) {
225   object = ChromeUtils.waiveXrays(object);
227   let redefine = obj => {
228     let desc = getter.call(obj);
229     getter = null;
231     delete object[prop];
232     if (desc) {
233       let defaults = {
234         configurable: true,
235         enumerable: true,
236       };
238       if (!desc.set && !desc.get) {
239         defaults.writable = true;
240       }
242       Object.defineProperty(object, prop, Object.assign(defaults, desc));
243     }
244   };
246   Object.defineProperty(object, prop, {
247     enumerable: true,
248     configurable: true,
250     get: Cu.exportFunction(function() {
251       redefine(this);
252       return object[prop];
253     }, object),
255     set: Cu.exportFunction(function(value) {
256       redefine(this);
257       object[prop] = value;
258     }, object),
259   });
262 const POSTPROCESSORS = {
263   convertImageDataToURL(imageData, context) {
264     let document = context.cloneScope.document;
265     let canvas = document.createElementNS(
266       "http://www.w3.org/1999/xhtml",
267       "canvas"
268     );
269     canvas.width = imageData.width;
270     canvas.height = imageData.height;
271     canvas.getContext("2d").putImageData(imageData, 0, 0);
273     return canvas.toDataURL("image/png");
274   },
275   webRequestBlockingPermissionRequired(string, context) {
276     if (string === "blocking" && !context.hasPermission("webRequestBlocking")) {
277       throw new context.cloneScope.Error(
278         "Using webRequest.addListener with the " +
279           "blocking option requires the 'webRequestBlocking' permission."
280       );
281     }
283     return string;
284   },
285   requireBackgroundServiceWorkerEnabled(value, context) {
286     if (WebExtensionPolicy.backgroundServiceWorkerEnabled) {
287       return value;
288     }
290     // Add an error to the manifest validations and throw the
291     // same error.
292     const msg = "background.service_worker is currently disabled";
293     context.logError(context.makeError(msg));
294     throw new Error(msg);
295   },
297   manifestVersionCheck(value, context) {
298     if (
299       value == 2 ||
300       (value == 3 &&
301         Services.prefs.getBoolPref("extensions.manifestV3.enabled", false))
302     ) {
303       return value;
304     }
305     const msg = `Unsupported manifest version: ${value}`;
306     context.logError(context.makeError(msg));
307     throw new Error(msg);
308   },
311 // Parses a regular expression, with support for the Python extended
312 // syntax that allows setting flags by including the string (?im)
313 function parsePattern(pattern) {
314   let flags = "";
315   let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
316   if (match) {
317     [, flags, pattern] = match;
318   }
319   return new RegExp(pattern, flags);
322 function getValueBaseType(value) {
323   let type = typeof value;
324   switch (type) {
325     case "object":
326       if (value === null) {
327         return "null";
328       }
329       if (Array.isArray(value)) {
330         return "array";
331       }
332       break;
334     case "number":
335       if (value % 1 === 0) {
336         return "integer";
337       }
338   }
339   return type;
342 // Methods of Context that are used by Schemas.normalize. These methods can be
343 // overridden at the construction of Context.
344 const CONTEXT_FOR_VALIDATION = ["checkLoadURL", "hasPermission", "logError"];
346 // Methods of Context that are used by Schemas.inject.
347 // Callers of Schemas.inject should implement all of these methods.
348 const CONTEXT_FOR_INJECTION = [
349   ...CONTEXT_FOR_VALIDATION,
350   "getImplementation",
351   "isPermissionRevokable",
352   "shouldInject",
355 // If the message is a function, call it and return the result.
356 // Otherwise, assume it's a string.
357 function forceString(msg) {
358   if (typeof msg === "function") {
359     return msg();
360   }
361   return msg;
365  * A context for schema validation and error reporting. This class is only used
366  * internally within Schemas.
367  */
368 class Context {
369   /**
370    * @param {object} params Provides the implementation of this class.
371    * @param {Array<string>} overridableMethods
372    */
373   constructor(params, overridableMethods = CONTEXT_FOR_VALIDATION) {
374     this.params = params;
376     if (typeof params.manifestVersion !== "number") {
377       throw new Error(
378         `Unexpected params.manifestVersion value: ${params.manifestVersion}`
379       );
380     }
382     this.path = [];
383     this.preprocessors = {
384       localize(value, context) {
385         return value;
386       },
387     };
388     this.postprocessors = POSTPROCESSORS;
389     this.isChromeCompat = false;
391     this.currentChoices = new Set();
392     this.choicePathIndex = 0;
394     for (let method of overridableMethods) {
395       if (method in params) {
396         this[method] = params[method].bind(params);
397       }
398     }
400     let props = ["preprocessors", "isChromeCompat", "manifestVersion"];
401     for (let prop of props) {
402       if (prop in params) {
403         if (prop in this && typeof this[prop] == "object") {
404           Object.assign(this[prop], params[prop]);
405         } else {
406           this[prop] = params[prop];
407         }
408       }
409     }
410   }
412   get choicePath() {
413     let path = this.path.slice(this.choicePathIndex);
414     return path.join(".");
415   }
417   get cloneScope() {
418     return this.params.cloneScope || undefined;
419   }
421   get url() {
422     return this.params.url;
423   }
425   get principal() {
426     return (
427       this.params.principal ||
428       Services.scriptSecurityManager.createNullPrincipal({})
429     );
430   }
432   /**
433    * Checks whether `url` may be loaded by the extension in this context.
434    *
435    * @param {string} url The URL that the extension wished to load.
436    * @returns {boolean} Whether the context may load `url`.
437    */
438   checkLoadURL(url) {
439     let ssm = Services.scriptSecurityManager;
440     try {
441       ssm.checkLoadURIWithPrincipal(
442         this.principal,
443         Services.io.newURI(url),
444         ssm.DISALLOW_INHERIT_PRINCIPAL
445       );
446     } catch (e) {
447       return false;
448     }
449     return true;
450   }
452   /**
453    * Checks whether this context has the given permission.
454    *
455    * @param {string} permission
456    *        The name of the permission to check.
457    *
458    * @returns {boolean} True if the context has the given permission.
459    */
460   hasPermission(permission) {
461     return false;
462   }
464   /**
465    * Checks whether the given permission can be dynamically revoked or
466    * granted.
467    *
468    * @param {string} permission
469    *        The name of the permission to check.
470    *
471    * @returns {boolean} True if the given permission is revokable.
472    */
473   isPermissionRevokable(permission) {
474     return false;
475   }
477   /**
478    * Returns an error result object with the given message, for return
479    * by Type normalization functions.
480    *
481    * If the context has a `currentTarget` value, this is prepended to
482    * the message to indicate the location of the error.
483    *
484    * @param {string|function} errorMessage
485    *        The error message which will be displayed when this is the
486    *        only possible matching schema. If a function is passed, it
487    *        will be evaluated when the error string is first needed, and
488    *        must return a string.
489    * @param {string|function} choicesMessage
490    *        The message describing the valid what constitutes a valid
491    *        value for this schema, which will be displayed when multiple
492    *        schema choices are available and none match.
493    *
494    *        A caller may pass `null` to prevent a choice from being
495    *        added, but this should *only* be done from code processing a
496    *        choices type.
497    * @param {boolean} [warning = false]
498    *        If true, make message prefixed `Warning`. If false, make message
499    *        prefixed `Error`
500    * @returns {object}
501    */
502   error(errorMessage, choicesMessage = undefined, warning = false) {
503     if (choicesMessage !== null) {
504       let { choicePath } = this;
505       if (choicePath) {
506         choicesMessage = `.${choicePath} must ${choicesMessage}`;
507       }
509       this.currentChoices.add(choicesMessage);
510     }
512     if (this.currentTarget) {
513       let { currentTarget } = this;
514       return {
515         error: () =>
516           `${
517             warning ? "Warning" : "Error"
518           } processing ${currentTarget}: ${forceString(errorMessage)}`,
519       };
520     }
521     return { error: errorMessage };
522   }
524   /**
525    * Creates an `Error` object belonging to the current unprivileged
526    * scope. If there is no unprivileged scope associated with this
527    * context, the message is returned as a string.
528    *
529    * If the context has a `currentTarget` value, this is prepended to
530    * the message, in the same way as for the `error` method.
531    *
532    * @param {string} message
533    * @param {object} [options]
534    * @param {boolean} [options.warning = false]
535    * @returns {Error}
536    */
537   makeError(message, { warning = false } = {}) {
538     let error = forceString(this.error(message, null, warning).error);
539     if (this.cloneScope) {
540       return new this.cloneScope.Error(error);
541     }
542     return error;
543   }
545   /**
546    * Logs the given error to the console. May be overridden to enable
547    * custom logging.
548    *
549    * @param {Error|string} error
550    */
551   logError(error) {
552     if (this.cloneScope) {
553       Cu.reportError(
554         // Error objects logged using Cu.reportError are not associated
555         // to the related innerWindowID. This results in a leaked docshell
556         // since consoleService cannot release the error object when the
557         // extension global is destroyed.
558         typeof error == "string" ? error : String(error),
559         // Report the error with the appropriate stack trace when the
560         // is related to an actual extension global (instead of being
561         // related to a manifest validation).
562         this.principal && ChromeUtils.getCallerLocation(this.principal)
563       );
564     } else {
565       Cu.reportError(error);
566     }
567   }
569   /**
570    * Returns the name of the value currently being normalized. For a
571    * nested object, this is usually approximately equivalent to the
572    * JavaScript property accessor for that property. Given:
573    *
574    *   { foo: { bar: [{ baz: x }] } }
575    *
576    * When processing the value for `x`, the currentTarget is
577    * 'foo.bar.0.baz'
578    */
579   get currentTarget() {
580     return this.path.join(".");
581   }
583   /**
584    * Executes the given callback, and returns an array of choice strings
585    * passed to {@see #error} during its execution.
586    *
587    * @param {function} callback
588    * @returns {object}
589    *          An object with a `result` property containing the return
590    *          value of the callback, and a `choice` property containing
591    *          an array of choices.
592    */
593   withChoices(callback) {
594     let { currentChoices, choicePathIndex } = this;
596     let choices = new Set();
597     this.currentChoices = choices;
598     this.choicePathIndex = this.path.length;
600     try {
601       let result = callback();
603       return { result, choices };
604     } finally {
605       this.currentChoices = currentChoices;
606       this.choicePathIndex = choicePathIndex;
608       if (choices.size == 1) {
609         for (let choice of choices) {
610           currentChoices.add(choice);
611         }
612       } else if (choices.size) {
613         this.error(null, () => {
614           let array = Array.from(choices, forceString);
615           let n = array.length - 1;
616           array[n] = `or ${array[n]}`;
618           return `must either [${array.join(", ")}]`;
619         });
620       }
621     }
622   }
624   /**
625    * Appends the given component to the `currentTarget` path to indicate
626    * that it is being processed, calls the given callback function, and
627    * then restores the original path.
628    *
629    * This is used to identify the path of the property being processed
630    * when reporting type errors.
631    *
632    * @param {string} component
633    * @param {function} callback
634    * @returns {*}
635    */
636   withPath(component, callback) {
637     this.path.push(component);
638     try {
639       return callback();
640     } finally {
641       this.path.pop();
642     }
643   }
645   matchManifestVersion(entry) {
646     let { manifestVersion } = this;
647     return (
648       manifestVersion >= entry.min_manifest_version &&
649       manifestVersion <= entry.max_manifest_version
650     );
651   }
655  * Represents a schema entry to be injected into an object. Handles the
656  * injection, revocation, and permissions of said entry.
658  * @param {InjectionContext} context
659  *        The injection context for the entry.
660  * @param {Entry} entry
661  *        The entry to inject.
662  * @param {object} parentObject
663  *        The object into which to inject this entry.
664  * @param {string} name
665  *        The property name at which to inject this entry.
666  * @param {Array<string>} path
667  *        The full path from the root entry to this entry.
668  * @param {Entry} parentEntry
669  *        The parent entry for the injected entry.
670  */
671 class InjectionEntry {
672   constructor(context, entry, parentObj, name, path, parentEntry) {
673     this.context = context;
674     this.entry = entry;
675     this.parentObj = parentObj;
676     this.name = name;
677     this.path = path;
678     this.parentEntry = parentEntry;
680     this.injected = null;
681     this.lazyInjected = null;
682   }
684   /**
685    * @property {Array<string>} allowedContexts
686    *        The list of allowed contexts into which the entry may be
687    *        injected.
688    */
689   get allowedContexts() {
690     let { allowedContexts } = this.entry;
691     if (allowedContexts.length) {
692       return allowedContexts;
693     }
694     return this.parentEntry.defaultContexts;
695   }
697   /**
698    * @property {boolean} isRevokable
699    *        Returns true if this entry may be dynamically injected or
700    *        revoked based on its permissions.
701    */
702   get isRevokable() {
703     return (
704       this.entry.permissions &&
705       this.entry.permissions.some(perm =>
706         this.context.isPermissionRevokable(perm)
707       )
708     );
709   }
711   /**
712    * @property {boolean} hasPermission
713    *        Returns true if the injection context currently has the
714    *        appropriate permissions to access this entry.
715    */
716   get hasPermission() {
717     return (
718       !this.entry.permissions ||
719       this.entry.permissions.some(perm => this.context.hasPermission(perm))
720     );
721   }
723   /**
724    * @property {boolean} shouldInject
725    *        Returns true if this entry should be injected in the given
726    *        context, without respect to permissions.
727    */
728   get shouldInject() {
729     return (
730       this.context.matchManifestVersion(this.entry) &&
731       this.context.shouldInject(
732         this.path.join("."),
733         this.name,
734         this.allowedContexts
735       )
736     );
737   }
739   /**
740    * Revokes this entry, removing its property from its parent object,
741    * and invalidating its wrappers.
742    */
743   revoke() {
744     if (this.lazyInjected) {
745       this.lazyInjected = false;
746     } else if (this.injected) {
747       if (this.injected.revoke) {
748         this.injected.revoke();
749       }
751       try {
752         let unwrapped = ChromeUtils.waiveXrays(this.parentObj);
753         delete unwrapped[this.name];
754       } catch (e) {
755         Cu.reportError(e);
756       }
758       let { value } = this.injected.descriptor;
759       if (value) {
760         this.context.revokeChildren(value);
761       }
763       this.injected = null;
764     }
765   }
767   /**
768    * Returns a property descriptor object for this entry, if it should
769    * be injected, or undefined if it should not.
770    *
771    * @returns {object?}
772    *        A property descriptor object, or undefined if the property
773    *        should be removed.
774    */
775   getDescriptor() {
776     this.lazyInjected = false;
778     if (this.injected) {
779       let path = [...this.path, this.name];
780       throw new Error(
781         `Attempting to re-inject already injected entry: ${path.join(".")}`
782       );
783     }
785     if (!this.shouldInject) {
786       return;
787     }
789     if (this.isRevokable) {
790       this.context.pendingEntries.add(this);
791     }
793     if (!this.hasPermission) {
794       return;
795     }
797     this.injected = this.entry.getDescriptor(this.path, this.context);
798     if (!this.injected) {
799       return undefined;
800     }
802     return this.injected.descriptor;
803   }
805   /**
806    * Injects a lazy property descriptor into the parent object which
807    * checks permissions and eligibility for injection the first time it
808    * is accessed.
809    */
810   lazyInject() {
811     if (this.lazyInjected || this.injected) {
812       let path = [...this.path, this.name];
813       throw new Error(
814         `Attempting to re-lazy-inject already injected entry: ${path.join(".")}`
815       );
816     }
818     this.lazyInjected = true;
819     exportLazyProperty(this.parentObj, this.name, () => {
820       if (this.lazyInjected) {
821         return this.getDescriptor();
822       }
823     });
824   }
826   /**
827    * Injects or revokes this entry if its current state does not match
828    * the context's current permissions.
829    */
830   permissionsChanged() {
831     if (this.injected) {
832       this.maybeRevoke();
833     } else {
834       this.maybeInject();
835     }
836   }
838   maybeInject() {
839     if (!this.injected && !this.lazyInjected) {
840       this.lazyInject();
841     }
842   }
844   maybeRevoke() {
845     if (this.injected && !this.hasPermission) {
846       this.revoke();
847     }
848   }
852  * Holds methods that run the actual implementation of the extension APIs. These
853  * methods are only called if the extension API invocation matches the signature
854  * as defined in the schema. Otherwise an error is reported to the context.
855  */
856 class InjectionContext extends Context {
857   constructor(params, schemaRoot) {
858     super(params, CONTEXT_FOR_INJECTION);
860     this.schemaRoot = schemaRoot;
862     this.pendingEntries = new Set();
863     this.children = new DefaultWeakMap(() => new Map());
865     this.injectedRoots = new Set();
867     if (params.setPermissionsChangedCallback) {
868       params.setPermissionsChangedCallback(this.permissionsChanged.bind(this));
869     }
870   }
872   /**
873    * Check whether the API should be injected.
874    *
875    * @abstract
876    * @param {string} namespace The namespace of the API. This may contain dots,
877    *     e.g. in the case of "devtools.inspectedWindow".
878    * @param {string} [name] The name of the property in the namespace.
879    *     `null` if we are checking whether the namespace should be injected.
880    * @param {Array<string>} allowedContexts A list of additional contexts in which
881    *     this API should be available. May include any of:
882    *         "main" - The main chrome browser process.
883    *         "addon" - An addon process.
884    *         "content" - A content process.
885    * @returns {boolean} Whether the API should be injected.
886    */
887   shouldInject(namespace, name, allowedContexts) {
888     throw new Error("Not implemented");
889   }
891   /**
892    * Generate the implementation for `namespace`.`name`.
893    *
894    * @abstract
895    * @param {string} namespace The full path to the namespace of the API, minus
896    *     the name of the method or property. E.g. "storage.local".
897    * @param {string} name The name of the method, property or event.
898    * @returns {SchemaAPIInterface} The implementation of the API.
899    */
900   getImplementation(namespace, name) {
901     throw new Error("Not implemented");
902   }
904   /**
905    * Updates all injection entries which may need to be updated after a
906    * permission change, revoking or re-injecting them as necessary.
907    */
908   permissionsChanged() {
909     for (let entry of this.pendingEntries) {
910       try {
911         entry.permissionsChanged();
912       } catch (e) {
913         Cu.reportError(e);
914       }
915     }
916   }
918   /**
919    * Recursively revokes all child injection entries of the given
920    * object.
921    *
922    * @param {object} object
923    *        The object for which to invoke children.
924    */
925   revokeChildren(object) {
926     if (!this.children.has(object)) {
927       return;
928     }
930     let children = this.children.get(object);
931     for (let [name, entry] of children.entries()) {
932       try {
933         entry.revoke();
934       } catch (e) {
935         Cu.reportError(e);
936       }
937       children.delete(name);
939       // When we revoke children for an object, we consider that object
940       // dead. If the entry is ever reified again, a new object is
941       // created, with new child entries.
942       this.pendingEntries.delete(entry);
943     }
944     this.children.delete(object);
945   }
947   _getInjectionEntry(entry, dest, name, path, parentEntry) {
948     let injection = new InjectionEntry(
949       this,
950       entry,
951       dest,
952       name,
953       path,
954       parentEntry
955     );
957     this.children.get(dest).set(name, injection);
959     return injection;
960   }
962   /**
963    * Returns the property descriptor for the given entry.
964    *
965    * @param {Entry} entry
966    *        The entry instance to return a descriptor for.
967    * @param {object} dest
968    *        The object into which this entry is being injected.
969    * @param {string} name
970    *        The property name on the destination object where the entry
971    *        will be injected.
972    * @param {Array<string>} path
973    *        The full path from the root injection object to this entry.
974    * @param {Entry} parentEntry
975    *        The parent entry for this entry.
976    *
977    * @returns {object?}
978    *        A property descriptor object, or null if the entry should
979    *        not be injected.
980    */
981   getDescriptor(entry, dest, name, path, parentEntry) {
982     let injection = this._getInjectionEntry(
983       entry,
984       dest,
985       name,
986       path,
987       parentEntry
988     );
990     return injection.getDescriptor();
991   }
993   /**
994    * Lazily injects the given entry into the given object.
995    *
996    * @param {Entry} entry
997    *        The entry instance to lazily inject.
998    * @param {object} dest
999    *        The object into which to inject this entry.
1000    * @param {string} name
1001    *        The property name at which to inject the entry.
1002    * @param {Array<string>} path
1003    *        The full path from the root injection object to this entry.
1004    * @param {Entry} parentEntry
1005    *        The parent entry for this entry.
1006    */
1007   injectInto(entry, dest, name, path, parentEntry) {
1008     let injection = this._getInjectionEntry(
1009       entry,
1010       dest,
1011       name,
1012       path,
1013       parentEntry
1014     );
1016     injection.lazyInject();
1017   }
1021  * The methods in this singleton represent the "format" specifier for
1022  * JSON Schema string types.
1024  * Each method either returns a normalized version of the original
1025  * value, or throws an error if the value is not valid for the given
1026  * format.
1027  */
1028 const FORMATS = {
1029   hostname(string, context) {
1030     let valid = true;
1032     try {
1033       valid = new URL(`http://${string}`).host === string;
1034     } catch (e) {
1035       valid = false;
1036     }
1038     if (!valid) {
1039       throw new Error(`Invalid hostname ${string}`);
1040     }
1042     return string;
1043   },
1045   url(string, context) {
1046     let url = new URL(string).href;
1048     if (!context.checkLoadURL(url)) {
1049       throw new Error(`Access denied for URL ${url}`);
1050     }
1051     return url;
1052   },
1054   origin(string, context) {
1055     let url;
1056     try {
1057       url = new URL(string);
1058     } catch (e) {
1059       throw new Error(`Invalid origin: ${string}`);
1060     }
1061     if (!/^https?:/.test(url.protocol)) {
1062       throw new Error(`Invalid origin must be http or https for URL ${string}`);
1063     }
1064     // url.origin is punycode so a direct check against string wont work.
1065     // url.href appends a slash even if not in the original string, we we
1066     // additionally check that string does not end in slash.
1067     if (string.endsWith("/") || url.href != new URL(url.origin).href) {
1068       throw new Error(
1069         `Invalid origin for URL ${string}, replace with origin ${url.origin}`
1070       );
1071     }
1072     if (!context.checkLoadURL(url.origin)) {
1073       throw new Error(`Access denied for URL ${url}`);
1074     }
1075     return url.origin;
1076   },
1078   relativeUrl(string, context) {
1079     if (!context.url) {
1080       // If there's no context URL, return relative URLs unresolved, and
1081       // skip security checks for them.
1082       try {
1083         new URL(string);
1084       } catch (e) {
1085         return string;
1086       }
1087     }
1089     let url = new URL(string, context.url).href;
1091     if (!context.checkLoadURL(url)) {
1092       throw new Error(`Access denied for URL ${url}`);
1093     }
1094     return url;
1095   },
1097   strictRelativeUrl(string, context) {
1098     void FORMATS.unresolvedRelativeUrl(string, context);
1099     return FORMATS.relativeUrl(string, context);
1100   },
1102   unresolvedRelativeUrl(string, context) {
1103     if (!string.startsWith("//")) {
1104       try {
1105         new URL(string);
1106       } catch (e) {
1107         return string;
1108       }
1109     }
1111     throw new SyntaxError(
1112       `String ${JSON.stringify(string)} must be a relative URL`
1113     );
1114   },
1116   homepageUrl(string, context) {
1117     // Pipes are used for separating homepages, but we only allow extensions to
1118     // set a single homepage. Encoding any pipes makes it one URL.
1119     return FORMATS.relativeUrl(
1120       string.replace(new RegExp("\\|", "g"), "%7C"),
1121       context
1122     );
1123   },
1125   imageDataOrStrictRelativeUrl(string, context) {
1126     // Do not accept a string which resolves as an absolute URL, or any
1127     // protocol-relative URL, except PNG or JPG data URLs
1128     if (
1129       !string.startsWith("data:image/png;base64,") &&
1130       !string.startsWith("data:image/jpeg;base64,")
1131     ) {
1132       try {
1133         return FORMATS.strictRelativeUrl(string, context);
1134       } catch (e) {
1135         throw new SyntaxError(
1136           `String ${JSON.stringify(
1137             string
1138           )} must be a relative or PNG or JPG data:image URL`
1139         );
1140       }
1141     }
1142     return string;
1143   },
1145   contentSecurityPolicy(string, context) {
1146     // Manifest V3 extension_pages allows localhost.  When sandbox is
1147     // implemented, or any other V3 or later directive, the flags
1148     // logic will need to be updated.
1149     let flags =
1150       context.manifestVersion < 3
1151         ? Ci.nsIAddonContentPolicy.CSP_ALLOW_ANY
1152         : Ci.nsIAddonContentPolicy.CSP_ALLOW_LOCALHOST;
1153     let error = contentPolicyService.validateAddonCSP(string, flags);
1154     if (error != null) {
1155       // The CSP validation error is not reported as part of the "choices" error message,
1156       // we log the CSP validation error explicitly here to make it easier for the addon developers
1157       // to see and fix the extension CSP.
1158       context.logError(`Error processing ${context.currentTarget}: ${error}`);
1159       return null;
1160     }
1161     return string;
1162   },
1164   date(string, context) {
1165     // A valid ISO 8601 timestamp.
1166     const PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/;
1167     if (!PATTERN.test(string)) {
1168       throw new Error(`Invalid date string ${string}`);
1169     }
1170     // Our pattern just checks the format, we could still have invalid
1171     // values (e.g., month=99 or month=02 and day=31).  Let the Date
1172     // constructor do the dirty work of validating.
1173     if (isNaN(new Date(string))) {
1174       throw new Error(`Invalid date string ${string}`);
1175     }
1176     return string;
1177   },
1179   manifestShortcutKey(string, context) {
1180     if (ShortcutUtils.validate(string) == ShortcutUtils.IS_VALID) {
1181       return string;
1182     }
1183     let errorMessage =
1184       `Value "${string}" must consist of ` +
1185       `either a combination of one or two modifiers, including ` +
1186       `a mandatory primary modifier and a key, separated by '+', ` +
1187       `or a media key. For details see: ` +
1188       `https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`;
1189     throw new Error(errorMessage);
1190   },
1192   manifestShortcutKeyOrEmpty(string, context) {
1193     return string === "" ? "" : FORMATS.manifestShortcutKey(string, context);
1194   },
1197 // Schema files contain namespaces, and each namespace contains types,
1198 // properties, functions, and events. An Entry is a base class for
1199 // types, properties, functions, and events.
1200 class Entry {
1201   constructor(schema = {}) {
1202     /**
1203      * If set to any value which evaluates as true, this entry is
1204      * deprecated, and any access to it will result in a deprecation
1205      * warning being logged to the browser console.
1206      *
1207      * If the value is a string, it will be appended to the deprecation
1208      * message. If it contains the substring "${value}", it will be
1209      * replaced with a string representation of the value being
1210      * processed.
1211      *
1212      * If the value is any other truthy value, a generic deprecation
1213      * message will be emitted.
1214      */
1215     this.deprecated = false;
1216     if ("deprecated" in schema) {
1217       this.deprecated = schema.deprecated;
1218     }
1220     /**
1221      * @property {string} [preprocessor]
1222      * If set to a string value, and a preprocessor of the same is
1223      * defined in the validation context, it will be applied to this
1224      * value prior to any normalization.
1225      */
1226     this.preprocessor = schema.preprocess || null;
1228     /**
1229      * @property {string} [postprocessor]
1230      * If set to a string value, and a postprocessor of the same is
1231      * defined in the validation context, it will be applied to this
1232      * value after any normalization.
1233      */
1234     this.postprocessor = schema.postprocess || null;
1236     /**
1237      * @property {Array<string>} allowedContexts A list of allowed contexts
1238      * to consider before generating the API.
1239      * These are not parsed by the schema, but passed to `shouldInject`.
1240      */
1241     this.allowedContexts = schema.allowedContexts || [];
1243     this.min_manifest_version =
1244       schema.min_manifest_version ?? MIN_MANIFEST_VERSION;
1245     this.max_manifest_version =
1246       schema.max_manifest_version ?? MAX_MANIFEST_VERSION;
1247   }
1249   /**
1250    * Preprocess the given value with the preprocessor declared in
1251    * `preprocessor`.
1252    *
1253    * @param {*} value
1254    * @param {Context} context
1255    * @returns {*}
1256    */
1257   preprocess(value, context) {
1258     if (this.preprocessor) {
1259       return context.preprocessors[this.preprocessor](value, context);
1260     }
1261     return value;
1262   }
1264   /**
1265    * Postprocess the given result with the postprocessor declared in
1266    * `postprocessor`.
1267    *
1268    * @param {object} result
1269    * @param {Context} context
1270    * @returns {object}
1271    */
1272   postprocess(result, context) {
1273     if (result.error || !this.postprocessor) {
1274       return result;
1275     }
1277     let value = context.postprocessors[this.postprocessor](
1278       result.value,
1279       context
1280     );
1281     return { value };
1282   }
1284   /**
1285    * Logs a deprecation warning for this entry, based on the value of
1286    * its `deprecated` property.
1287    *
1288    * @param {Context} context
1289    * @param {value} [value]
1290    */
1291   logDeprecation(context, value = null) {
1292     let message = "This property is deprecated";
1293     if (typeof this.deprecated == "string") {
1294       message = this.deprecated;
1295       if (message.includes("${value}")) {
1296         try {
1297           value = JSON.stringify(value);
1298         } catch (e) {
1299           value = String(value);
1300         }
1301         message = message.replace(/\$\{value\}/g, () => value);
1302       }
1303     }
1305     this.logWarning(context, message);
1306   }
1308   /**
1309    * @param {Context} context
1310    * @param {string} warningMessage
1311    */
1312   logWarning(context, warningMessage) {
1313     let error = context.makeError(warningMessage, { warning: true });
1314     context.logError(error);
1316     if (treatWarningsAsErrors) {
1317       // This pref is false by default, and true by default in tests to
1318       // discourage the use of deprecated APIs in our unit tests.
1319       // If a warning is an expected part of a test, temporarily set the pref
1320       // to false, e.g. with the ExtensionTestUtils.failOnSchemaWarnings helper.
1321       Services.console.logStringMessage(
1322         "Treating warning as error because the preference " +
1323           "extensions.webextensions.warnings-as-errors is set to true"
1324       );
1325       if (typeof error === "string") {
1326         error = new Error(error);
1327       }
1328       throw error;
1329     }
1330   }
1332   /**
1333    * Checks whether the entry is deprecated and, if so, logs a
1334    * deprecation message.
1335    *
1336    * @param {Context} context
1337    * @param {value} [value]
1338    */
1339   checkDeprecated(context, value = null) {
1340     if (this.deprecated) {
1341       this.logDeprecation(context, value);
1342     }
1343   }
1345   /**
1346    * Returns an object containing property descriptor for use when
1347    * injecting this entry into an API object.
1348    *
1349    * @param {Array<string>} path The API path, e.g. `["storage", "local"]`.
1350    * @param {InjectionContext} context
1351    *
1352    * @returns {object?}
1353    *        An object containing a `descriptor` property, specifying the
1354    *        entry's property descriptor, and an optional `revoke`
1355    *        method, to be called when the entry is being revoked.
1356    */
1357   getDescriptor(path, context) {
1358     return undefined;
1359   }
1362 // Corresponds either to a type declared in the "types" section of the
1363 // schema or else to any type object used throughout the schema.
1364 class Type extends Entry {
1365   /**
1366    * @property {Array<string>} EXTRA_PROPERTIES
1367    *        An array of extra properties which may be present for
1368    *        schemas of this type.
1369    */
1370   static get EXTRA_PROPERTIES() {
1371     return [
1372       "description",
1373       "deprecated",
1374       "preprocess",
1375       "postprocess",
1376       "allowedContexts",
1377       "min_manifest_version",
1378       "max_manifest_version",
1379     ];
1380   }
1382   /**
1383    * Parses the given schema object and returns an instance of this
1384    * class which corresponds to its properties.
1385    *
1386    * @param {SchemaRoot} root
1387    *        The root schema for this type.
1388    * @param {object} schema
1389    *        A JSON schema object which corresponds to a definition of
1390    *        this type.
1391    * @param {Array<string>} path
1392    *        The path to this schema object from the root schema,
1393    *        corresponding to the property names and array indices
1394    *        traversed during parsing in order to arrive at this schema
1395    *        object.
1396    * @param {Array<string>} [extraProperties]
1397    *        An array of extra property names which are valid for this
1398    *        schema in the current context.
1399    * @returns {Type}
1400    *        An instance of this type which corresponds to the given
1401    *        schema object.
1402    * @static
1403    */
1404   static parseSchema(root, schema, path, extraProperties = []) {
1405     this.checkSchemaProperties(schema, path, extraProperties);
1407     return new this(schema);
1408   }
1410   /**
1411    * Checks that all of the properties present in the given schema
1412    * object are valid properties for this type, and throws if invalid.
1413    *
1414    * @param {object} schema
1415    *        A JSON schema object.
1416    * @param {Array<string>} path
1417    *        The path to this schema object from the root schema,
1418    *        corresponding to the property names and array indices
1419    *        traversed during parsing in order to arrive at this schema
1420    *        object.
1421    * @param {Array<string>} [extra]
1422    *        An array of extra property names which are valid for this
1423    *        schema in the current context.
1424    * @throws {Error}
1425    *        An error describing the first invalid property found in the
1426    *        schema object.
1427    */
1428   static checkSchemaProperties(schema, path, extra = []) {
1429     if (DEBUG) {
1430       let allowedSet = new Set([...this.EXTRA_PROPERTIES, ...extra]);
1432       for (let prop of Object.keys(schema)) {
1433         if (!allowedSet.has(prop)) {
1434           throw new Error(
1435             `Internal error: Namespace ${path.join(".")} has ` +
1436               `invalid type property "${prop}" ` +
1437               `in type "${schema.id || JSON.stringify(schema)}"`
1438           );
1439         }
1440       }
1441     }
1442   }
1444   // Takes a value, checks that it has the correct type, and returns a
1445   // "normalized" version of the value. The normalized version will
1446   // include "nulls" in place of omitted optional properties. The
1447   // result of this function is either {error: "Some type error"} or
1448   // {value: <normalized-value>}.
1449   normalize(value, context) {
1450     return context.error("invalid type");
1451   }
1453   // Unlike normalize, this function does a shallow check to see if
1454   // |baseType| (one of the possible getValueBaseType results) is
1455   // valid for this type. It returns true or false. It's used to fill
1456   // in optional arguments to functions before actually type checking
1458   checkBaseType(baseType) {
1459     return false;
1460   }
1462   // Helper method that simply relies on checkBaseType to implement
1463   // normalize. Subclasses can choose to use it or not.
1464   normalizeBase(type, value, context) {
1465     if (this.checkBaseType(getValueBaseType(value))) {
1466       this.checkDeprecated(context, value);
1467       return { value: this.preprocess(value, context) };
1468     }
1470     let choice;
1471     if ("aeiou".includes(type[0])) {
1472       choice = `be an ${type} value`;
1473     } else {
1474       choice = `be a ${type} value`;
1475     }
1477     return context.error(
1478       () => `Expected ${type} instead of ${JSON.stringify(value)}`,
1479       choice
1480     );
1481   }
1484 // Type that allows any value.
1485 class AnyType extends Type {
1486   normalize(value, context) {
1487     this.checkDeprecated(context, value);
1488     return this.postprocess({ value }, context);
1489   }
1491   checkBaseType(baseType) {
1492     return true;
1493   }
1496 // An untagged union type.
1497 class ChoiceType extends Type {
1498   static get EXTRA_PROPERTIES() {
1499     return ["choices", ...super.EXTRA_PROPERTIES];
1500   }
1502   static parseSchema(root, schema, path, extraProperties = []) {
1503     this.checkSchemaProperties(schema, path, extraProperties);
1505     let choices = schema.choices.map(t => root.parseSchema(t, path));
1506     return new this(schema, choices);
1507   }
1509   constructor(schema, choices) {
1510     super(schema);
1511     this.choices = choices;
1512   }
1514   extend(type) {
1515     this.choices.push(...type.choices);
1517     return this;
1518   }
1520   normalize(value, context) {
1521     this.checkDeprecated(context, value);
1523     let error;
1524     let { choices, result } = context.withChoices(() => {
1525       for (let choice of this.choices) {
1526         // Ignore a possible choice if it is not supported by
1527         // the manifest version we are normalizing.
1528         if (!context.matchManifestVersion(choice)) {
1529           continue;
1530         }
1532         let r = choice.normalize(value, context);
1533         if (!r.error) {
1534           return r;
1535         }
1537         error = r;
1538       }
1539     });
1541     if (result) {
1542       return result;
1543     }
1544     if (choices.size <= 1) {
1545       return error;
1546     }
1548     choices = Array.from(choices, forceString);
1549     let n = choices.length - 1;
1550     choices[n] = `or ${choices[n]}`;
1552     let message;
1553     if (typeof value === "object") {
1554       message = () => `Value must either: ${choices.join(", ")}`;
1555     } else {
1556       message = () =>
1557         `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`;
1558     }
1560     return context.error(message, null);
1561   }
1563   checkBaseType(baseType) {
1564     return this.choices.some(t => t.checkBaseType(baseType));
1565   }
1567   getDescriptor(path, context) {
1568     // In StringType.getDescriptor, unlike any other Type, a descriptor is returned if
1569     // it is an enumeration.  Since we need versioned choices in some cases, here we
1570     // build a list of valid enumerations that will work for a given manifest version.
1571     if (
1572       !this.choices.length ||
1573       !this.choices.every(t => t.checkBaseType("string") && t.enumeration)
1574     ) {
1575       return;
1576     }
1578     let obj = Cu.createObjectIn(context.cloneScope);
1579     let descriptor = { value: obj };
1580     for (let choice of this.choices) {
1581       // Ignore a possible choice if it is not supported by
1582       // the manifest version we are normalizing.
1583       if (!context.matchManifestVersion(choice)) {
1584         continue;
1585       }
1586       let d = choice.getDescriptor(path, context);
1587       if (d) {
1588         Object.assign(obj, d.descriptor.value);
1589       }
1590     }
1592     return { descriptor };
1593   }
1596 // This is a reference to another type--essentially a typedef.
1597 class RefType extends Type {
1598   static get EXTRA_PROPERTIES() {
1599     return ["$ref", ...super.EXTRA_PROPERTIES];
1600   }
1602   static parseSchema(root, schema, path, extraProperties = []) {
1603     this.checkSchemaProperties(schema, path, extraProperties);
1605     let ref = schema.$ref;
1606     let ns = path.join(".");
1607     if (ref.includes(".")) {
1608       [, ns, ref] = /^(.*)\.(.*?)$/.exec(ref);
1609     }
1610     return new this(root, schema, ns, ref);
1611   }
1613   // For a reference to a type named T declared in namespace NS,
1614   // namespaceName will be NS and reference will be T.
1615   constructor(root, schema, namespaceName, reference) {
1616     super(schema);
1617     this.root = root;
1618     this.namespaceName = namespaceName;
1619     this.reference = reference;
1620   }
1622   get targetType() {
1623     let ns = this.root.getNamespace(this.namespaceName);
1624     let type = ns.get(this.reference);
1625     if (!type) {
1626       throw new Error(`Internal error: Type ${this.reference} not found`);
1627     }
1628     return type;
1629   }
1631   normalize(value, context) {
1632     this.checkDeprecated(context, value);
1633     return this.targetType.normalize(value, context);
1634   }
1636   checkBaseType(baseType) {
1637     return this.targetType.checkBaseType(baseType);
1638   }
1641 class StringType extends Type {
1642   static get EXTRA_PROPERTIES() {
1643     return [
1644       "enum",
1645       "minLength",
1646       "maxLength",
1647       "pattern",
1648       "format",
1649       ...super.EXTRA_PROPERTIES,
1650     ];
1651   }
1653   static parseSchema(root, schema, path, extraProperties = []) {
1654     this.checkSchemaProperties(schema, path, extraProperties);
1656     let enumeration = schema.enum || null;
1657     if (enumeration) {
1658       // The "enum" property is either a list of strings that are
1659       // valid values or else a list of {name, description} objects,
1660       // where the .name values are the valid values.
1661       enumeration = enumeration.map(e => {
1662         if (typeof e == "object") {
1663           return e.name;
1664         }
1665         return e;
1666       });
1667     }
1669     let pattern = null;
1670     if (schema.pattern) {
1671       try {
1672         pattern = parsePattern(schema.pattern);
1673       } catch (e) {
1674         throw new Error(
1675           `Internal error: Invalid pattern ${JSON.stringify(schema.pattern)}`
1676         );
1677       }
1678     }
1680     let format = null;
1681     if (schema.format) {
1682       if (!(schema.format in FORMATS)) {
1683         throw new Error(
1684           `Internal error: Invalid string format ${schema.format}`
1685         );
1686       }
1687       format = FORMATS[schema.format];
1688     }
1689     return new this(
1690       schema,
1691       schema.id || undefined,
1692       enumeration,
1693       schema.minLength || 0,
1694       schema.maxLength || Infinity,
1695       pattern,
1696       format
1697     );
1698   }
1700   constructor(
1701     schema,
1702     name,
1703     enumeration,
1704     minLength,
1705     maxLength,
1706     pattern,
1707     format
1708   ) {
1709     super(schema);
1710     this.name = name;
1711     this.enumeration = enumeration;
1712     this.minLength = minLength;
1713     this.maxLength = maxLength;
1714     this.pattern = pattern;
1715     this.format = format;
1716   }
1718   normalize(value, context) {
1719     let r = this.normalizeBase("string", value, context);
1720     if (r.error) {
1721       return r;
1722     }
1723     value = r.value;
1725     if (this.enumeration) {
1726       if (this.enumeration.includes(value)) {
1727         return this.postprocess({ value }, context);
1728       }
1730       let choices = this.enumeration.map(JSON.stringify).join(", ");
1732       return context.error(
1733         () => `Invalid enumeration value ${JSON.stringify(value)}`,
1734         `be one of [${choices}]`
1735       );
1736     }
1738     if (value.length < this.minLength) {
1739       return context.error(
1740         () =>
1741           `String ${JSON.stringify(value)} is too short (must be ${
1742             this.minLength
1743           })`,
1744         `be longer than ${this.minLength}`
1745       );
1746     }
1747     if (value.length > this.maxLength) {
1748       return context.error(
1749         () =>
1750           `String ${JSON.stringify(value)} is too long (must be ${
1751             this.maxLength
1752           })`,
1753         `be shorter than ${this.maxLength}`
1754       );
1755     }
1757     if (this.pattern && !this.pattern.test(value)) {
1758       return context.error(
1759         () => `String ${JSON.stringify(value)} must match ${this.pattern}`,
1760         `match the pattern ${this.pattern.toSource()}`
1761       );
1762     }
1764     if (this.format) {
1765       try {
1766         r.value = this.format(r.value, context);
1767       } catch (e) {
1768         return context.error(
1769           String(e),
1770           `match the format "${this.format.name}"`
1771         );
1772       }
1773     }
1775     return r;
1776   }
1778   checkBaseType(baseType) {
1779     return baseType == "string";
1780   }
1782   getDescriptor(path, context) {
1783     if (this.enumeration) {
1784       let obj = Cu.createObjectIn(context.cloneScope);
1786       for (let e of this.enumeration) {
1787         obj[e.toUpperCase()] = e;
1788       }
1790       return {
1791         descriptor: { value: obj },
1792       };
1793     }
1794   }
1797 class NullType extends Type {
1798   normalize(value, context) {
1799     return this.normalizeBase("null", value, context);
1800   }
1802   checkBaseType(baseType) {
1803     return baseType == "null";
1804   }
1807 let FunctionEntry;
1808 let Event;
1809 let SubModuleType;
1811 class ObjectType extends Type {
1812   static get EXTRA_PROPERTIES() {
1813     return [
1814       "properties",
1815       "patternProperties",
1816       "$import",
1817       ...super.EXTRA_PROPERTIES,
1818     ];
1819   }
1821   static parseSchema(root, schema, path, extraProperties = []) {
1822     if ("functions" in schema) {
1823       return SubModuleType.parseSchema(root, schema, path, extraProperties);
1824     }
1826     if (DEBUG && !("$extend" in schema)) {
1827       // Only allow extending "properties" and "patternProperties".
1828       extraProperties = [
1829         "additionalProperties",
1830         "isInstanceOf",
1831         ...extraProperties,
1832       ];
1833     }
1834     this.checkSchemaProperties(schema, path, extraProperties);
1836     let imported = null;
1837     if ("$import" in schema) {
1838       let importPath = schema.$import;
1839       let idx = importPath.indexOf(".");
1840       if (idx === -1) {
1841         imported = [path[0], importPath];
1842       } else {
1843         imported = [importPath.slice(0, idx), importPath.slice(idx + 1)];
1844       }
1845     }
1847     let parseProperty = (schema, extraProps = []) => {
1848       return {
1849         type: root.parseSchema(
1850           schema,
1851           path,
1852           DEBUG && [
1853             "unsupported",
1854             "onError",
1855             "permissions",
1856             "default",
1857             ...extraProps,
1858           ]
1859         ),
1860         optional: schema.optional || false,
1861         unsupported: schema.unsupported || false,
1862         onError: schema.onError || null,
1863         default: schema.default === undefined ? null : schema.default,
1864       };
1865     };
1867     // Parse explicit "properties" object.
1868     let properties = Object.create(null);
1869     for (let propName of Object.keys(schema.properties || {})) {
1870       properties[propName] = parseProperty(schema.properties[propName], [
1871         "optional",
1872       ]);
1873     }
1875     // Parse regexp properties from "patternProperties" object.
1876     let patternProperties = [];
1877     for (let propName of Object.keys(schema.patternProperties || {})) {
1878       let pattern;
1879       try {
1880         pattern = parsePattern(propName);
1881       } catch (e) {
1882         throw new Error(
1883           `Internal error: Invalid property pattern ${JSON.stringify(propName)}`
1884         );
1885       }
1887       patternProperties.push({
1888         pattern,
1889         type: parseProperty(schema.patternProperties[propName]),
1890       });
1891     }
1893     // Parse "additionalProperties" schema.
1894     let additionalProperties = null;
1895     if (schema.additionalProperties) {
1896       let type = schema.additionalProperties;
1897       if (type === true) {
1898         type = { type: "any" };
1899       }
1901       additionalProperties = root.parseSchema(type, path);
1902     }
1904     return new this(
1905       schema,
1906       properties,
1907       additionalProperties,
1908       patternProperties,
1909       schema.isInstanceOf || null,
1910       imported
1911     );
1912   }
1914   constructor(
1915     schema,
1916     properties,
1917     additionalProperties,
1918     patternProperties,
1919     isInstanceOf,
1920     imported
1921   ) {
1922     super(schema);
1923     this.properties = properties;
1924     this.additionalProperties = additionalProperties;
1925     this.patternProperties = patternProperties;
1926     this.isInstanceOf = isInstanceOf;
1928     if (imported) {
1929       let [ns, path] = imported;
1930       ns = Schemas.getNamespace(ns);
1931       let importedType = ns.get(path);
1932       if (!importedType) {
1933         throw new Error(`Internal error: imported type ${path} not found`);
1934       }
1936       if (DEBUG && !(importedType instanceof ObjectType)) {
1937         throw new Error(
1938           `Internal error: cannot import non-object type ${path}`
1939         );
1940       }
1942       this.properties = Object.assign(
1943         {},
1944         importedType.properties,
1945         this.properties
1946       );
1947       this.patternProperties = [
1948         ...importedType.patternProperties,
1949         ...this.patternProperties,
1950       ];
1951       this.additionalProperties =
1952         importedType.additionalProperties || this.additionalProperties;
1953     }
1954   }
1956   extend(type) {
1957     for (let key of Object.keys(type.properties)) {
1958       if (key in this.properties) {
1959         throw new Error(
1960           `InternalError: Attempt to extend an object with conflicting property "${key}"`
1961         );
1962       }
1963       this.properties[key] = type.properties[key];
1964     }
1966     this.patternProperties.push(...type.patternProperties);
1968     return this;
1969   }
1971   checkBaseType(baseType) {
1972     return baseType == "object";
1973   }
1975   /**
1976    * Extracts the enumerable properties of the given object, including
1977    * function properties which would normally be omitted by X-ray
1978    * wrappers.
1979    *
1980    * @param {object} value
1981    * @param {Context} context
1982    *        The current parse context.
1983    * @returns {object}
1984    *        An object with an `error` or `value` property.
1985    */
1986   extractProperties(value, context) {
1987     // |value| should be a JS Xray wrapping an object in the
1988     // extension compartment. This works well except when we need to
1989     // access callable properties on |value| since JS Xrays don't
1990     // support those. To work around the problem, we verify that
1991     // |value| is a plain JS object (i.e., not anything scary like a
1992     // Proxy). Then we copy the properties out of it into a normal
1993     // object using a waiver wrapper.
1995     let klass = ChromeUtils.getClassName(value, true);
1996     if (klass != "Object") {
1997       throw context.error(
1998         `Expected a plain JavaScript object, got a ${klass}`,
1999         `be a plain JavaScript object`
2000       );
2001     }
2003     return ChromeUtils.shallowClone(value);
2004   }
2006   checkProperty(context, prop, propType, result, properties, remainingProps) {
2007     let { type, optional, unsupported, onError } = propType;
2008     let error = null;
2010     if (!context.matchManifestVersion(type)) {
2011       if (prop in properties) {
2012         error = context.error(
2013           `Property "${prop}" is unsupported in Manifest Version ${context.manifestVersion}`,
2014           `not contain an unsupported "${prop}" property`
2015         );
2016         if (context.manifestVersion === 2) {
2017           // Existing MV2 extensions might have some of the new MV3 properties.
2018           // Since we've ignored them till now, we should just warn and bail.
2019           this.logWarning(context, forceString(error.error));
2020           return;
2021         }
2022       }
2023     } else if (unsupported) {
2024       if (prop in properties) {
2025         error = context.error(
2026           `Property "${prop}" is unsupported by Firefox`,
2027           `not contain an unsupported "${prop}" property`
2028         );
2029       }
2030     } else if (prop in properties) {
2031       if (
2032         optional &&
2033         (properties[prop] === null || properties[prop] === undefined)
2034       ) {
2035         result[prop] = propType.default;
2036       } else {
2037         let r = context.withPath(prop, () =>
2038           type.normalize(properties[prop], context)
2039         );
2040         if (r.error) {
2041           error = r;
2042         } else {
2043           result[prop] = r.value;
2044           properties[prop] = r.value;
2045         }
2046       }
2047       remainingProps.delete(prop);
2048     } else if (!optional) {
2049       error = context.error(
2050         `Property "${prop}" is required`,
2051         `contain the required "${prop}" property`
2052       );
2053     } else if (optional !== "omit-key-if-missing") {
2054       result[prop] = propType.default;
2055     }
2057     if (error) {
2058       if (onError == "warn") {
2059         this.logWarning(context, forceString(error.error));
2060       } else if (onError != "ignore") {
2061         throw error;
2062       }
2064       result[prop] = propType.default;
2065     }
2066   }
2068   normalize(value, context) {
2069     try {
2070       let v = this.normalizeBase("object", value, context);
2071       if (v.error) {
2072         return v;
2073       }
2074       value = v.value;
2076       if (this.isInstanceOf) {
2077         if (DEBUG) {
2078           if (
2079             Object.keys(this.properties).length ||
2080             this.patternProperties.length ||
2081             !(this.additionalProperties instanceof AnyType)
2082           ) {
2083             throw new Error(
2084               "InternalError: isInstanceOf can only be used " +
2085                 "with objects that are otherwise unrestricted"
2086             );
2087           }
2088         }
2090         if (
2091           ChromeUtils.getClassName(value) !== this.isInstanceOf &&
2092           (this.isInstanceOf !== "Element" || value.nodeType !== 1)
2093         ) {
2094           return context.error(
2095             `Object must be an instance of ${this.isInstanceOf}`,
2096             `be an instance of ${this.isInstanceOf}`
2097           );
2098         }
2100         // This is kind of a hack, but we can't normalize things that
2101         // aren't JSON, so we just return them.
2102         return this.postprocess({ value }, context);
2103       }
2105       let properties = this.extractProperties(value, context);
2106       let remainingProps = new Set(Object.keys(properties));
2108       let result = {};
2109       for (let prop of Object.keys(this.properties)) {
2110         this.checkProperty(
2111           context,
2112           prop,
2113           this.properties[prop],
2114           result,
2115           properties,
2116           remainingProps
2117         );
2118       }
2120       for (let prop of Object.keys(properties)) {
2121         for (let { pattern, type } of this.patternProperties) {
2122           if (pattern.test(prop)) {
2123             this.checkProperty(
2124               context,
2125               prop,
2126               type,
2127               result,
2128               properties,
2129               remainingProps
2130             );
2131           }
2132         }
2133       }
2135       if (this.additionalProperties) {
2136         for (let prop of remainingProps) {
2137           let r = context.withPath(prop, () =>
2138             this.additionalProperties.normalize(properties[prop], context)
2139           );
2140           if (r.error) {
2141             return r;
2142           }
2143           result[prop] = r.value;
2144         }
2145       } else if (remainingProps.size == 1) {
2146         return context.error(
2147           `Unexpected property "${[...remainingProps]}"`,
2148           `not contain an unexpected "${[...remainingProps]}" property`
2149         );
2150       } else if (remainingProps.size) {
2151         let props = [...remainingProps].sort().join(", ");
2152         return context.error(
2153           `Unexpected properties: ${props}`,
2154           `not contain the unexpected properties [${props}]`
2155         );
2156       }
2158       return this.postprocess({ value: result }, context);
2159     } catch (e) {
2160       if (e.error) {
2161         return e;
2162       }
2163       throw e;
2164     }
2165   }
2168 // This type is just a placeholder to be referred to by
2169 // SubModuleProperty. No value is ever expected to have this type.
2170 SubModuleType = class SubModuleType extends Type {
2171   static get EXTRA_PROPERTIES() {
2172     return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
2173   }
2175   static parseSchema(root, schema, path, extraProperties = []) {
2176     this.checkSchemaProperties(schema, path, extraProperties);
2178     // The path we pass in here is only used for error messages.
2179     path = [...path, schema.id];
2180     let functions = schema.functions
2181       .filter(fun => !fun.unsupported)
2182       .map(fun => FunctionEntry.parseSchema(root, fun, path));
2184     let events = [];
2186     if (schema.events) {
2187       events = schema.events
2188         .filter(event => !event.unsupported)
2189         .map(event => Event.parseSchema(root, event, path));
2190     }
2192     return new this(schema, functions, events);
2193   }
2195   constructor(schema, functions, events) {
2196     // schema contains properties such as min/max_manifest_version needed
2197     // in the base class so that the Context class can version compare
2198     // any entries against the manifest version.
2199     super(schema);
2200     this.functions = functions;
2201     this.events = events;
2202   }
2205 class NumberType extends Type {
2206   normalize(value, context) {
2207     let r = this.normalizeBase("number", value, context);
2208     if (r.error) {
2209       return r;
2210     }
2212     if (isNaN(r.value) || !Number.isFinite(r.value)) {
2213       return context.error(
2214         "NaN and infinity are not valid",
2215         "be a finite number"
2216       );
2217     }
2219     return r;
2220   }
2222   checkBaseType(baseType) {
2223     return baseType == "number" || baseType == "integer";
2224   }
2227 class IntegerType extends Type {
2228   static get EXTRA_PROPERTIES() {
2229     return ["minimum", "maximum", ...super.EXTRA_PROPERTIES];
2230   }
2232   static parseSchema(root, schema, path, extraProperties = []) {
2233     this.checkSchemaProperties(schema, path, extraProperties);
2235     let { minimum = -Infinity, maximum = Infinity } = schema;
2236     return new this(schema, minimum, maximum);
2237   }
2239   constructor(schema, minimum, maximum) {
2240     super(schema);
2241     this.minimum = minimum;
2242     this.maximum = maximum;
2243   }
2245   normalize(value, context) {
2246     let r = this.normalizeBase("integer", value, context);
2247     if (r.error) {
2248       return r;
2249     }
2250     value = r.value;
2252     // Ensure it's between -2**31 and 2**31-1
2253     if (!Number.isSafeInteger(value)) {
2254       return context.error(
2255         "Integer is out of range",
2256         "be a valid 32 bit signed integer"
2257       );
2258     }
2260     if (value < this.minimum) {
2261       return context.error(
2262         `Integer ${value} is too small (must be at least ${this.minimum})`,
2263         `be at least ${this.minimum}`
2264       );
2265     }
2266     if (value > this.maximum) {
2267       return context.error(
2268         `Integer ${value} is too big (must be at most ${this.maximum})`,
2269         `be no greater than ${this.maximum}`
2270       );
2271     }
2273     return this.postprocess(r, context);
2274   }
2276   checkBaseType(baseType) {
2277     return baseType == "integer";
2278   }
2281 class BooleanType extends Type {
2282   static get EXTRA_PROPERTIES() {
2283     return ["enum", ...super.EXTRA_PROPERTIES];
2284   }
2286   static parseSchema(root, schema, path, extraProperties = []) {
2287     this.checkSchemaProperties(schema, path, extraProperties);
2288     let enumeration = schema.enum || null;
2289     return new this(schema, enumeration);
2290   }
2292   constructor(schema, enumeration) {
2293     super(schema);
2294     this.enumeration = enumeration;
2295   }
2297   normalize(value, context) {
2298     if (!this.checkBaseType(getValueBaseType(value))) {
2299       return context.error(
2300         () => `Expected boolean instead of ${JSON.stringify(value)}`,
2301         `be a boolean`
2302       );
2303     }
2304     value = this.preprocess(value, context);
2305     if (this.enumeration && !this.enumeration.includes(value)) {
2306       return context.error(
2307         () => `Invalid value ${JSON.stringify(value)}`,
2308         `be ${this.enumeration}`
2309       );
2310     }
2311     this.checkDeprecated(context, value);
2312     return { value };
2313   }
2315   checkBaseType(baseType) {
2316     return baseType == "boolean";
2317   }
2320 class ArrayType extends Type {
2321   static get EXTRA_PROPERTIES() {
2322     return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
2323   }
2325   static parseSchema(root, schema, path, extraProperties = []) {
2326     this.checkSchemaProperties(schema, path, extraProperties);
2328     let items = root.parseSchema(schema.items, path, ["onError"]);
2330     return new this(
2331       schema,
2332       items,
2333       schema.minItems || 0,
2334       schema.maxItems || Infinity
2335     );
2336   }
2338   constructor(schema, itemType, minItems, maxItems) {
2339     super(schema);
2340     this.itemType = itemType;
2341     this.minItems = minItems;
2342     this.maxItems = maxItems;
2343     this.onError = schema.items.onError || null;
2344   }
2346   normalize(value, context) {
2347     let v = this.normalizeBase("array", value, context);
2348     if (v.error) {
2349       return v;
2350     }
2351     value = v.value;
2353     let result = [];
2354     for (let [i, element] of value.entries()) {
2355       element = context.withPath(String(i), () =>
2356         this.itemType.normalize(element, context)
2357       );
2358       if (element.error) {
2359         if (this.onError == "warn") {
2360           this.logWarning(context, forceString(element.error));
2361         } else if (this.onError != "ignore") {
2362           return element;
2363         }
2364         continue;
2365       }
2366       result.push(element.value);
2367     }
2369     if (result.length < this.minItems) {
2370       return context.error(
2371         `Array requires at least ${this.minItems} items; you have ${result.length}`,
2372         `have at least ${this.minItems} items`
2373       );
2374     }
2376     if (result.length > this.maxItems) {
2377       return context.error(
2378         `Array requires at most ${this.maxItems} items; you have ${result.length}`,
2379         `have at most ${this.maxItems} items`
2380       );
2381     }
2383     return this.postprocess({ value: result }, context);
2384   }
2386   checkBaseType(baseType) {
2387     return baseType == "array";
2388   }
2391 class FunctionType extends Type {
2392   static get EXTRA_PROPERTIES() {
2393     return [
2394       "parameters",
2395       "async",
2396       "returns",
2397       "requireUserInput",
2398       ...super.EXTRA_PROPERTIES,
2399     ];
2400   }
2402   static parseSchema(root, schema, path, extraProperties = []) {
2403     this.checkSchemaProperties(schema, path, extraProperties);
2405     let isAsync = !!schema.async;
2406     let isExpectingCallback = typeof schema.async === "string";
2407     let parameters = null;
2408     if ("parameters" in schema) {
2409       parameters = [];
2410       for (let param of schema.parameters) {
2411         // Callbacks default to optional for now, because of promise
2412         // handling.
2413         let isCallback = isAsync && param.name == schema.async;
2414         if (isCallback) {
2415           isExpectingCallback = false;
2416         }
2418         parameters.push({
2419           type: root.parseSchema(param, path, ["name", "optional", "default"]),
2420           name: param.name,
2421           optional: param.optional == null ? isCallback : param.optional,
2422           default: param.default == undefined ? null : param.default,
2423         });
2424       }
2425     }
2426     let hasAsyncCallback = false;
2427     if (isAsync) {
2428       hasAsyncCallback =
2429         parameters &&
2430         parameters.length &&
2431         parameters[parameters.length - 1].name == schema.async;
2432     }
2434     if (DEBUG) {
2435       if (isExpectingCallback) {
2436         throw new Error(
2437           `Internal error: Expected a callback parameter ` +
2438             `with name ${schema.async}`
2439         );
2440       }
2442       if (isAsync && schema.returns) {
2443         throw new Error(
2444           "Internal error: Async functions must not have return values."
2445         );
2446       }
2447       if (
2448         isAsync &&
2449         schema.allowAmbiguousOptionalArguments &&
2450         !hasAsyncCallback
2451       ) {
2452         throw new Error(
2453           "Internal error: Async functions with ambiguous " +
2454             "arguments must declare the callback as the last parameter"
2455         );
2456       }
2457     }
2459     return new this(
2460       schema,
2461       parameters,
2462       isAsync,
2463       hasAsyncCallback,
2464       !!schema.requireUserInput
2465     );
2466   }
2468   constructor(schema, parameters, isAsync, hasAsyncCallback, requireUserInput) {
2469     super(schema);
2470     this.parameters = parameters;
2471     this.isAsync = isAsync;
2472     this.hasAsyncCallback = hasAsyncCallback;
2473     this.requireUserInput = requireUserInput;
2474   }
2476   normalize(value, context) {
2477     return this.normalizeBase("function", value, context);
2478   }
2480   checkBaseType(baseType) {
2481     return baseType == "function";
2482   }
2485 // Represents a "property" defined in a schema namespace with a
2486 // particular value. Essentially this is a constant.
2487 class ValueProperty extends Entry {
2488   constructor(schema, name, value) {
2489     super(schema);
2490     this.name = name;
2491     this.value = value;
2492   }
2494   getDescriptor(path, context) {
2495     // Prevent injection if not a supported version.
2496     if (!context.matchManifestVersion(this)) {
2497       return;
2498     }
2500     return {
2501       descriptor: { value: this.value },
2502     };
2503   }
2506 // Represents a "property" defined in a schema namespace that is not a
2507 // constant.
2508 class TypeProperty extends Entry {
2509   constructor(schema, path, name, type, writable, permissions) {
2510     super(schema);
2511     this.path = path;
2512     this.name = name;
2513     this.type = type;
2514     this.writable = writable;
2515     this.permissions = permissions;
2516   }
2518   throwError(context, msg) {
2519     throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2520   }
2522   getDescriptor(path, context) {
2523     if (this.unsupported || !context.matchManifestVersion(this)) {
2524       return;
2525     }
2527     let apiImpl = context.getImplementation(path.join("."), this.name);
2529     let getStub = () => {
2530       this.checkDeprecated(context);
2531       return apiImpl.getProperty();
2532     };
2534     let descriptor = {
2535       get: Cu.exportFunction(getStub, context.cloneScope),
2536     };
2538     if (this.writable) {
2539       let setStub = value => {
2540         let normalized = this.type.normalize(value, context);
2541         if (normalized.error) {
2542           this.throwError(context, forceString(normalized.error));
2543         }
2545         apiImpl.setProperty(normalized.value);
2546       };
2548       descriptor.set = Cu.exportFunction(setStub, context.cloneScope);
2549     }
2551     return {
2552       descriptor,
2553       revoke() {
2554         apiImpl.revoke();
2555         apiImpl = null;
2556       },
2557     };
2558   }
2561 class SubModuleProperty extends Entry {
2562   // A SubModuleProperty represents a tree of objects and properties
2563   // to expose to an extension. Currently we support only a limited
2564   // form of sub-module properties, where "$ref" points to a
2565   // SubModuleType containing a list of functions and "properties" is
2566   // a list of additional simple properties.
2567   //
2568   // name: Name of the property stuff is being added to.
2569   // namespaceName: Namespace in which the property lives.
2570   // reference: Name of the type defining the functions to add to the property.
2571   // properties: Additional properties to add to the module (unsupported).
2572   constructor(root, schema, path, name, reference, properties, permissions) {
2573     super(schema);
2574     this.root = root;
2575     this.name = name;
2576     this.path = path;
2577     this.namespaceName = path.join(".");
2578     this.reference = reference;
2579     this.properties = properties;
2580     this.permissions = permissions;
2581   }
2583   getDescriptor(path, context) {
2584     let obj = Cu.createObjectIn(context.cloneScope);
2586     let ns = this.root.getNamespace(this.namespaceName);
2587     let type = ns.get(this.reference);
2588     if (!type && this.reference.includes(".")) {
2589       let [namespaceName, ref] = this.reference.split(".");
2590       ns = this.root.getNamespace(namespaceName);
2591       type = ns.get(ref);
2592     }
2593     // Prevent injection if not a supported version.
2594     if (!context.matchManifestVersion(type)) {
2595       return;
2596     }
2598     if (DEBUG) {
2599       if (!type || !(type instanceof SubModuleType)) {
2600         throw new Error(
2601           `Internal error: ${this.namespaceName}.${this.reference} ` +
2602             `is not a sub-module`
2603         );
2604       }
2605     }
2606     let subpath = [...path, this.name];
2608     let functions = type.functions;
2609     for (let fun of functions) {
2610       context.injectInto(fun, obj, fun.name, subpath, ns);
2611     }
2613     let events = type.events;
2614     for (let event of events) {
2615       context.injectInto(event, obj, event.name, subpath, ns);
2616     }
2618     // TODO: Inject this.properties.
2620     return {
2621       descriptor: { value: obj },
2622       revoke() {
2623         let unwrapped = ChromeUtils.waiveXrays(obj);
2624         for (let fun of functions) {
2625           try {
2626             delete unwrapped[fun.name];
2627           } catch (e) {
2628             Cu.reportError(e);
2629           }
2630         }
2631       },
2632     };
2633   }
2636 // This class is a base class for FunctionEntrys and Events. It takes
2637 // care of validating parameter lists (i.e., handling of optional
2638 // parameters and parameter type checking).
2639 class CallEntry extends Entry {
2640   constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) {
2641     super(schema);
2642     this.path = path;
2643     this.name = name;
2644     this.parameters = parameters;
2645     this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments;
2646   }
2648   throwError(context, msg) {
2649     throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2650   }
2652   checkParameters(args, context) {
2653     let fixedArgs = [];
2655     // First we create a new array, fixedArgs, that is the same as
2656     // |args| but with default values in place of omitted optional parameters.
2657     let check = (parameterIndex, argIndex) => {
2658       if (parameterIndex == this.parameters.length) {
2659         if (argIndex == args.length) {
2660           return true;
2661         }
2662         return false;
2663       }
2665       let parameter = this.parameters[parameterIndex];
2666       if (parameter.optional) {
2667         // Try skipping it.
2668         fixedArgs[parameterIndex] = parameter.default;
2669         if (check(parameterIndex + 1, argIndex)) {
2670           return true;
2671         }
2672       }
2674       if (argIndex == args.length) {
2675         return false;
2676       }
2678       let arg = args[argIndex];
2679       if (!parameter.type.checkBaseType(getValueBaseType(arg))) {
2680         // For Chrome compatibility, use the default value if null or undefined
2681         // is explicitly passed but is not a valid argument in this position.
2682         if (parameter.optional && (arg === null || arg === undefined)) {
2683           fixedArgs[parameterIndex] = Cu.cloneInto(parameter.default, global);
2684         } else {
2685           return false;
2686         }
2687       } else {
2688         fixedArgs[parameterIndex] = arg;
2689       }
2691       return check(parameterIndex + 1, argIndex + 1);
2692     };
2694     if (this.allowAmbiguousOptionalArguments) {
2695       // When this option is set, it's up to the implementation to
2696       // parse arguments.
2697       // The last argument for asynchronous methods is either a function or null.
2698       // This is specifically done for runtime.sendMessage.
2699       if (this.hasAsyncCallback && typeof args[args.length - 1] != "function") {
2700         args.push(null);
2701       }
2702       return args;
2703     }
2704     let success = check(0, 0);
2705     if (!success) {
2706       this.throwError(context, "Incorrect argument types");
2707     }
2709     // Now we normalize (and fully type check) all non-omitted arguments.
2710     fixedArgs = fixedArgs.map((arg, parameterIndex) => {
2711       if (arg === null) {
2712         return null;
2713       }
2714       let parameter = this.parameters[parameterIndex];
2715       let r = parameter.type.normalize(arg, context);
2716       if (r.error) {
2717         this.throwError(
2718           context,
2719           `Type error for parameter ${parameter.name} (${forceString(r.error)})`
2720         );
2721       }
2722       return r.value;
2723     });
2725     return fixedArgs;
2726   }
2729 // Represents a "function" defined in a schema namespace.
2730 FunctionEntry = class FunctionEntry extends CallEntry {
2731   static parseSchema(root, schema, path) {
2732     // When not in DEBUG mode, we just need to know *if* this returns.
2733     let returns = !!schema.returns;
2734     if (DEBUG && "returns" in schema) {
2735       returns = {
2736         type: root.parseSchema(schema.returns, path, ["optional", "name"]),
2737         optional: schema.returns.optional || false,
2738         name: "result",
2739       };
2740     }
2742     return new this(
2743       schema,
2744       path,
2745       schema.name,
2746       root.parseSchema(schema, path, [
2747         "name",
2748         "unsupported",
2749         "returns",
2750         "permissions",
2751         "allowAmbiguousOptionalArguments",
2752         "allowCrossOriginArguments",
2753       ]),
2754       schema.unsupported || false,
2755       schema.allowAmbiguousOptionalArguments || false,
2756       schema.allowCrossOriginArguments || false,
2757       returns,
2758       schema.permissions || null
2759     );
2760   }
2762   constructor(
2763     schema,
2764     path,
2765     name,
2766     type,
2767     unsupported,
2768     allowAmbiguousOptionalArguments,
2769     allowCrossOriginArguments,
2770     returns,
2771     permissions
2772   ) {
2773     super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
2774     this.unsupported = unsupported;
2775     this.returns = returns;
2776     this.permissions = permissions;
2777     this.allowCrossOriginArguments = allowCrossOriginArguments;
2779     this.isAsync = type.isAsync;
2780     this.hasAsyncCallback = type.hasAsyncCallback;
2781     this.requireUserInput = type.requireUserInput;
2782   }
2784   checkValue({ type, optional, name }, value, context) {
2785     if (optional && value == null) {
2786       return;
2787     }
2788     if (
2789       type.reference === "ExtensionPanel" ||
2790       type.reference === "ExtensionSidebarPane" ||
2791       type.reference === "Port"
2792     ) {
2793       // TODO: We currently treat objects with functions as SubModuleType,
2794       // which is just wrong, and a bigger yak.  Skipping for now.
2795       return;
2796     }
2797     const { error } = type.normalize(value, context);
2798     if (error) {
2799       this.throwError(
2800         context,
2801         `Type error for ${name} value (${forceString(error)})`
2802       );
2803     }
2804   }
2806   checkCallback(args, context) {
2807     const callback = this.parameters[this.parameters.length - 1];
2808     for (const [i, param] of callback.type.parameters.entries()) {
2809       this.checkValue(param, args[i], context);
2810     }
2811   }
2813   getDescriptor(path, context) {
2814     let apiImpl = context.getImplementation(path.join("."), this.name);
2816     let stub;
2817     if (this.isAsync) {
2818       stub = (...args) => {
2819         this.checkDeprecated(context);
2820         let actuals = this.checkParameters(args, context);
2821         let callback = null;
2822         if (this.hasAsyncCallback) {
2823           callback = actuals.pop();
2824         }
2825         if (callback === null && context.isChromeCompat) {
2826           // We pass an empty stub function as a default callback for
2827           // the `chrome` API, so promise objects are not returned,
2828           // and lastError values are reported immediately.
2829           callback = () => {};
2830         }
2831         if (DEBUG && this.hasAsyncCallback && callback) {
2832           let original = callback;
2833           callback = (...args) => {
2834             this.checkCallback(args, context);
2835             original(...args);
2836           };
2837         }
2838         let result = apiImpl.callAsyncFunction(
2839           actuals,
2840           callback,
2841           this.requireUserInput
2842         );
2843         if (DEBUG && this.hasAsyncCallback && !callback) {
2844           return result.then(result => {
2845             this.checkCallback([result], context);
2846             return result;
2847           });
2848         }
2849         return result;
2850       };
2851     } else if (!this.returns) {
2852       stub = (...args) => {
2853         this.checkDeprecated(context);
2854         let actuals = this.checkParameters(args, context);
2855         return apiImpl.callFunctionNoReturn(actuals);
2856       };
2857     } else {
2858       stub = (...args) => {
2859         this.checkDeprecated(context);
2860         let actuals = this.checkParameters(args, context);
2861         let result = apiImpl.callFunction(actuals);
2862         if (DEBUG && this.returns) {
2863           this.checkValue(this.returns, result, context);
2864         }
2865         return result;
2866       };
2867     }
2869     return {
2870       descriptor: {
2871         value: Cu.exportFunction(stub, context.cloneScope, {
2872           allowCrossOriginArguments: this.allowCrossOriginArguments,
2873         }),
2874       },
2875       revoke() {
2876         apiImpl.revoke();
2877         apiImpl = null;
2878       },
2879     };
2880   }
2883 // Represents an "event" defined in a schema namespace.
2885 // TODO Bug 1369722: we should be able to remove the eslint-disable-line that follows
2886 // once Bug 1369722 has been fixed.
2887 // eslint-disable-next-line no-global-assign
2888 Event = class Event extends CallEntry {
2889   static parseSchema(root, event, path) {
2890     let extraParameters = Array.from(event.extraParameters || [], param => ({
2891       type: root.parseSchema(param, path, ["name", "optional", "default"]),
2892       name: param.name,
2893       optional: param.optional || false,
2894       default: param.default == undefined ? null : param.default,
2895     }));
2897     let extraProperties = [
2898       "name",
2899       "unsupported",
2900       "permissions",
2901       "extraParameters",
2902       // We ignore these properties for now.
2903       "returns",
2904       "filters",
2905     ];
2907     return new this(
2908       event,
2909       path,
2910       event.name,
2911       root.parseSchema(event, path, extraProperties),
2912       extraParameters,
2913       event.unsupported || false,
2914       event.permissions || null
2915     );
2916   }
2918   constructor(
2919     schema,
2920     path,
2921     name,
2922     type,
2923     extraParameters,
2924     unsupported,
2925     permissions
2926   ) {
2927     super(schema, path, name, extraParameters);
2928     this.type = type;
2929     this.unsupported = unsupported;
2930     this.permissions = permissions;
2931   }
2933   checkListener(listener, context) {
2934     let r = this.type.normalize(listener, context);
2935     if (r.error) {
2936       this.throwError(context, "Invalid listener");
2937     }
2938     return r.value;
2939   }
2941   getDescriptor(path, context) {
2942     let apiImpl = context.getImplementation(path.join("."), this.name);
2944     let addStub = (listener, ...args) => {
2945       listener = this.checkListener(listener, context);
2946       let actuals = this.checkParameters(args, context);
2947       apiImpl.addListener(listener, actuals);
2948     };
2950     let removeStub = listener => {
2951       listener = this.checkListener(listener, context);
2952       apiImpl.removeListener(listener);
2953     };
2955     let hasStub = listener => {
2956       listener = this.checkListener(listener, context);
2957       return apiImpl.hasListener(listener);
2958     };
2960     let obj = Cu.createObjectIn(context.cloneScope);
2962     Cu.exportFunction(addStub, obj, { defineAs: "addListener" });
2963     Cu.exportFunction(removeStub, obj, { defineAs: "removeListener" });
2964     Cu.exportFunction(hasStub, obj, { defineAs: "hasListener" });
2966     return {
2967       descriptor: { value: obj },
2968       revoke() {
2969         apiImpl.revoke();
2970         apiImpl = null;
2972         let unwrapped = ChromeUtils.waiveXrays(obj);
2973         delete unwrapped.addListener;
2974         delete unwrapped.removeListener;
2975         delete unwrapped.hasListener;
2976       },
2977     };
2978   }
2981 const TYPES = Object.freeze(
2982   Object.assign(Object.create(null), {
2983     any: AnyType,
2984     array: ArrayType,
2985     boolean: BooleanType,
2986     function: FunctionType,
2987     integer: IntegerType,
2988     null: NullType,
2989     number: NumberType,
2990     object: ObjectType,
2991     string: StringType,
2992   })
2995 const LOADERS = {
2996   events: "loadEvent",
2997   functions: "loadFunction",
2998   properties: "loadProperty",
2999   types: "loadType",
3002 class Namespace extends Map {
3003   constructor(root, name, path) {
3004     super();
3006     this.root = root;
3008     this._lazySchemas = [];
3009     this.initialized = false;
3011     this.name = name;
3012     this.path = name ? [...path, name] : [...path];
3014     this.superNamespace = null;
3016     this.min_manifest_version = MIN_MANIFEST_VERSION;
3017     this.max_manifest_version = MAX_MANIFEST_VERSION;
3019     this.permissions = null;
3020     this.allowedContexts = [];
3021     this.defaultContexts = [];
3022   }
3024   /**
3025    * Adds a JSON Schema object to the set of schemas that represent this
3026    * namespace.
3027    *
3028    * @param {object} schema
3029    *        A JSON schema object which partially describes this
3030    *        namespace.
3031    */
3032   addSchema(schema) {
3033     this._lazySchemas.push(schema);
3035     for (let prop of [
3036       "permissions",
3037       "allowedContexts",
3038       "defaultContexts",
3039       "min_manifest_version",
3040       "max_manifest_version",
3041     ]) {
3042       if (schema[prop]) {
3043         this[prop] = schema[prop];
3044       }
3045     }
3047     if (schema.$import) {
3048       this.superNamespace = this.root.getNamespace(schema.$import);
3049     }
3050   }
3052   /**
3053    * Initializes the keys of this namespace based on the schema objects
3054    * added via previous `addSchema` calls.
3055    */
3056   init() {
3057     if (this.initialized) {
3058       return;
3059     }
3061     if (this.superNamespace) {
3062       this._lazySchemas.unshift(...this.superNamespace._lazySchemas);
3063     }
3065     for (let type of Object.keys(LOADERS)) {
3066       this[type] = new DefaultMap(() => []);
3067     }
3069     for (let schema of this._lazySchemas) {
3070       for (let type of schema.types || []) {
3071         if (!type.unsupported) {
3072           this.types.get(type.$extend || type.id).push(type);
3073         }
3074       }
3076       for (let [name, prop] of Object.entries(schema.properties || {})) {
3077         if (!prop.unsupported) {
3078           this.properties.get(name).push(prop);
3079         }
3080       }
3082       for (let fun of schema.functions || []) {
3083         if (!fun.unsupported) {
3084           this.functions.get(fun.name).push(fun);
3085         }
3086       }
3088       for (let event of schema.events || []) {
3089         if (!event.unsupported) {
3090           this.events.get(event.name).push(event);
3091         }
3092       }
3093     }
3095     // For each type of top-level property in the schema object, iterate
3096     // over all properties of that type, and create a temporary key for
3097     // each property pointing to its type. Those temporary properties
3098     // are later used to instantiate an Entry object based on the actual
3099     // schema object.
3100     for (let type of Object.keys(LOADERS)) {
3101       for (let key of this[type].keys()) {
3102         this.set(key, type);
3103       }
3104     }
3106     this.initialized = true;
3108     if (DEBUG) {
3109       for (let key of this.keys()) {
3110         this.get(key);
3111       }
3112     }
3113   }
3115   /**
3116    * Initializes the value of a given key, by parsing the schema object
3117    * associated with it and replacing its temporary value with an `Entry`
3118    * instance.
3119    *
3120    * @param {string} key
3121    *        The name of the property to initialize.
3122    * @param {string} type
3123    *        The type of property the key represents. Must have a
3124    *        corresponding entry in the `LOADERS` object, pointing to the
3125    *        initialization method for that type.
3126    *
3127    * @returns {Entry}
3128    */
3129   initKey(key, type) {
3130     let loader = LOADERS[type];
3132     for (let schema of this[type].get(key)) {
3133       this.set(key, this[loader](key, schema));
3134     }
3136     return this.get(key);
3137   }
3139   loadType(name, type) {
3140     if ("$extend" in type) {
3141       return this.extendType(type);
3142     }
3143     return this.root.parseSchema(type, this.path, ["id"]);
3144   }
3146   extendType(type) {
3147     let targetType = this.get(type.$extend);
3149     // Only allow extending object and choices types for now.
3150     if (targetType instanceof ObjectType) {
3151       type.type = "object";
3152     } else if (DEBUG) {
3153       if (!targetType) {
3154         throw new Error(
3155           `Internal error: Attempt to extend a nonexistent type ${type.$extend}`
3156         );
3157       } else if (!(targetType instanceof ChoiceType)) {
3158         throw new Error(
3159           `Internal error: Attempt to extend a non-extensible type ${type.$extend}`
3160         );
3161       }
3162     }
3164     let parsed = this.root.parseSchema(type, this.path, ["$extend"]);
3166     if (DEBUG && parsed.constructor !== targetType.constructor) {
3167       throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
3168     }
3170     targetType.extend(parsed);
3172     return targetType;
3173   }
3175   loadProperty(name, prop) {
3176     if ("$ref" in prop) {
3177       if (!prop.unsupported) {
3178         return new SubModuleProperty(
3179           this.root,
3180           prop,
3181           this.path,
3182           name,
3183           prop.$ref,
3184           prop.properties || {},
3185           prop.permissions || null
3186         );
3187       }
3188     } else if ("value" in prop) {
3189       return new ValueProperty(prop, name, prop.value);
3190     } else {
3191       // We ignore the "optional" attribute on properties since we
3192       // don't inject anything here anyway.
3193       let type = this.root.parseSchema(
3194         prop,
3195         [this.name],
3196         ["optional", "permissions", "writable"]
3197       );
3198       return new TypeProperty(
3199         prop,
3200         this.path,
3201         name,
3202         type,
3203         prop.writable || false,
3204         prop.permissions || null
3205       );
3206     }
3207   }
3209   loadFunction(name, fun) {
3210     return FunctionEntry.parseSchema(this.root, fun, this.path);
3211   }
3213   loadEvent(name, event) {
3214     return Event.parseSchema(this.root, event, this.path);
3215   }
3217   /**
3218    * Injects the properties of this namespace into the given object.
3219    *
3220    * @param {object} dest
3221    *        The object into which to inject the namespace properties.
3222    * @param {InjectionContext} context
3223    *        The injection context with which to inject the properties.
3224    */
3225   injectInto(dest, context) {
3226     for (let name of this.keys()) {
3227       // If the entry does not match the manifest version do not
3228       // inject the property.  This prevents the item from being
3229       // enumerable in the namespace object.  We cannot accomplish
3230       // this inside exportLazyProperty, it specifically injects
3231       // an enumerable object.
3232       let entry = this.get(name);
3233       if (!context.matchManifestVersion(entry)) {
3234         continue;
3235       }
3236       exportLazyProperty(dest, name, () => {
3237         let entry = this.get(name);
3239         return context.getDescriptor(entry, dest, name, this.path, this);
3240       });
3241     }
3242   }
3244   getDescriptor(path, context) {
3245     let obj = Cu.createObjectIn(context.cloneScope);
3247     let ns = context.schemaRoot.getNamespace(this.path.join("."));
3248     ns.injectInto(obj, context);
3250     // Only inject the namespace object if it isn't empty.
3251     if (Object.keys(obj).length) {
3252       return {
3253         descriptor: { value: obj },
3254       };
3255     }
3256   }
3258   keys() {
3259     this.init();
3260     return super.keys();
3261   }
3263   *entries() {
3264     for (let key of this.keys()) {
3265       yield [key, this.get(key)];
3266     }
3267   }
3269   get(key) {
3270     this.init();
3271     let value = super.get(key);
3273     // The initial values of lazily-initialized schema properties are
3274     // strings, pointing to the type of property, corresponding to one
3275     // of the entries in the `LOADERS` object.
3276     if (typeof value === "string") {
3277       value = this.initKey(key, value);
3278     }
3280     return value;
3281   }
3283   /**
3284    * Returns a Namespace object for the given namespace name. If a
3285    * namespace object with this name does not already exist, it is
3286    * created. If the name contains any '.' characters, namespaces are
3287    * recursively created, for each dot-separated component.
3288    *
3289    * @param {string} name
3290    *        The name of the sub-namespace to retrieve.
3291    * @param {boolean} [create = true]
3292    *        If true, create any intermediate namespaces which don't
3293    *        exist.
3294    *
3295    * @returns {Namespace}
3296    */
3297   getNamespace(name, create = true) {
3298     let subName;
3300     let idx = name.indexOf(".");
3301     if (idx > 0) {
3302       subName = name.slice(idx + 1);
3303       name = name.slice(0, idx);
3304     }
3306     let ns = super.get(name);
3307     if (!ns) {
3308       if (!create) {
3309         return null;
3310       }
3311       ns = new Namespace(this.root, name, this.path);
3312       this.set(name, ns);
3313     }
3315     if (subName) {
3316       return ns.getNamespace(subName);
3317     }
3318     return ns;
3319   }
3321   getOwnNamespace(name) {
3322     return this.getNamespace(name);
3323   }
3325   has(key) {
3326     this.init();
3327     return super.has(key);
3328   }
3332  * A namespace which combines the children of an arbitrary number of
3333  * sub-namespaces.
3334  */
3335 class Namespaces extends Namespace {
3336   constructor(root, name, path, namespaces) {
3337     super(root, name, path);
3339     this.namespaces = namespaces;
3340   }
3342   injectInto(obj, context) {
3343     for (let ns of this.namespaces) {
3344       ns.injectInto(obj, context);
3345     }
3346   }
3350  * A root schema which combines the contents of an arbitrary number of base
3351  * schema roots.
3352  */
3353 class SchemaRoots extends Namespaces {
3354   constructor(root, bases) {
3355     bases = bases.map(base => base.rootSchema || base);
3357     super(null, "", [], bases);
3359     this.root = root;
3360     this.bases = bases;
3361     this._namespaces = new Map();
3362   }
3364   _getNamespace(name, create) {
3365     let results = [];
3366     for (let root of this.bases) {
3367       let ns = root.getNamespace(name, create);
3368       if (ns) {
3369         results.push(ns);
3370       }
3371     }
3373     if (results.length == 1) {
3374       return results[0];
3375     }
3377     if (results.length) {
3378       return new Namespaces(this.root, name, name.split("."), results);
3379     }
3380     return null;
3381   }
3383   getNamespace(name, create) {
3384     let ns = this._namespaces.get(name);
3385     if (!ns) {
3386       ns = this._getNamespace(name, create);
3387       if (ns) {
3388         this._namespaces.set(name, ns);
3389       }
3390     }
3391     return ns;
3392   }
3394   *getNamespaces(name) {
3395     for (let root of this.bases) {
3396       yield* root.getNamespaces(name);
3397     }
3398   }
3402  * A root schema namespace containing schema data which is isolated from data in
3403  * other schema roots. May extend a base namespace, in which case schemas in
3404  * this root may refer to types in a base, but not vice versa.
3406  * @param {SchemaRoot|Array<SchemaRoot>|null} base
3407  *        A base schema root (or roots) from which to derive, or null.
3408  * @param {Map<string, Array|StructuredCloneHolder>} schemaJSON
3409  *        A map of schema URLs and corresponding JSON blobs from which to
3410  *        populate this root namespace.
3411  */
3412 class SchemaRoot extends Namespace {
3413   constructor(base, schemaJSON) {
3414     super(null, "", []);
3416     if (Array.isArray(base)) {
3417       base = new SchemaRoots(this, base);
3418     }
3420     this.root = this;
3421     this.base = base;
3422     this.schemaJSON = schemaJSON;
3423   }
3425   *getNamespaces(path) {
3426     let name = path.join(".");
3428     let ns = this.getNamespace(name, false);
3429     if (ns) {
3430       yield ns;
3431     }
3433     if (this.base) {
3434       yield* this.base.getNamespaces(name);
3435     }
3436   }
3438   /**
3439    * Returns the sub-namespace with the given name. If the given namespace
3440    * doesn't already exist, attempts to find it in the base SchemaRoot before
3441    * creating a new empty namespace.
3442    *
3443    * @param {string} name
3444    *        The namespace to retrieve.
3445    * @param {boolean} [create = true]
3446    *        If true, an empty namespace should be created if one does not
3447    *        already exist.
3448    * @returns {Namespace|null}
3449    */
3450   getNamespace(name, create = true) {
3451     let ns = super.getNamespace(name, false);
3452     if (ns) {
3453       return ns;
3454     }
3456     ns = this.base && this.base.getNamespace(name, false);
3457     if (ns) {
3458       return ns;
3459     }
3460     return create && super.getNamespace(name, create);
3461   }
3463   /**
3464    * Like getNamespace, but does not take the base SchemaRoot into account.
3465    *
3466    * @param {string} name
3467    *        The namespace to retrieve.
3468    * @returns {Namespace}
3469    */
3470   getOwnNamespace(name) {
3471     return super.getNamespace(name);
3472   }
3474   parseSchema(schema, path, extraProperties = []) {
3475     let allowedProperties = DEBUG && new Set(extraProperties);
3477     if ("choices" in schema) {
3478       return ChoiceType.parseSchema(this, schema, path, allowedProperties);
3479     } else if ("$ref" in schema) {
3480       return RefType.parseSchema(this, schema, path, allowedProperties);
3481     }
3483     let type = TYPES[schema.type];
3485     if (DEBUG) {
3486       allowedProperties.add("type");
3488       if (!("type" in schema)) {
3489         throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
3490       }
3492       if (!type) {
3493         throw new Error(`Unexpected type ${schema.type}`);
3494       }
3495     }
3497     return type.parseSchema(this, schema, path, allowedProperties);
3498   }
3500   parseSchemas() {
3501     for (let [key, schema] of this.schemaJSON.entries()) {
3502       try {
3503         if (typeof schema.deserialize === "function") {
3504           schema = schema.deserialize(global, isParentProcess);
3506           // If we're in the parent process, we need to keep the
3507           // StructuredCloneHolder blob around in order to send to future child
3508           // processes. If we're in a child, we have no further use for it, so
3509           // just store the deserialized schema data in its place.
3510           if (!isParentProcess) {
3511             this.schemaJSON.set(key, schema);
3512           }
3513         }
3515         this.loadSchema(schema);
3516       } catch (e) {
3517         Cu.reportError(e);
3518       }
3519     }
3520   }
3522   loadSchema(json) {
3523     for (let namespace of json) {
3524       this.getOwnNamespace(namespace.namespace).addSchema(namespace);
3525     }
3526   }
3528   /**
3529    * Checks whether a given object has the necessary permissions to
3530    * expose the given namespace.
3531    *
3532    * @param {string} namespace
3533    *        The top-level namespace to check permissions for.
3534    * @param {object} wrapperFuncs
3535    *        Wrapper functions for the given context.
3536    * @param {function} wrapperFuncs.hasPermission
3537    *        A function which, when given a string argument, returns true
3538    *        if the context has the given permission.
3539    * @returns {boolean}
3540    *        True if the context has permission for the given namespace.
3541    */
3542   checkPermissions(namespace, wrapperFuncs) {
3543     let ns = this.getNamespace(namespace);
3544     if (ns && ns.permissions) {
3545       return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
3546     }
3547     return true;
3548   }
3550   /**
3551    * Inject registered extension APIs into `dest`.
3552    *
3553    * @param {object} dest The root namespace for the APIs.
3554    *     This object is usually exposed to extensions as "chrome" or "browser".
3555    * @param {object} wrapperFuncs An implementation of the InjectionContext
3556    *     interface, which runs the actual functionality of the generated API.
3557    */
3558   inject(dest, wrapperFuncs) {
3559     let context = new InjectionContext(wrapperFuncs, this);
3561     this.injectInto(dest, context);
3562   }
3564   injectInto(dest, context) {
3565     // For schema graphs where multiple schema roots have the same base, don't
3566     // inject it more than once.
3568     if (!context.injectedRoots.has(this)) {
3569       context.injectedRoots.add(this);
3570       if (this.base) {
3571         this.base.injectInto(dest, context);
3572       }
3573       super.injectInto(dest, context);
3574     }
3575   }
3577   /**
3578    * Normalize `obj` according to the loaded schema for `typeName`.
3579    *
3580    * @param {object} obj The object to normalize against the schema.
3581    * @param {string} typeName The name in the format namespace.propertyname
3582    * @param {object} context An implementation of Context. Any validation errors
3583    *     are reported to the given context.
3584    * @returns {object} The normalized object.
3585    */
3586   normalize(obj, typeName, context) {
3587     let [namespaceName, prop] = typeName.split(".");
3588     let ns = this.getNamespace(namespaceName);
3589     let type = ns.get(prop);
3591     let result = type.normalize(obj, new Context(context));
3592     if (result.error) {
3593       return { error: forceString(result.error) };
3594     }
3595     return result;
3596   }
3599 this.Schemas = {
3600   initialized: false,
3602   REVOKE: Symbol("@@revoke"),
3604   // Maps a schema URL to the JSON contained in that schema file. This
3605   // is useful for sending the JSON across processes.
3606   schemaJSON: new Map(),
3608   // A map of schema JSON which should be available in all content processes.
3609   contentSchemaJSON: new Map(),
3611   // A map of schema JSON which should only be available to extension processes.
3612   privilegedSchemaJSON: new Map(),
3614   _rootSchema: null,
3616   // A weakmap for the validation Context class instances given an extension
3617   // context (keyed by the extensin context instance).
3618   // This is used instead of the InjectionContext for webIDL API validation
3619   // and normalization (see Schemas.checkParameters).
3620   paramsValidationContexts: new DefaultWeakMap(
3621     extContext => new Context(extContext)
3622   ),
3624   get rootSchema() {
3625     if (!this.initialized) {
3626       this.init();
3627     }
3628     if (!this._rootSchema) {
3629       this._rootSchema = new SchemaRoot(null, this.schemaJSON);
3630       this._rootSchema.parseSchemas();
3631     }
3632     return this._rootSchema;
3633   },
3635   getNamespace(name) {
3636     return this.rootSchema.getNamespace(name);
3637   },
3639   init() {
3640     if (this.initialized) {
3641       return;
3642     }
3643     this.initialized = true;
3645     if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
3646       let addSchemas = schemas => {
3647         for (let [key, value] of schemas.entries()) {
3648           this.schemaJSON.set(key, value);
3649         }
3650       };
3652       if (WebExtensionPolicy.isExtensionProcess || DEBUG) {
3653         addSchemas(Services.cpmm.sharedData.get(KEY_PRIVILEGED_SCHEMAS));
3654       }
3656       let schemas = Services.cpmm.sharedData.get(KEY_CONTENT_SCHEMAS);
3657       if (schemas) {
3658         addSchemas(schemas);
3659       }
3660     }
3661   },
3663   _loadCachedSchemasPromise: null,
3664   loadCachedSchemas() {
3665     if (!this._loadCachedSchemasPromise) {
3666       this._loadCachedSchemasPromise = StartupCache.schemas
3667         .getAll()
3668         .then(results => {
3669           return results;
3670         });
3671     }
3673     return this._loadCachedSchemasPromise;
3674   },
3676   addSchema(url, schema, content = false) {
3677     this.schemaJSON.set(url, schema);
3679     if (content) {
3680       this.contentSchemaJSON.set(url, schema);
3681     } else {
3682       this.privilegedSchemaJSON.set(url, schema);
3683     }
3685     if (this._rootSchema) {
3686       throw new Error("Schema loaded after root schema populated");
3687     }
3688   },
3690   updateSharedSchemas() {
3691     let { sharedData } = Services.ppmm;
3693     sharedData.set(KEY_CONTENT_SCHEMAS, this.contentSchemaJSON);
3694     sharedData.set(KEY_PRIVILEGED_SCHEMAS, this.privilegedSchemaJSON);
3695   },
3697   fetch(url) {
3698     return readJSONAndBlobbify(url);
3699   },
3701   processSchema(json) {
3702     return blobbify(json);
3703   },
3705   async load(url, content = false) {
3706     if (!isParentProcess) {
3707       return;
3708     }
3710     const startTime = Cu.now();
3711     let schemaCache = await this.loadCachedSchemas();
3712     const fromCache = schemaCache.has(url);
3714     let blob =
3715       schemaCache.get(url) ||
3716       (await StartupCache.schemas.get(url, readJSONAndBlobbify));
3718     if (!this.schemaJSON.has(url)) {
3719       this.addSchema(url, blob, content);
3720     }
3722     ChromeUtils.addProfilerMarker(
3723       "ExtensionSchemas",
3724       { startTime },
3725       `load ${url}, from cache: ${fromCache}`
3726     );
3727   },
3729   /**
3730    * Checks whether a given object has the necessary permissions to
3731    * expose the given namespace.
3732    *
3733    * @param {string} namespace
3734    *        The top-level namespace to check permissions for.
3735    * @param {object} wrapperFuncs
3736    *        Wrapper functions for the given context.
3737    * @param {function} wrapperFuncs.hasPermission
3738    *        A function which, when given a string argument, returns true
3739    *        if the context has the given permission.
3740    * @returns {boolean}
3741    *        True if the context has permission for the given namespace.
3742    */
3743   checkPermissions(namespace, wrapperFuncs) {
3744     return this.rootSchema.checkPermissions(namespace, wrapperFuncs);
3745   },
3747   /**
3748    * Returns a sorted array of permission names for the given permission types.
3749    *
3750    * @param {Array} types An array of permission types, defaults to all permissions.
3751    * @returns {Array} sorted array of permission names
3752    */
3753   getPermissionNames(
3754     types = [
3755       "Permission",
3756       "OptionalPermission",
3757       "PermissionNoPrompt",
3758       "OptionalPermissionNoPrompt",
3759     ]
3760   ) {
3761     const ns = this.getNamespace("manifest");
3762     let names = [];
3763     for (let typeName of types) {
3764       for (let choice of ns
3765         .get(typeName)
3766         .choices.filter(choice => choice.enumeration)) {
3767         names = names.concat(choice.enumeration);
3768       }
3769     }
3770     return names.sort();
3771   },
3773   exportLazyGetter,
3775   /**
3776    * Inject registered extension APIs into `dest`.
3777    *
3778    * @param {object} dest The root namespace for the APIs.
3779    *     This object is usually exposed to extensions as "chrome" or "browser".
3780    * @param {object} wrapperFuncs An implementation of the InjectionContext
3781    *     interface, which runs the actual functionality of the generated API.
3782    */
3783   inject(dest, wrapperFuncs) {
3784     this.rootSchema.inject(dest, wrapperFuncs);
3785   },
3787   /**
3788    * Normalize `obj` according to the loaded schema for `typeName`.
3789    *
3790    * @param {object} obj The object to normalize against the schema.
3791    * @param {string} typeName The name in the format namespace.propertyname
3792    * @param {object} context An implementation of Context. Any validation errors
3793    *     are reported to the given context.
3794    * @returns {object} The normalized object.
3795    */
3796   normalize(obj, typeName, context) {
3797     return this.rootSchema.normalize(obj, typeName, context);
3798   },
3800   /**
3801    * Validate and normalize the arguments for an API request originated
3802    * from the webIDL API bindings.
3803    *
3804    * This provides for calls originating through WebIDL the parameters
3805    * validation and normalization guarantees that the ext-APINAMESPACE.js
3806    * scripts expects (what InjectionContext does for the regular bindings).
3807    *
3808    * @param {object}     extContext
3809    * @param {string}     apiNamespace
3810    * @param {string}     apiName
3811    * @param {Array<any>} args
3812    *
3813    * @returns {Array<any>} Normalized arguments array.
3814    */
3815   checkParameters(extContext, apiNamespace, apiName, args) {
3816     const apiSchema = this.getNamespace(apiNamespace)?.get(apiName);
3817     if (!apiSchema) {
3818       throw new Error(`API Schema not found for ${apiNamespace}.${apiName}`);
3819     }
3821     return apiSchema.checkParameters(
3822       args,
3823       this.paramsValidationContexts.get(extContext)
3824     );
3825   },