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";
7
8const global = this;
9
10const { AppConstants } = ChromeUtils.import(
11  "resource://gre/modules/AppConstants.jsm"
12);
13const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14const { XPCOMUtils } = ChromeUtils.import(
15  "resource://gre/modules/XPCOMUtils.jsm"
16);
17
18XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
19
20const { ExtensionUtils } = ChromeUtils.import(
21  "resource://gre/modules/ExtensionUtils.jsm"
22);
23var { DefaultMap, DefaultWeakMap } = ExtensionUtils;
24
25ChromeUtils.defineModuleGetter(
26  this,
27  "ExtensionParent",
28  "resource://gre/modules/ExtensionParent.jsm"
29);
30ChromeUtils.defineModuleGetter(
31  this,
32  "NetUtil",
33  "resource://gre/modules/NetUtil.jsm"
34);
35ChromeUtils.defineModuleGetter(
36  this,
37  "ShortcutUtils",
38  "resource://gre/modules/ShortcutUtils.jsm"
39);
40XPCOMUtils.defineLazyServiceGetter(
41  this,
42  "contentPolicyService",
43  "@mozilla.org/addons/content-policy;1",
44  "nsIAddonContentPolicy"
45);
46
47XPCOMUtils.defineLazyGetter(
48  this,
49  "StartupCache",
50  () => ExtensionParent.StartupCache
51);
52
53XPCOMUtils.defineLazyPreferenceGetter(
54  this,
55  "treatWarningsAsErrors",
56  "extensions.webextensions.warnings-as-errors",
57  false
58);
59
60var EXPORTED_SYMBOLS = ["SchemaRoot", "Schemas"];
61
62const KEY_CONTENT_SCHEMAS = "extensions-framework/schemas/content";
63const KEY_PRIVILEGED_SCHEMAS = "extensions-framework/schemas/privileged";
64
65const MIN_MANIFEST_VERSION = 2;
66const MAX_MANIFEST_VERSION = 3;
67
68const { DEBUG } = AppConstants;
69
70const isParentProcess =
71  Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
72
73function 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          );
89
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);
96
97          resolve(JSON.parse(text));
98        } catch (e) {
99          reject(e);
100        }
101      }
102    );
103  });
104}
105
106function 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  }
115
116  let result = {};
117
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    }
126
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  }
133
134  return result;
135}
136
137function 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);
142
143  return new StructuredCloneHolder(json);
144}
145
146async function readJSONAndBlobbify(url) {
147  let json = await readJSON(url);
148
149  return blobbify(json);
150}
151
152/**
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.
157 *
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.
161 *
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 */
170function exportLazyGetter(object, prop, getter) {
171  object = ChromeUtils.waiveXrays(object);
172
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    }
184
185    getter = null;
186
187    return value;
188  };
189
190  Object.defineProperty(object, prop, {
191    enumerable: true,
192    configurable: true,
193
194    get: Cu.exportFunction(function() {
195      return redefine(getter.call(this));
196    }, object),
197
198    set: Cu.exportFunction(value => {
199      redefine(value);
200    }, object),
201  });
202}
203
204/**
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.
208 *
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.
212 *
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 */
224function exportLazyProperty(object, prop, getter) {
225  object = ChromeUtils.waiveXrays(object);
226
227  let redefine = obj => {
228    let desc = getter.call(obj);
229    getter = null;
230
231    delete object[prop];
232    if (desc) {
233      let defaults = {
234        configurable: true,
235        enumerable: true,
236      };
237
238      if (!desc.set && !desc.get) {
239        defaults.writable = true;
240      }
241
242      Object.defineProperty(object, prop, Object.assign(defaults, desc));
243    }
244  };
245
246  Object.defineProperty(object, prop, {
247    enumerable: true,
248    configurable: true,
249
250    get: Cu.exportFunction(function() {
251      redefine(this);
252      return object[prop];
253    }, object),
254
255    set: Cu.exportFunction(function(value) {
256      redefine(this);
257      object[prop] = value;
258    }, object),
259  });
260}
261
262const 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);
272
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    }
282
283    return string;
284  },
285  requireBackgroundServiceWorkerEnabled(value, context) {
286    if (WebExtensionPolicy.backgroundServiceWorkerEnabled) {
287      return value;
288    }
289
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  },
296
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  },
309};
310
311// Parses a regular expression, with support for the Python extended
312// syntax that allows setting flags by including the string (?im)
313function 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);
320}
321
322function 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;
333
334    case "number":
335      if (value % 1 === 0) {
336        return "integer";
337      }
338  }
339  return type;
340}
341
342// Methods of Context that are used by Schemas.normalize. These methods can be
343// overridden at the construction of Context.
344const CONTEXT_FOR_VALIDATION = ["checkLoadURL", "hasPermission", "logError"];
345
346// Methods of Context that are used by Schemas.inject.
347// Callers of Schemas.inject should implement all of these methods.
348const CONTEXT_FOR_INJECTION = [
349  ...CONTEXT_FOR_VALIDATION,
350  "getImplementation",
351  "isPermissionRevokable",
352  "shouldInject",
353];
354
355// If the message is a function, call it and return the result.
356// Otherwise, assume it's a string.
357function forceString(msg) {
358  if (typeof msg === "function") {
359    return msg();
360  }
361  return msg;
362}
363
364/**
365 * A context for schema validation and error reporting. This class is only used
366 * internally within Schemas.
367 */
368class 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;
375
376    if (typeof params.manifestVersion !== "number") {
377      throw new Error(
378        `Unexpected params.manifestVersion value: ${params.manifestVersion}`
379      );
380    }
381
382    this.path = [];
383    this.preprocessors = {
384      localize(value, context) {
385        return value;
386      },
387    };
388    this.postprocessors = POSTPROCESSORS;
389    this.isChromeCompat = false;
390
391    this.currentChoices = new Set();
392    this.choicePathIndex = 0;
393
394    for (let method of overridableMethods) {
395      if (method in params) {
396        this[method] = params[method].bind(params);
397      }
398    }
399
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  }
411
412  get choicePath() {
413    let path = this.path.slice(this.choicePathIndex);
414    return path.join(".");
415  }
416
417  get cloneScope() {
418    return this.params.cloneScope || undefined;
419  }
420
421  get url() {
422    return this.params.url;
423  }
424
425  get principal() {
426    return (
427      this.params.principal ||
428      Services.scriptSecurityManager.createNullPrincipal({})
429    );
430  }
431
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  }
451
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  }
463
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  }
476
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      }
508
509      this.currentChoices.add(choicesMessage);
510    }
511
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  }
523
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  }
544
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  }
568
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  }
582
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;
595
596    let choices = new Set();
597    this.currentChoices = choices;
598    this.choicePathIndex = this.path.length;
599
600    try {
601      let result = callback();
602
603      return { result, choices };
604    } finally {
605      this.currentChoices = currentChoices;
606      this.choicePathIndex = choicePathIndex;
607
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]}`;
617
618          return `must either [${array.join(", ")}]`;
619        });
620      }
621    }
622  }
623
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  }
644
645  matchManifestVersion(entry) {
646    let { manifestVersion } = this;
647    return (
648      manifestVersion >= entry.min_manifest_version &&
649      manifestVersion <= entry.max_manifest_version
650    );
651  }
652}
653
654/**
655 * Represents a schema entry to be injected into an object. Handles the
656 * injection, revocation, and permissions of said entry.
657 *
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 */
671class 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;
679
680    this.injected = null;
681    this.lazyInjected = null;
682  }
683
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  }
696
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  }
710
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  }
722
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  }
738
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      }
750
751      try {
752        let unwrapped = ChromeUtils.waiveXrays(this.parentObj);
753        delete unwrapped[this.name];
754      } catch (e) {
755        Cu.reportError(e);
756      }
757
758      let { value } = this.injected.descriptor;
759      if (value) {
760        this.context.revokeChildren(value);
761      }
762
763      this.injected = null;
764    }
765  }
766
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;
777
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    }
784
785    if (!this.shouldInject) {
786      return;
787    }
788
789    if (this.isRevokable) {
790      this.context.pendingEntries.add(this);
791    }
792
793    if (!this.hasPermission) {
794      return;
795    }
796
797    this.injected = this.entry.getDescriptor(this.path, this.context);
798    if (!this.injected) {
799      return undefined;
800    }
801
802    return this.injected.descriptor;
803  }
804
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    }
817
818    this.lazyInjected = true;
819    exportLazyProperty(this.parentObj, this.name, () => {
820      if (this.lazyInjected) {
821        return this.getDescriptor();
822      }
823    });
824  }
825
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  }
837
838  maybeInject() {
839    if (!this.injected && !this.lazyInjected) {
840      this.lazyInject();
841    }
842  }
843
844  maybeRevoke() {
845    if (this.injected && !this.hasPermission) {
846      this.revoke();
847    }
848  }
849}
850
851/**
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 */
856class InjectionContext extends Context {
857  constructor(params, schemaRoot) {
858    super(params, CONTEXT_FOR_INJECTION);
859
860    this.schemaRoot = schemaRoot;
861
862    this.pendingEntries = new Set();
863    this.children = new DefaultWeakMap(() => new Map());
864
865    this.injectedRoots = new Set();
866
867    if (params.setPermissionsChangedCallback) {
868      params.setPermissionsChangedCallback(this.permissionsChanged.bind(this));
869    }
870  }
871
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  }
890
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  }
903
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  }
917
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    }
929
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);
938
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  }
946
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    );
956
957    this.children.get(dest).set(name, injection);
958
959    return injection;
960  }
961
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    );
989
990    return injection.getDescriptor();
991  }
992
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    );
1015
1016    injection.lazyInject();
1017  }
1018}
1019
1020/**
1021 * The methods in this singleton represent the "format" specifier for
1022 * JSON Schema string types.
1023 *
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 */
1028const FORMATS = {
1029  hostname(string, context) {
1030    let valid = true;
1031
1032    try {
1033      valid = new URL(`http://${string}`).host === string;
1034    } catch (e) {
1035      valid = false;
1036    }
1037
1038    if (!valid) {
1039      throw new Error(`Invalid hostname ${string}`);
1040    }
1041
1042    return string;
1043  },
1044
1045  url(string, context) {
1046    let url = new URL(string).href;
1047
1048    if (!context.checkLoadURL(url)) {
1049      throw new Error(`Access denied for URL ${url}`);
1050    }
1051    return url;
1052  },
1053
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  },
1077
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    }
1088
1089    let url = new URL(string, context.url).href;
1090
1091    if (!context.checkLoadURL(url)) {
1092      throw new Error(`Access denied for URL ${url}`);
1093    }
1094    return url;
1095  },
1096
1097  strictRelativeUrl(string, context) {
1098    void FORMATS.unresolvedRelativeUrl(string, context);
1099    return FORMATS.relativeUrl(string, context);
1100  },
1101
1102  unresolvedRelativeUrl(string, context) {
1103    if (!string.startsWith("//")) {
1104      try {
1105        new URL(string);
1106      } catch (e) {
1107        return string;
1108      }
1109    }
1110
1111    throw new SyntaxError(
1112      `String ${JSON.stringify(string)} must be a relative URL`
1113    );
1114  },
1115
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  },
1124
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  },
1144
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  },
1163
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  },
1178
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  },
1191
1192  manifestShortcutKeyOrEmpty(string, context) {
1193    return string === "" ? "" : FORMATS.manifestShortcutKey(string, context);
1194  },
1195};
1196
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.
1200class 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    }
1219
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;
1227
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;
1235
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 || [];
1242
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  }
1248
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  }
1263
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    }
1276
1277    let value = context.postprocessors[this.postprocessor](
1278      result.value,
1279      context
1280    );
1281    return { value };
1282  }
1283
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    }
1304
1305    this.logWarning(context, message);
1306  }
1307
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);
1315
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  }
1331
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  }
1344
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  }
1360}
1361
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.
1364class 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  }
1381
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);
1406
1407    return new this(schema);
1408  }
1409
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]);
1431
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  }
1443
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  }
1452
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
1457
1458  checkBaseType(baseType) {
1459    return false;
1460  }
1461
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    }
1469
1470    let choice;
1471    if ("aeiou".includes(type[0])) {
1472      choice = `be an ${type} value`;
1473    } else {
1474      choice = `be a ${type} value`;
1475    }
1476
1477    return context.error(
1478      () => `Expected ${type} instead of ${JSON.stringify(value)}`,
1479      choice
1480    );
1481  }
1482}
1483
1484// Type that allows any value.
1485class AnyType extends Type {
1486  normalize(value, context) {
1487    this.checkDeprecated(context, value);
1488    return this.postprocess({ value }, context);
1489  }
1490
1491  checkBaseType(baseType) {
1492    return true;
1493  }
1494}
1495
1496// An untagged union type.
1497class ChoiceType extends Type {
1498  static get EXTRA_PROPERTIES() {
1499    return ["choices", ...super.EXTRA_PROPERTIES];
1500  }
1501
1502  static parseSchema(root, schema, path, extraProperties = []) {
1503    this.checkSchemaProperties(schema, path, extraProperties);
1504
1505    let choices = schema.choices.map(t => root.parseSchema(t, path));
1506    return new this(schema, choices);
1507  }
1508
1509  constructor(schema, choices) {
1510    super(schema);
1511    this.choices = choices;
1512  }
1513
1514  extend(type) {
1515    this.choices.push(...type.choices);
1516
1517    return this;
1518  }
1519
1520  normalize(value, context) {
1521    this.checkDeprecated(context, value);
1522
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        }
1531
1532        let r = choice.normalize(value, context);
1533        if (!r.error) {
1534          return r;
1535        }
1536
1537        error = r;
1538      }
1539    });
1540
1541    if (result) {
1542      return result;
1543    }
1544    if (choices.size <= 1) {
1545      return error;
1546    }
1547
1548    choices = Array.from(choices, forceString);
1549    let n = choices.length - 1;
1550    choices[n] = `or ${choices[n]}`;
1551
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    }
1559
1560    return context.error(message, null);
1561  }
1562
1563  checkBaseType(baseType) {
1564    return this.choices.some(t => t.checkBaseType(baseType));
1565  }
1566
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    }
1577
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    }
1591
1592    return { descriptor };
1593  }
1594}
1595
1596// This is a reference to another type--essentially a typedef.
1597class RefType extends Type {
1598  static get EXTRA_PROPERTIES() {
1599    return ["$ref", ...super.EXTRA_PROPERTIES];
1600  }
1601
1602  static parseSchema(root, schema, path, extraProperties = []) {
1603    this.checkSchemaProperties(schema, path, extraProperties);
1604
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  }
1612
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  }
1621
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  }
1630
1631  normalize(value, context) {
1632    this.checkDeprecated(context, value);
1633    return this.targetType.normalize(value, context);
1634  }
1635
1636  checkBaseType(baseType) {
1637    return this.targetType.checkBaseType(baseType);
1638  }
1639}
1640
1641class 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  }
1652
1653  static parseSchema(root, schema, path, extraProperties = []) {
1654    this.checkSchemaProperties(schema, path, extraProperties);
1655
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    }
1668
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    }
1679
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  }
1699
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  }
1717
1718  normalize(value, context) {
1719    let r = this.normalizeBase("string", value, context);
1720    if (r.error) {
1721      return r;
1722    }
1723    value = r.value;
1724
1725    if (this.enumeration) {
1726      if (this.enumeration.includes(value)) {
1727        return this.postprocess({ value }, context);
1728      }
1729
1730      let choices = this.enumeration.map(JSON.stringify).join(", ");
1731
1732      return context.error(
1733        () => `Invalid enumeration value ${JSON.stringify(value)}`,
1734        `be one of [${choices}]`
1735      );
1736    }
1737
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    }
1756
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    }
1763
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    }
1774
1775    return r;
1776  }
1777
1778  checkBaseType(baseType) {
1779    return baseType == "string";
1780  }
1781
1782  getDescriptor(path, context) {
1783    if (this.enumeration) {
1784      let obj = Cu.createObjectIn(context.cloneScope);
1785
1786      for (let e of this.enumeration) {
1787        obj[e.toUpperCase()] = e;
1788      }
1789
1790      return {
1791        descriptor: { value: obj },
1792      };
1793    }
1794  }
1795}
1796
1797class NullType extends Type {
1798  normalize(value, context) {
1799    return this.normalizeBase("null", value, context);
1800  }
1801
1802  checkBaseType(baseType) {
1803    return baseType == "null";
1804  }
1805}
1806
1807let FunctionEntry;
1808let Event;
1809let SubModuleType;
1810
1811class ObjectType extends Type {
1812  static get EXTRA_PROPERTIES() {
1813    return [
1814      "properties",
1815      "patternProperties",
1816      "$import",
1817      ...super.EXTRA_PROPERTIES,
1818    ];
1819  }
1820
1821  static parseSchema(root, schema, path, extraProperties = []) {
1822    if ("functions" in schema) {
1823      return SubModuleType.parseSchema(root, schema, path, extraProperties);
1824    }
1825
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);
1835
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    }
1846
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    };
1866
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    }
1874
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      }
1886
1887      patternProperties.push({
1888        pattern,
1889        type: parseProperty(schema.patternProperties[propName]),
1890      });
1891    }
1892
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      }
1900
1901      additionalProperties = root.parseSchema(type, path);
1902    }
1903
1904    return new this(
1905      schema,
1906      properties,
1907      additionalProperties,
1908      patternProperties,
1909      schema.isInstanceOf || null,
1910      imported
1911    );
1912  }
1913
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;
1927
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      }
1935
1936      if (DEBUG && !(importedType instanceof ObjectType)) {
1937        throw new Error(
1938          `Internal error: cannot import non-object type ${path}`
1939        );
1940      }
1941
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  }
1955
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    }
1965
1966    this.patternProperties.push(...type.patternProperties);
1967
1968    return this;
1969  }
1970
1971  checkBaseType(baseType) {
1972    return baseType == "object";
1973  }
1974
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.
1994
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    }
2002
2003    return ChromeUtils.shallowClone(value);
2004  }
2005
2006  checkProperty(context, prop, propType, result, properties, remainingProps) {
2007    let { type, optional, unsupported, onError } = propType;
2008    let error = null;
2009
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    }
2056
2057    if (error) {
2058      if (onError == "warn") {
2059        this.logWarning(context, forceString(error.error));
2060      } else if (onError != "ignore") {
2061        throw error;
2062      }
2063
2064      result[prop] = propType.default;
2065    }
2066  }
2067
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;
2075
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        }
2089
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        }
2099
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      }
2104
2105      let properties = this.extractProperties(value, context);
2106      let remainingProps = new Set(Object.keys(properties));
2107
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      }
2119
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      }
2134
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      }
2157
2158      return this.postprocess({ value: result }, context);
2159    } catch (e) {
2160      if (e.error) {
2161        return e;
2162      }
2163      throw e;
2164    }
2165  }
2166}
2167
2168// This type is just a placeholder to be referred to by
2169// SubModuleProperty. No value is ever expected to have this type.
2170SubModuleType = class SubModuleType extends Type {
2171  static get EXTRA_PROPERTIES() {
2172    return ["functions", "events", "properties", ...super.EXTRA_PROPERTIES];
2173  }
2174
2175  static parseSchema(root, schema, path, extraProperties = []) {
2176    this.checkSchemaProperties(schema, path, extraProperties);
2177
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));
2183
2184    let events = [];
2185
2186    if (schema.events) {
2187      events = schema.events
2188        .filter(event => !event.unsupported)
2189        .map(event => Event.parseSchema(root, event, path));
2190    }
2191
2192    return new this(schema, functions, events);
2193  }
2194
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  }
2203};
2204
2205class NumberType extends Type {
2206  normalize(value, context) {
2207    let r = this.normalizeBase("number", value, context);
2208    if (r.error) {
2209      return r;
2210    }
2211
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    }
2218
2219    return r;
2220  }
2221
2222  checkBaseType(baseType) {
2223    return baseType == "number" || baseType == "integer";
2224  }
2225}
2226
2227class IntegerType extends Type {
2228  static get EXTRA_PROPERTIES() {
2229    return ["minimum", "maximum", ...super.EXTRA_PROPERTIES];
2230  }
2231
2232  static parseSchema(root, schema, path, extraProperties = []) {
2233    this.checkSchemaProperties(schema, path, extraProperties);
2234
2235    let { minimum = -Infinity, maximum = Infinity } = schema;
2236    return new this(schema, minimum, maximum);
2237  }
2238
2239  constructor(schema, minimum, maximum) {
2240    super(schema);
2241    this.minimum = minimum;
2242    this.maximum = maximum;
2243  }
2244
2245  normalize(value, context) {
2246    let r = this.normalizeBase("integer", value, context);
2247    if (r.error) {
2248      return r;
2249    }
2250    value = r.value;
2251
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    }
2259
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    }
2272
2273    return this.postprocess(r, context);
2274  }
2275
2276  checkBaseType(baseType) {
2277    return baseType == "integer";
2278  }
2279}
2280
2281class BooleanType extends Type {
2282  static get EXTRA_PROPERTIES() {
2283    return ["enum", ...super.EXTRA_PROPERTIES];
2284  }
2285
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  }
2291
2292  constructor(schema, enumeration) {
2293    super(schema);
2294    this.enumeration = enumeration;
2295  }
2296
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  }
2314
2315  checkBaseType(baseType) {
2316    return baseType == "boolean";
2317  }
2318}
2319
2320class ArrayType extends Type {
2321  static get EXTRA_PROPERTIES() {
2322    return ["items", "minItems", "maxItems", ...super.EXTRA_PROPERTIES];
2323  }
2324
2325  static parseSchema(root, schema, path, extraProperties = []) {
2326    this.checkSchemaProperties(schema, path, extraProperties);
2327
2328    let items = root.parseSchema(schema.items, path, ["onError"]);
2329
2330    return new this(
2331      schema,
2332      items,
2333      schema.minItems || 0,
2334      schema.maxItems || Infinity
2335    );
2336  }
2337
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  }
2345
2346  normalize(value, context) {
2347    let v = this.normalizeBase("array", value, context);
2348    if (v.error) {
2349      return v;
2350    }
2351    value = v.value;
2352
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    }
2368
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    }
2375
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    }
2382
2383    return this.postprocess({ value: result }, context);
2384  }
2385
2386  checkBaseType(baseType) {
2387    return baseType == "array";
2388  }
2389}
2390
2391class FunctionType extends Type {
2392  static get EXTRA_PROPERTIES() {
2393    return [
2394      "parameters",
2395      "async",
2396      "returns",
2397      "requireUserInput",
2398      ...super.EXTRA_PROPERTIES,
2399    ];
2400  }
2401
2402  static parseSchema(root, schema, path, extraProperties = []) {
2403    this.checkSchemaProperties(schema, path, extraProperties);
2404
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        }
2417
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    }
2433
2434    if (DEBUG) {
2435      if (isExpectingCallback) {
2436        throw new Error(
2437          `Internal error: Expected a callback parameter ` +
2438            `with name ${schema.async}`
2439        );
2440      }
2441
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    }
2458
2459    return new this(
2460      schema,
2461      parameters,
2462      isAsync,
2463      hasAsyncCallback,
2464      !!schema.requireUserInput
2465    );
2466  }
2467
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  }
2475
2476  normalize(value, context) {
2477    return this.normalizeBase("function", value, context);
2478  }
2479
2480  checkBaseType(baseType) {
2481    return baseType == "function";
2482  }
2483}
2484
2485// Represents a "property" defined in a schema namespace with a
2486// particular value. Essentially this is a constant.
2487class ValueProperty extends Entry {
2488  constructor(schema, name, value) {
2489    super(schema);
2490    this.name = name;
2491    this.value = value;
2492  }
2493
2494  getDescriptor(path, context) {
2495    // Prevent injection if not a supported version.
2496    if (!context.matchManifestVersion(this)) {
2497      return;
2498    }
2499
2500    return {
2501      descriptor: { value: this.value },
2502    };
2503  }
2504}
2505
2506// Represents a "property" defined in a schema namespace that is not a
2507// constant.
2508class 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  }
2517
2518  throwError(context, msg) {
2519    throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2520  }
2521
2522  getDescriptor(path, context) {
2523    if (this.unsupported || !context.matchManifestVersion(this)) {
2524      return;
2525    }
2526
2527    let apiImpl = context.getImplementation(path.join("."), this.name);
2528
2529    let getStub = () => {
2530      this.checkDeprecated(context);
2531      return apiImpl.getProperty();
2532    };
2533
2534    let descriptor = {
2535      get: Cu.exportFunction(getStub, context.cloneScope),
2536    };
2537
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        }
2544
2545        apiImpl.setProperty(normalized.value);
2546      };
2547
2548      descriptor.set = Cu.exportFunction(setStub, context.cloneScope);
2549    }
2550
2551    return {
2552      descriptor,
2553      revoke() {
2554        apiImpl.revoke();
2555        apiImpl = null;
2556      },
2557    };
2558  }
2559}
2560
2561class 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  }
2582
2583  getDescriptor(path, context) {
2584    let obj = Cu.createObjectIn(context.cloneScope);
2585
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    }
2597
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];
2607
2608    let functions = type.functions;
2609    for (let fun of functions) {
2610      context.injectInto(fun, obj, fun.name, subpath, ns);
2611    }
2612
2613    let events = type.events;
2614    for (let event of events) {
2615      context.injectInto(event, obj, event.name, subpath, ns);
2616    }
2617
2618    // TODO: Inject this.properties.
2619
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  }
2634}
2635
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).
2639class 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  }
2647
2648  throwError(context, msg) {
2649    throw context.makeError(`${msg} for ${this.path.join(".")}.${this.name}.`);
2650  }
2651
2652  checkParameters(args, context) {
2653    let fixedArgs = [];
2654
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      }
2664
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      }
2673
2674      if (argIndex == args.length) {
2675        return false;
2676      }
2677
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      }
2690
2691      return check(parameterIndex + 1, argIndex + 1);
2692    };
2693
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    }
2708
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    });
2724
2725    return fixedArgs;
2726  }
2727}
2728
2729// Represents a "function" defined in a schema namespace.
2730FunctionEntry = 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    }
2741
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  }
2761
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;
2778
2779    this.isAsync = type.isAsync;
2780    this.hasAsyncCallback = type.hasAsyncCallback;
2781    this.requireUserInput = type.requireUserInput;
2782  }
2783
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  }
2805
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  }
2812
2813  getDescriptor(path, context) {
2814    let apiImpl = context.getImplementation(path.join("."), this.name);
2815
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    }
2868
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  }
2881};
2882
2883// Represents an "event" defined in a schema namespace.
2884//
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
2888Event = 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    }));
2896
2897    let extraProperties = [
2898      "name",
2899      "unsupported",
2900      "permissions",
2901      "extraParameters",
2902      // We ignore these properties for now.
2903      "returns",
2904      "filters",
2905    ];
2906
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  }
2917
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  }
2932
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  }
2940
2941  getDescriptor(path, context) {
2942    let apiImpl = context.getImplementation(path.join("."), this.name);
2943
2944    let addStub = (listener, ...args) => {
2945      listener = this.checkListener(listener, context);
2946      let actuals = this.checkParameters(args, context);
2947      apiImpl.addListener(listener, actuals);
2948    };
2949
2950    let removeStub = listener => {
2951      listener = this.checkListener(listener, context);
2952      apiImpl.removeListener(listener);
2953    };
2954
2955    let hasStub = listener => {
2956      listener = this.checkListener(listener, context);
2957      return apiImpl.hasListener(listener);
2958    };
2959
2960    let obj = Cu.createObjectIn(context.cloneScope);
2961
2962    Cu.exportFunction(addStub, obj, { defineAs: "addListener" });
2963    Cu.exportFunction(removeStub, obj, { defineAs: "removeListener" });
2964    Cu.exportFunction(hasStub, obj, { defineAs: "hasListener" });
2965
2966    return {
2967      descriptor: { value: obj },
2968      revoke() {
2969        apiImpl.revoke();
2970        apiImpl = null;
2971
2972        let unwrapped = ChromeUtils.waiveXrays(obj);
2973        delete unwrapped.addListener;
2974        delete unwrapped.removeListener;
2975        delete unwrapped.hasListener;
2976      },
2977    };
2978  }
2979};
2980
2981const 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  })
2993);
2994
2995const LOADERS = {
2996  events: "loadEvent",
2997  functions: "loadFunction",
2998  properties: "loadProperty",
2999  types: "loadType",
3000};
3001
3002class Namespace extends Map {
3003  constructor(root, name, path) {
3004    super();
3005
3006    this.root = root;
3007
3008    this._lazySchemas = [];
3009    this.initialized = false;
3010
3011    this.name = name;
3012    this.path = name ? [...path, name] : [...path];
3013
3014    this.superNamespace = null;
3015
3016    this.min_manifest_version = MIN_MANIFEST_VERSION;
3017    this.max_manifest_version = MAX_MANIFEST_VERSION;
3018
3019    this.permissions = null;
3020    this.allowedContexts = [];
3021    this.defaultContexts = [];
3022  }
3023
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);
3034
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    }
3046
3047    if (schema.$import) {
3048      this.superNamespace = this.root.getNamespace(schema.$import);
3049    }
3050  }
3051
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    }
3060
3061    if (this.superNamespace) {
3062      this._lazySchemas.unshift(...this.superNamespace._lazySchemas);
3063    }
3064
3065    for (let type of Object.keys(LOADERS)) {
3066      this[type] = new DefaultMap(() => []);
3067    }
3068
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      }
3075
3076      for (let [name, prop] of Object.entries(schema.properties || {})) {
3077        if (!prop.unsupported) {
3078          this.properties.get(name).push(prop);
3079        }
3080      }
3081
3082      for (let fun of schema.functions || []) {
3083        if (!fun.unsupported) {
3084          this.functions.get(fun.name).push(fun);
3085        }
3086      }
3087
3088      for (let event of schema.events || []) {
3089        if (!event.unsupported) {
3090          this.events.get(event.name).push(event);
3091        }
3092      }
3093    }
3094
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    }
3105
3106    this.initialized = true;
3107
3108    if (DEBUG) {
3109      for (let key of this.keys()) {
3110        this.get(key);
3111      }
3112    }
3113  }
3114
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];
3131
3132    for (let schema of this[type].get(key)) {
3133      this.set(key, this[loader](key, schema));
3134    }
3135
3136    return this.get(key);
3137  }
3138
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  }
3145
3146  extendType(type) {
3147    let targetType = this.get(type.$extend);
3148
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    }
3163
3164    let parsed = this.root.parseSchema(type, this.path, ["$extend"]);
3165
3166    if (DEBUG && parsed.constructor !== targetType.constructor) {
3167      throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
3168    }
3169
3170    targetType.extend(parsed);
3171
3172    return targetType;
3173  }
3174
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  }
3208
3209  loadFunction(name, fun) {
3210    return FunctionEntry.parseSchema(this.root, fun, this.path);
3211  }
3212
3213  loadEvent(name, event) {
3214    return Event.parseSchema(this.root, event, this.path);
3215  }
3216
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);
3238
3239        return context.getDescriptor(entry, dest, name, this.path, this);
3240      });
3241    }
3242  }
3243
3244  getDescriptor(path, context) {
3245    let obj = Cu.createObjectIn(context.cloneScope);
3246
3247    let ns = context.schemaRoot.getNamespace(this.path.join("."));
3248    ns.injectInto(obj, context);
3249
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  }
3257
3258  keys() {
3259    this.init();
3260    return super.keys();
3261  }
3262
3263  *entries() {
3264    for (let key of this.keys()) {
3265      yield [key, this.get(key)];
3266    }
3267  }
3268
3269  get(key) {
3270    this.init();
3271    let value = super.get(key);
3272
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    }
3279
3280    return value;
3281  }
3282
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;
3299
3300    let idx = name.indexOf(".");
3301    if (idx > 0) {
3302      subName = name.slice(idx + 1);
3303      name = name.slice(0, idx);
3304    }
3305
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    }
3314
3315    if (subName) {
3316      return ns.getNamespace(subName);
3317    }
3318    return ns;
3319  }
3320
3321  getOwnNamespace(name) {
3322    return this.getNamespace(name);
3323  }
3324
3325  has(key) {
3326    this.init();
3327    return super.has(key);
3328  }
3329}
3330
3331/**
3332 * A namespace which combines the children of an arbitrary number of
3333 * sub-namespaces.
3334 */
3335class Namespaces extends Namespace {
3336  constructor(root, name, path, namespaces) {
3337    super(root, name, path);
3338
3339    this.namespaces = namespaces;
3340  }
3341
3342  injectInto(obj, context) {
3343    for (let ns of this.namespaces) {
3344      ns.injectInto(obj, context);
3345    }
3346  }
3347}
3348
3349/**
3350 * A root schema which combines the contents of an arbitrary number of base
3351 * schema roots.
3352 */
3353class SchemaRoots extends Namespaces {
3354  constructor(root, bases) {
3355    bases = bases.map(base => base.rootSchema || base);
3356
3357    super(null, "", [], bases);
3358
3359    this.root = root;
3360    this.bases = bases;
3361    this._namespaces = new Map();
3362  }
3363
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    }
3372
3373    if (results.length == 1) {
3374      return results[0];
3375    }
3376
3377    if (results.length) {
3378      return new Namespaces(this.root, name, name.split("."), results);
3379    }
3380    return null;
3381  }
3382
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  }
3393
3394  *getNamespaces(name) {
3395    for (let root of this.bases) {
3396      yield* root.getNamespaces(name);
3397    }
3398  }
3399}
3400
3401/**
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.
3405 *
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 */
3412class SchemaRoot extends Namespace {
3413  constructor(base, schemaJSON) {
3414    super(null, "", []);
3415
3416    if (Array.isArray(base)) {
3417      base = new SchemaRoots(this, base);
3418    }
3419
3420    this.root = this;
3421    this.base = base;
3422    this.schemaJSON = schemaJSON;
3423  }
3424
3425  *getNamespaces(path) {
3426    let name = path.join(".");
3427
3428    let ns = this.getNamespace(name, false);
3429    if (ns) {
3430      yield ns;
3431    }
3432
3433    if (this.base) {
3434      yield* this.base.getNamespaces(name);
3435    }
3436  }
3437
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    }
3455
3456    ns = this.base && this.base.getNamespace(name, false);
3457    if (ns) {
3458      return ns;
3459    }
3460    return create && super.getNamespace(name, create);
3461  }
3462
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  }
3473
3474  parseSchema(schema, path, extraProperties = []) {
3475    let allowedProperties = DEBUG && new Set(extraProperties);
3476
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    }
3482
3483    let type = TYPES[schema.type];
3484
3485    if (DEBUG) {
3486      allowedProperties.add("type");
3487
3488      if (!("type" in schema)) {
3489        throw new Error(`Unexpected value for type: ${JSON.stringify(schema)}`);
3490      }
3491
3492      if (!type) {
3493        throw new Error(`Unexpected type ${schema.type}`);
3494      }
3495    }
3496
3497    return type.parseSchema(this, schema, path, allowedProperties);
3498  }
3499
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);
3505
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        }
3514
3515        this.loadSchema(schema);
3516      } catch (e) {
3517        Cu.reportError(e);
3518      }
3519    }
3520  }
3521
3522  loadSchema(json) {
3523    for (let namespace of json) {
3524      this.getOwnNamespace(namespace.namespace).addSchema(namespace);
3525    }
3526  }
3527
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  }
3549
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);
3560
3561    this.injectInto(dest, context);
3562  }
3563
3564  injectInto(dest, context) {
3565    // For schema graphs where multiple schema roots have the same base, don't
3566    // inject it more than once.
3567
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  }
3576
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);
3590
3591    let result = type.normalize(obj, new Context(context));
3592    if (result.error) {
3593      return { error: forceString(result.error) };
3594    }
3595    return result;
3596  }
3597}
3598
3599this.Schemas = {
3600  initialized: false,
3601
3602  REVOKE: Symbol("@@revoke"),
3603
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(),
3607
3608  // A map of schema JSON which should be available in all content processes.
3609  contentSchemaJSON: new Map(),
3610
3611  // A map of schema JSON which should only be available to extension processes.
3612  privilegedSchemaJSON: new Map(),
3613
3614  _rootSchema: null,
3615
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  ),
3623
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  },
3634
3635  getNamespace(name) {
3636    return this.rootSchema.getNamespace(name);
3637  },
3638
3639  init() {
3640    if (this.initialized) {
3641      return;
3642    }
3643    this.initialized = true;
3644
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      };
3651
3652      if (WebExtensionPolicy.isExtensionProcess || DEBUG) {
3653        addSchemas(Services.cpmm.sharedData.get(KEY_PRIVILEGED_SCHEMAS));
3654      }
3655
3656      let schemas = Services.cpmm.sharedData.get(KEY_CONTENT_SCHEMAS);
3657      if (schemas) {
3658        addSchemas(schemas);
3659      }
3660    }
3661  },
3662
3663  _loadCachedSchemasPromise: null,
3664  loadCachedSchemas() {
3665    if (!this._loadCachedSchemasPromise) {
3666      this._loadCachedSchemasPromise = StartupCache.schemas
3667        .getAll()
3668        .then(results => {
3669          return results;
3670        });
3671    }
3672
3673    return this._loadCachedSchemasPromise;
3674  },
3675
3676  addSchema(url, schema, content = false) {
3677    this.schemaJSON.set(url, schema);
3678
3679    if (content) {
3680      this.contentSchemaJSON.set(url, schema);
3681    } else {
3682      this.privilegedSchemaJSON.set(url, schema);
3683    }
3684
3685    if (this._rootSchema) {
3686      throw new Error("Schema loaded after root schema populated");
3687    }
3688  },
3689
3690  updateSharedSchemas() {
3691    let { sharedData } = Services.ppmm;
3692
3693    sharedData.set(KEY_CONTENT_SCHEMAS, this.contentSchemaJSON);
3694    sharedData.set(KEY_PRIVILEGED_SCHEMAS, this.privilegedSchemaJSON);
3695  },
3696
3697  fetch(url) {
3698    return readJSONAndBlobbify(url);
3699  },
3700
3701  processSchema(json) {
3702    return blobbify(json);
3703  },
3704
3705  async load(url, content = false) {
3706    if (!isParentProcess) {
3707      return;
3708    }
3709
3710    const startTime = Cu.now();
3711    let schemaCache = await this.loadCachedSchemas();
3712    const fromCache = schemaCache.has(url);
3713
3714    let blob =
3715      schemaCache.get(url) ||
3716      (await StartupCache.schemas.get(url, readJSONAndBlobbify));
3717
3718    if (!this.schemaJSON.has(url)) {
3719      this.addSchema(url, blob, content);
3720    }
3721
3722    ChromeUtils.addProfilerMarker(
3723      "ExtensionSchemas",
3724      { startTime },
3725      `load ${url}, from cache: ${fromCache}`
3726    );
3727  },
3728
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  },
3746
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  },
3772
3773  exportLazyGetter,
3774
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  },
3786
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  },
3799
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    }
3820
3821    return apiSchema.checkParameters(
3822      args,
3823      this.paramsValidationContexts.get(extContext)
3824    );
3825  },
3826};
3827