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
8/**
9 * This module contains utilities and base classes for logic which is
10 * common between the parent and child process, and in particular
11 * between ExtensionParent.jsm and ExtensionChild.jsm.
12 */
13
14/* exported ExtensionCommon */
15
16var EXPORTED_SYMBOLS = ["ExtensionCommon"];
17
18const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
19const { XPCOMUtils } = ChromeUtils.import(
20  "resource://gre/modules/XPCOMUtils.jsm"
21);
22
23XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
24
25XPCOMUtils.defineLazyModuleGetters(this, {
26  ConsoleAPI: "resource://gre/modules/Console.jsm",
27  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
28  Schemas: "resource://gre/modules/Schemas.jsm",
29  SchemaRoot: "resource://gre/modules/Schemas.jsm",
30});
31
32XPCOMUtils.defineLazyServiceGetter(
33  this,
34  "styleSheetService",
35  "@mozilla.org/content/style-sheet-service;1",
36  "nsIStyleSheetService"
37);
38
39const { ExtensionUtils } = ChromeUtils.import(
40  "resource://gre/modules/ExtensionUtils.jsm"
41);
42
43var {
44  DefaultMap,
45  DefaultWeakMap,
46  ExtensionError,
47  filterStack,
48  getInnerWindowID,
49  getUniqueId,
50} = ExtensionUtils;
51
52function getConsole() {
53  return new ConsoleAPI({
54    maxLogLevelPref: "extensions.webextensions.log.level",
55    prefix: "WebExtensions",
56  });
57}
58
59XPCOMUtils.defineLazyGetter(this, "console", getConsole);
60
61var ExtensionCommon;
62
63// Run a function and report exceptions.
64function runSafeSyncWithoutClone(f, ...args) {
65  try {
66    return f(...args);
67  } catch (e) {
68    dump(
69      `Extension error: ${e} ${e.fileName} ${
70        e.lineNumber
71      }\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(
72        Error()
73      )}]]\n`
74    );
75    Cu.reportError(e);
76  }
77}
78
79// Return true if the given value is an instance of the given
80// native type.
81function instanceOf(value, type) {
82  return (
83    value &&
84    typeof value === "object" &&
85    ChromeUtils.getClassName(value) === type
86  );
87}
88
89/**
90 * Convert any of several different representations of a date/time to a Date object.
91 * Accepts several formats:
92 * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
93 * either a number or a string.
94 *
95 * @param {Date|string|number} date
96 *      The date to convert.
97 * @returns {Date}
98 *      A Date object
99 */
100function normalizeTime(date) {
101  // Of all the formats we accept the "number of milliseconds since the epoch as a string"
102  // is an outlier, everything else can just be passed directly to the Date constructor.
103  return new Date(
104    typeof date == "string" && /^\d+$/.test(date) ? parseInt(date, 10) : date
105  );
106}
107
108function withHandlingUserInput(window, callable) {
109  let handle = window.windowUtils.setHandlingUserInput(true);
110  try {
111    return callable();
112  } finally {
113    handle.destruct();
114  }
115}
116
117/**
118 * Defines a lazy getter for the given property on the given object. The
119 * first time the property is accessed, the return value of the getter
120 * is defined on the current `this` object with the given property name.
121 * Importantly, this means that a lazy getter defined on an object
122 * prototype will be invoked separately for each object instance that
123 * it's accessed on.
124 *
125 * @param {object} object
126 *        The prototype object on which to define the getter.
127 * @param {string|Symbol} prop
128 *        The property name for which to define the getter.
129 * @param {function} getter
130 *        The function to call in order to generate the final property
131 *        value.
132 */
133function defineLazyGetter(object, prop, getter) {
134  let redefine = (obj, value) => {
135    Object.defineProperty(obj, prop, {
136      enumerable: true,
137      configurable: true,
138      writable: true,
139      value,
140    });
141    return value;
142  };
143
144  Object.defineProperty(object, prop, {
145    enumerable: true,
146    configurable: true,
147
148    get() {
149      return redefine(this, getter.call(this));
150    },
151
152    set(value) {
153      redefine(this, value);
154    },
155  });
156}
157
158function checkLoadURL(url, principal, options) {
159  let ssm = Services.scriptSecurityManager;
160
161  let flags = ssm.STANDARD;
162  if (!options.allowScript) {
163    flags |= ssm.DISALLOW_SCRIPT;
164  }
165  if (!options.allowInheritsPrincipal) {
166    flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
167  }
168  if (options.dontReportErrors) {
169    flags |= ssm.DONT_REPORT_ERRORS;
170  }
171
172  try {
173    ssm.checkLoadURIWithPrincipal(principal, Services.io.newURI(url), flags);
174  } catch (e) {
175    return false;
176  }
177  return true;
178}
179
180function makeWidgetId(id) {
181  id = id.toLowerCase();
182  // FIXME: This allows for collisions.
183  return id.replace(/[^a-z0-9_-]/g, "_");
184}
185
186function isDeadOrRemote(obj) {
187  return Cu.isDeadWrapper(obj) || Cu.isRemoteProxy(obj);
188}
189
190/**
191 * A sentinel class to indicate that an array of values should be
192 * treated as an array when used as a promise resolution value, but as a
193 * spread expression (...args) when passed to a callback.
194 */
195class SpreadArgs extends Array {
196  constructor(args) {
197    super();
198    this.push(...args);
199  }
200}
201
202/**
203 * Like SpreadArgs, but also indicates that the array values already
204 * belong to the target compartment, and should not be cloned before
205 * being passed.
206 *
207 * The `unwrappedValues` property contains an Array object which belongs
208 * to the target compartment, and contains the same unwrapped values
209 * passed the NoCloneSpreadArgs constructor.
210 */
211class NoCloneSpreadArgs {
212  constructor(args) {
213    this.unwrappedValues = args;
214  }
215
216  [Symbol.iterator]() {
217    return this.unwrappedValues[Symbol.iterator]();
218  }
219}
220
221const LISTENERS = Symbol("listeners");
222const ONCE_MAP = Symbol("onceMap");
223
224class EventEmitter {
225  constructor() {
226    this[LISTENERS] = new Map();
227    this[ONCE_MAP] = new WeakMap();
228  }
229
230  /**
231   * Checks whether there is some listener for the given event.
232   *
233   * @param {string} event
234   *       The name of the event to listen for.
235   * @returns {boolean}
236   */
237  has(event) {
238    return this[LISTENERS].has(event);
239  }
240
241  /**
242   * Adds the given function as a listener for the given event.
243   *
244   * The listener function may optionally return a Promise which
245   * resolves when it has completed all operations which event
246   * dispatchers may need to block on.
247   *
248   * @param {string} event
249   *       The name of the event to listen for.
250   * @param {function(string, ...any)} listener
251   *        The listener to call when events are emitted.
252   */
253  on(event, listener) {
254    let listeners = this[LISTENERS].get(event);
255    if (!listeners) {
256      listeners = new Set();
257      this[LISTENERS].set(event, listeners);
258    }
259
260    listeners.add(listener);
261  }
262
263  /**
264   * Removes the given function as a listener for the given event.
265   *
266   * @param {string} event
267   *       The name of the event to stop listening for.
268   * @param {function(string, ...any)} listener
269   *        The listener function to remove.
270   */
271  off(event, listener) {
272    let set = this[LISTENERS].get(event);
273    if (set) {
274      set.delete(listener);
275      set.delete(this[ONCE_MAP].get(listener));
276      if (!set.size) {
277        this[LISTENERS].delete(event);
278      }
279    }
280  }
281
282  /**
283   * Adds the given function as a listener for the given event once.
284   *
285   * @param {string} event
286   *       The name of the event to listen for.
287   * @param {function(string, ...any)} listener
288   *        The listener to call when events are emitted.
289   */
290  once(event, listener) {
291    let wrapper = (...args) => {
292      this.off(event, wrapper);
293      this[ONCE_MAP].delete(listener);
294
295      return listener(...args);
296    };
297    this[ONCE_MAP].set(listener, wrapper);
298
299    this.on(event, wrapper);
300  }
301
302  /**
303   * Triggers all listeners for the given event. If any listeners return
304   * a value, returns a promise which resolves when all returned
305   * promises have resolved. Otherwise, returns undefined.
306   *
307   * @param {string} event
308   *       The name of the event to emit.
309   * @param {any} args
310   *        Arbitrary arguments to pass to the listener functions, after
311   *        the event name.
312   * @returns {Promise?}
313   */
314  emit(event, ...args) {
315    let listeners = this[LISTENERS].get(event);
316
317    if (listeners) {
318      let promises = [];
319
320      for (let listener of listeners) {
321        try {
322          let result = listener(event, ...args);
323          if (result !== undefined) {
324            promises.push(result);
325          }
326        } catch (e) {
327          Cu.reportError(e);
328        }
329      }
330
331      if (promises.length) {
332        return Promise.all(promises);
333      }
334    }
335  }
336}
337
338/**
339 * Base class for WebExtension APIs.  Each API creates a new class
340 * that inherits from this class, the derived class is instantiated
341 * once for each extension that uses the API.
342 */
343class ExtensionAPI extends EventEmitter {
344  constructor(extension) {
345    super();
346
347    this.extension = extension;
348
349    extension.once("shutdown", (what, isAppShutdown) => {
350      if (this.onShutdown) {
351        this.onShutdown(isAppShutdown);
352      }
353      this.extension = null;
354    });
355  }
356
357  destroy() {}
358
359  onManifestEntry(entry) {}
360
361  getAPI(context) {
362    throw new Error("Not Implemented");
363  }
364}
365
366/**
367 * Subclass to add APIs commonly used with persistent events.
368 * If a namespace uses events, it should use this subclass.
369 *
370 * this.apiNamespace = class extends ExtensionAPIPersistent {};
371 */
372class ExtensionAPIPersistent extends ExtensionAPI {
373  /**
374   * Check for event entry.
375   *
376   * @param {string} event The event name e.g. onStateChanged
377   * @returns {boolean}
378   */
379  hasEventRegistrar(event) {
380    return (
381      this.PERSISTENT_EVENTS && Object.hasOwn(this.PERSISTENT_EVENTS, event)
382    );
383  }
384
385  /**
386   * Get the event registration fuction
387   *
388   * @param {string} event The event name e.g. onStateChanged
389   * @returns {Function} register is used to start the listener
390   *                     register returns an object containing
391   *                     a convert and unregister function.
392   */
393  getEventRegistrar(event) {
394    if (this.hasEventRegistrar(event)) {
395      return this.PERSISTENT_EVENTS[event].bind(this);
396    }
397  }
398
399  /**
400   * Used when instantiating an EventManager instance to register the listener.
401   *
402   * @param {Object} options used for event registration
403   * @param {BaseContext} options.context Passed when creating an EventManager instance.
404   * @param {string} options.event The function passed to the listener to fire the event.
405   * @param {Function} options.fire The function passed to the listener to fire the event.
406   * @returns {Function} the unregister function used in the EventManager.
407   */
408  registerEventListener(options) {
409    let register = this.getEventRegistrar(options.event);
410    if (register) {
411      return register(options).unregister;
412    }
413  }
414
415  /**
416   * Used to prime a listener for when the background script is not running.
417   *
418   * @param {string} event The event name e.g. onStateChanged or captiveURL.onChange.
419   * @param {Function} fire The function passed to the listener to fire the event.
420   * @param {Array} params Params passed to the event listener.
421   * @param {boolean} isInStartup unused here but passed for subclass use.
422   * @returns {Object} the unregister and convert functions used in the EventManager.
423   */
424  primeListener(event, fire, params, isInStartup) {
425    let register = this.getEventRegistrar(event);
426    if (register) {
427      return register({ fire, isInStartup }, ...params);
428    }
429  }
430}
431
432/**
433 * A wrapper around a window that returns the window iff the inner window
434 * matches the inner window at the construction of this wrapper.
435 *
436 * This wrapper should not be used after the inner window is destroyed.
437 **/
438class InnerWindowReference {
439  constructor(contentWindow, innerWindowID) {
440    this.contentWindow = contentWindow;
441    this.innerWindowID = innerWindowID;
442    this.needWindowIDCheck = false;
443
444    contentWindow.addEventListener(
445      "pagehide",
446      this,
447      { mozSystemGroup: true },
448      false
449    );
450    contentWindow.addEventListener(
451      "pageshow",
452      this,
453      { mozSystemGroup: true },
454      false
455    );
456  }
457
458  get() {
459    // If the pagehide event has fired, the inner window ID needs to be checked,
460    // in case the window ref is dereferenced in a pageshow listener (before our
461    // pageshow listener was dispatched) or during the unload event.
462    if (
463      !this.needWindowIDCheck ||
464      (!isDeadOrRemote(this.contentWindow) &&
465        getInnerWindowID(this.contentWindow) === this.innerWindowID)
466    ) {
467      return this.contentWindow;
468    }
469    return null;
470  }
471
472  invalidate() {
473    // If invalidate() is called while the inner window is in the bfcache, then
474    // we are unable to remove the event listener, and handleEvent will be
475    // called once more if the page is revived from the bfcache.
476    if (this.contentWindow && !isDeadOrRemote(this.contentWindow)) {
477      this.contentWindow.removeEventListener("pagehide", this, {
478        mozSystemGroup: true,
479      });
480      this.contentWindow.removeEventListener("pageshow", this, {
481        mozSystemGroup: true,
482      });
483    }
484    this.contentWindow = null;
485    this.needWindowIDCheck = false;
486  }
487
488  handleEvent(event) {
489    if (this.contentWindow) {
490      this.needWindowIDCheck = event.type === "pagehide";
491    } else {
492      // Remove listener when restoring from the bfcache - see invalidate().
493      event.currentTarget.removeEventListener("pagehide", this, {
494        mozSystemGroup: true,
495      });
496      event.currentTarget.removeEventListener("pageshow", this, {
497        mozSystemGroup: true,
498      });
499    }
500  }
501}
502
503/**
504 * This class contains the information we have about an individual
505 * extension.  It is never instantiated directly, instead subclasses
506 * for each type of process extend this class and add members that are
507 * relevant for that process.
508 * @abstract
509 */
510class BaseContext {
511  constructor(envType, extension) {
512    this.envType = envType;
513    this.onClose = new Set();
514    this.checkedLastError = false;
515    this._lastError = null;
516    this.contextId = getUniqueId();
517    this.unloaded = false;
518    this.extension = extension;
519    this.manifestVersion = extension.manifestVersion;
520    this.jsonSandbox = null;
521    this.active = true;
522    this.incognito = null;
523    this.messageManager = null;
524    this.contentWindow = null;
525    this.innerWindowID = 0;
526
527    // These two properties are assigned in ContentScriptContextChild subclass
528    // to keep a copy of the content script sandbox Error and Promise globals
529    // (which are used by the WebExtensions internals) before any extension
530    // content script code had any chance to redefine them.
531    this.cloneScopeError = null;
532    this.cloneScopePromise = null;
533  }
534
535  get Error() {
536    // Return the copy stored in the context instance (when the context is an instance of
537    // ContentScriptContextChild or the global from extension page window otherwise).
538    return this.cloneScopeError || this.cloneScope.Error;
539  }
540
541  get Promise() {
542    // Return the copy stored in the context instance (when the context is an instance of
543    // ContentScriptContextChild or the global from extension page window otherwise).
544    return this.cloneScopePromise || this.cloneScope.Promise;
545  }
546
547  get privateBrowsingAllowed() {
548    return this.extension.privateBrowsingAllowed;
549  }
550
551  /**
552   * Whether the extension context is using the WebIDL bindings for the
553   * WebExtensions APIs.
554   * To be overridden in subclasses (e.g. WorkerContextChild) and to be
555   * optionally used in ExtensionAPI classes to customize the behavior of the
556   * API when the calls to the extension API are originated from the WebIDL
557   * bindings.
558   */
559  get useWebIDLBindings() {
560    return false;
561  }
562
563  canAccessWindow(window) {
564    return this.extension.canAccessWindow(window);
565  }
566
567  canAccessContainer(userContextId) {
568    return this.extension.canAccessContainer(userContextId);
569  }
570
571  /**
572   * Opens a conduit linked to this context, populating related address fields.
573   * Only available in child contexts with an associated contentWindow.
574   * @param {object} subject
575   * @param {ConduitAddress} address
576   * @returns {PointConduit}
577   */
578  openConduit(subject, address) {
579    let wgc = this.contentWindow.windowGlobalChild;
580    let conduit = wgc.getActor("Conduits").openConduit(subject, {
581      id: subject.id || getUniqueId(),
582      extensionId: this.extension.id,
583      envType: this.envType,
584      ...address,
585    });
586    this.callOnClose(conduit);
587    conduit.setCloseCallback(() => {
588      this.forgetOnClose(conduit);
589    });
590    return conduit;
591  }
592
593  setContentWindow(contentWindow) {
594    if (!this.canAccessWindow(contentWindow)) {
595      throw new Error(
596        "BaseContext attempted to load when extension is not allowed due to incognito settings."
597      );
598    }
599
600    this.innerWindowID = getInnerWindowID(contentWindow);
601    this.messageManager = contentWindow.docShell.messageManager;
602
603    if (this.incognito == null) {
604      this.incognito = PrivateBrowsingUtils.isContentWindowPrivate(
605        contentWindow
606      );
607    }
608
609    let windowRef = new InnerWindowReference(contentWindow, this.innerWindowID);
610    Object.defineProperty(this, "active", {
611      configurable: true,
612      enumerable: true,
613      get: () => windowRef.get() !== null,
614    });
615    Object.defineProperty(this, "contentWindow", {
616      configurable: true,
617      enumerable: true,
618      get: () => windowRef.get(),
619    });
620    this.callOnClose({
621      close: () => {
622        // Allow other "close" handlers to use these properties, until the next tick.
623        Promise.resolve().then(() => {
624          windowRef.invalidate();
625          windowRef = null;
626          Object.defineProperty(this, "contentWindow", { value: null });
627          Object.defineProperty(this, "active", { value: false });
628        });
629      },
630    });
631  }
632
633  // All child contexts must implement logActivity.  This is handled if the child
634  // context subclasses ExtensionBaseContextChild.  ProxyContextParent overrides
635  // this with a noop for parent contexts.
636  logActivity(type, name, data) {
637    throw new Error(`Not implemented for ${this.envType}`);
638  }
639
640  get cloneScope() {
641    throw new Error("Not implemented");
642  }
643
644  get principal() {
645    throw new Error("Not implemented");
646  }
647
648  runSafe(callback, ...args) {
649    return this.applySafe(callback, args);
650  }
651
652  runSafeWithoutClone(callback, ...args) {
653    return this.applySafeWithoutClone(callback, args);
654  }
655
656  applySafe(callback, args, caller) {
657    if (this.unloaded) {
658      Cu.reportError("context.runSafe called after context unloaded", caller);
659    } else if (!this.active) {
660      Cu.reportError(
661        "context.runSafe called while context is inactive",
662        caller
663      );
664    } else {
665      try {
666        let { cloneScope } = this;
667        args = args.map(arg => Cu.cloneInto(arg, cloneScope));
668      } catch (e) {
669        Cu.reportError(e);
670        dump(
671          `runSafe failure: cloning into ${
672            this.cloneScope
673          }: ${e}\n\n${filterStack(Error())}`
674        );
675      }
676
677      return this.applySafeWithoutClone(callback, args, caller);
678    }
679  }
680
681  applySafeWithoutClone(callback, args, caller) {
682    if (this.unloaded) {
683      Cu.reportError(
684        "context.runSafeWithoutClone called after context unloaded",
685        caller
686      );
687    } else if (!this.active) {
688      Cu.reportError(
689        "context.runSafeWithoutClone called while context is inactive",
690        caller
691      );
692    } else {
693      try {
694        return Reflect.apply(callback, null, args);
695      } catch (e) {
696        dump(
697          `Extension error: ${e} ${e.fileName} ${
698            e.lineNumber
699          }\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(
700            Error()
701          )}]]\n`
702        );
703        Cu.reportError(e);
704      }
705    }
706  }
707
708  checkLoadURL(url, options = {}) {
709    // As an optimization, f the URL starts with the extension's base URL,
710    // don't do any further checks. It's always allowed to load it.
711    if (url.startsWith(this.extension.baseURL)) {
712      return true;
713    }
714
715    return checkLoadURL(url, this.principal, options);
716  }
717
718  /**
719   * Safely call JSON.stringify() on an object that comes from an
720   * extension.
721   *
722   * @param {array<any>} args Arguments for JSON.stringify()
723   * @returns {string} The stringified representation of obj
724   */
725  jsonStringify(...args) {
726    if (!this.jsonSandbox) {
727      this.jsonSandbox = Cu.Sandbox(this.principal, {
728        sameZoneAs: this.cloneScope,
729        wantXrays: false,
730      });
731    }
732
733    return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
734  }
735
736  callOnClose(obj) {
737    this.onClose.add(obj);
738  }
739
740  forgetOnClose(obj) {
741    this.onClose.delete(obj);
742  }
743
744  get lastError() {
745    this.checkedLastError = true;
746    return this._lastError;
747  }
748
749  set lastError(val) {
750    this.checkedLastError = false;
751    this._lastError = val;
752  }
753
754  /**
755   * Normalizes the given error object for use by the target scope. If
756   * the target is an error object which belongs to that scope, it is
757   * returned as-is. If it is an ordinary object with a `message`
758   * property, it is converted into an error belonging to the target
759   * scope. If it is an Error object which does *not* belong to the
760   * clone scope, it is reported, and converted to an unexpected
761   * exception error.
762   *
763   * @param {Error|object} error
764   * @param {SavedFrame?} [caller]
765   * @returns {Error}
766   */
767  normalizeError(error, caller) {
768    if (error instanceof this.Error) {
769      return error;
770    }
771    let message, fileName;
772    if (error && typeof error === "object") {
773      const isPlain = ChromeUtils.getClassName(error) === "Object";
774      if (isPlain && error.mozWebExtLocation) {
775        caller = error.mozWebExtLocation;
776      }
777      if (isPlain && caller && (error.mozWebExtLocation || !error.fileName)) {
778        caller = Cu.cloneInto(caller, this.cloneScope);
779        return ChromeUtils.createError(error.message, caller);
780      }
781
782      if (
783        isPlain ||
784        error instanceof ExtensionError ||
785        this.principal.subsumes(Cu.getObjectPrincipal(error))
786      ) {
787        message = error.message;
788        fileName = error.fileName;
789      }
790    }
791
792    if (!message) {
793      Cu.reportError(error);
794      message = "An unexpected error occurred";
795    }
796    return new this.Error(message, fileName);
797  }
798
799  /**
800   * Sets the value of `.lastError` to `error`, calls the given
801   * callback, and reports an error if the value has not been checked
802   * when the callback returns.
803   *
804   * @param {object} error An object with a `message` property. May
805   *     optionally be an `Error` object belonging to the target scope.
806   * @param {SavedFrame?} caller
807   *        The optional caller frame which triggered this callback, to be used
808   *        in error reporting.
809   * @param {function} callback The callback to call.
810   * @returns {*} The return value of callback.
811   */
812  withLastError(error, caller, callback) {
813    this.lastError = this.normalizeError(error);
814    try {
815      return callback();
816    } finally {
817      if (!this.checkedLastError) {
818        Cu.reportError(`Unchecked lastError value: ${this.lastError}`, caller);
819      }
820      this.lastError = null;
821    }
822  }
823
824  /**
825   * Captures the most recent stack frame which belongs to the extension.
826   *
827   * @returns {SavedFrame?}
828   */
829  getCaller() {
830    return ChromeUtils.getCallerLocation(this.principal);
831  }
832
833  /**
834   * Wraps the given promise so it can be safely returned to extension
835   * code in this context.
836   *
837   * If `callback` is provided, however, it is used as a completion
838   * function for the promise, and no promise is returned. In this case,
839   * the callback is called when the promise resolves or rejects. In the
840   * latter case, `lastError` is set to the rejection value, and the
841   * callback function must check `browser.runtime.lastError` or
842   * `extension.runtime.lastError` in order to prevent it being reported
843   * to the console.
844   *
845   * @param {Promise} promise The promise with which to wrap the
846   *     callback. May resolve to a `SpreadArgs` instance, in which case
847   *     each element will be used as a separate argument.
848   *
849   *     Unless the promise object belongs to the cloneScope global, its
850   *     resolution value is cloned into cloneScope prior to calling the
851   *     `callback` function or resolving the wrapped promise.
852   *
853   * @param {function} [callback] The callback function to wrap
854   *
855   * @returns {Promise|undefined} If callback is null, a promise object
856   *     belonging to the target scope. Otherwise, undefined.
857   */
858  wrapPromise(promise, callback = null) {
859    let caller = this.getCaller();
860    let applySafe = this.applySafe.bind(this);
861    if (Cu.getGlobalForObject(promise) === this.cloneScope) {
862      applySafe = this.applySafeWithoutClone.bind(this);
863    }
864
865    if (callback) {
866      promise.then(
867        args => {
868          if (this.unloaded) {
869            Cu.reportError(`Promise resolved after context unloaded\n`, caller);
870          } else if (!this.active) {
871            Cu.reportError(
872              `Promise resolved while context is inactive\n`,
873              caller
874            );
875          } else if (args instanceof NoCloneSpreadArgs) {
876            this.applySafeWithoutClone(callback, args.unwrappedValues, caller);
877          } else if (args instanceof SpreadArgs) {
878            applySafe(callback, args, caller);
879          } else {
880            applySafe(callback, [args], caller);
881          }
882        },
883        error => {
884          this.withLastError(error, caller, () => {
885            if (this.unloaded) {
886              Cu.reportError(
887                `Promise rejected after context unloaded\n`,
888                caller
889              );
890            } else if (!this.active) {
891              Cu.reportError(
892                `Promise rejected while context is inactive\n`,
893                caller
894              );
895            } else {
896              this.applySafeWithoutClone(callback, [], caller);
897            }
898          });
899        }
900      );
901    } else {
902      return new this.Promise((resolve, reject) => {
903        promise.then(
904          value => {
905            if (this.unloaded) {
906              Cu.reportError(
907                `Promise resolved after context unloaded\n`,
908                caller
909              );
910            } else if (!this.active) {
911              Cu.reportError(
912                `Promise resolved while context is inactive\n`,
913                caller
914              );
915            } else if (value instanceof NoCloneSpreadArgs) {
916              let values = value.unwrappedValues;
917              this.applySafeWithoutClone(
918                resolve,
919                values.length == 1 ? [values[0]] : [values],
920                caller
921              );
922            } else if (value instanceof SpreadArgs) {
923              applySafe(resolve, value.length == 1 ? value : [value], caller);
924            } else {
925              applySafe(resolve, [value], caller);
926            }
927          },
928          value => {
929            if (this.unloaded) {
930              Cu.reportError(
931                `Promise rejected after context unloaded: ${value &&
932                  value.message}\n`,
933                caller
934              );
935            } else if (!this.active) {
936              Cu.reportError(
937                `Promise rejected while context is inactive: ${value &&
938                  value.message}\n`,
939                caller
940              );
941            } else {
942              this.applySafeWithoutClone(
943                reject,
944                [this.normalizeError(value, caller)],
945                caller
946              );
947            }
948          }
949        );
950      });
951    }
952  }
953
954  unload() {
955    this.unloaded = true;
956
957    for (let obj of this.onClose) {
958      obj.close();
959    }
960    this.onClose.clear();
961  }
962
963  /**
964   * A simple proxy for unload(), for use with callOnClose().
965   */
966  close() {
967    this.unload();
968  }
969}
970
971/**
972 * An object that runs the implementation of a schema API. Instantiations of
973 * this interfaces are used by Schemas.jsm.
974 *
975 * @interface
976 */
977class SchemaAPIInterface {
978  /**
979   * Calls this as a function that returns its return value.
980   *
981   * @abstract
982   * @param {Array} args The parameters for the function.
983   * @returns {*} The return value of the invoked function.
984   */
985  callFunction(args) {
986    throw new Error("Not implemented");
987  }
988
989  /**
990   * Calls this as a function and ignores its return value.
991   *
992   * @abstract
993   * @param {Array} args The parameters for the function.
994   */
995  callFunctionNoReturn(args) {
996    throw new Error("Not implemented");
997  }
998
999  /**
1000   * Calls this as a function that completes asynchronously.
1001   *
1002   * @abstract
1003   * @param {Array} args The parameters for the function.
1004   * @param {function(*)} [callback] The callback to be called when the function
1005   *     completes.
1006   * @param {boolean} [requireUserInput=false] If true, the function should
1007   *                  fail if the browser is not currently handling user input.
1008   * @returns {Promise|undefined} Must be void if `callback` is set, and a
1009   *     promise otherwise. The promise is resolved when the function completes.
1010   */
1011  callAsyncFunction(args, callback, requireUserInput = false) {
1012    throw new Error("Not implemented");
1013  }
1014
1015  /**
1016   * Retrieves the value of this as a property.
1017   *
1018   * @abstract
1019   * @returns {*} The value of the property.
1020   */
1021  getProperty() {
1022    throw new Error("Not implemented");
1023  }
1024
1025  /**
1026   * Assigns the value to this as property.
1027   *
1028   * @abstract
1029   * @param {string} value The new value of the property.
1030   */
1031  setProperty(value) {
1032    throw new Error("Not implemented");
1033  }
1034
1035  /**
1036   * Registers a `listener` to this as an event.
1037   *
1038   * @abstract
1039   * @param {function} listener The callback to be called when the event fires.
1040   * @param {Array} args Extra parameters for EventManager.addListener.
1041   * @see EventManager.addListener
1042   */
1043  addListener(listener, args) {
1044    throw new Error("Not implemented");
1045  }
1046
1047  /**
1048   * Checks whether `listener` is listening to this as an event.
1049   *
1050   * @abstract
1051   * @param {function} listener The event listener.
1052   * @returns {boolean} Whether `listener` is registered with this as an event.
1053   * @see EventManager.hasListener
1054   */
1055  hasListener(listener) {
1056    throw new Error("Not implemented");
1057  }
1058
1059  /**
1060   * Unregisters `listener` from this as an event.
1061   *
1062   * @abstract
1063   * @param {function} listener The event listener.
1064   * @see EventManager.removeListener
1065   */
1066  removeListener(listener) {
1067    throw new Error("Not implemented");
1068  }
1069
1070  /**
1071   * Revokes the implementation object, and prevents any further method
1072   * calls from having external effects.
1073   *
1074   * @abstract
1075   */
1076  revoke() {
1077    throw new Error("Not implemented");
1078  }
1079}
1080
1081/**
1082 * An object that runs a locally implemented API.
1083 */
1084class LocalAPIImplementation extends SchemaAPIInterface {
1085  /**
1086   * Constructs an implementation of the `name` method or property of `pathObj`.
1087   *
1088   * @param {object} pathObj The object containing the member with name `name`.
1089   * @param {string} name The name of the implemented member.
1090   * @param {BaseContext} context The context in which the schema is injected.
1091   */
1092  constructor(pathObj, name, context) {
1093    super();
1094    this.pathObj = pathObj;
1095    this.name = name;
1096    this.context = context;
1097  }
1098
1099  revoke() {
1100    if (this.pathObj[this.name][Schemas.REVOKE]) {
1101      this.pathObj[this.name][Schemas.REVOKE]();
1102    }
1103
1104    this.pathObj = null;
1105    this.name = null;
1106    this.context = null;
1107  }
1108
1109  callFunction(args) {
1110    try {
1111      return this.pathObj[this.name](...args);
1112    } catch (e) {
1113      throw this.context.normalizeError(e);
1114    }
1115  }
1116
1117  callFunctionNoReturn(args) {
1118    try {
1119      this.pathObj[this.name](...args);
1120    } catch (e) {
1121      throw this.context.normalizeError(e);
1122    }
1123  }
1124
1125  callAsyncFunction(args, callback, requireUserInput) {
1126    let promise;
1127    try {
1128      if (requireUserInput) {
1129        if (!this.context.contentWindow.windowUtils.isHandlingUserInput) {
1130          throw new ExtensionError(
1131            `${this.name} may only be called from a user input handler`
1132          );
1133        }
1134      }
1135      promise = this.pathObj[this.name](...args) || Promise.resolve();
1136    } catch (e) {
1137      promise = Promise.reject(e);
1138    }
1139    return this.context.wrapPromise(promise, callback);
1140  }
1141
1142  getProperty() {
1143    return this.pathObj[this.name];
1144  }
1145
1146  setProperty(value) {
1147    this.pathObj[this.name] = value;
1148  }
1149
1150  addListener(listener, args) {
1151    try {
1152      this.pathObj[this.name].addListener.call(null, listener, ...args);
1153    } catch (e) {
1154      throw this.context.normalizeError(e);
1155    }
1156  }
1157
1158  hasListener(listener) {
1159    return this.pathObj[this.name].hasListener.call(null, listener);
1160  }
1161
1162  removeListener(listener) {
1163    this.pathObj[this.name].removeListener.call(null, listener);
1164  }
1165}
1166
1167// Recursively copy properties from source to dest.
1168function deepCopy(dest, source) {
1169  for (let prop in source) {
1170    let desc = Object.getOwnPropertyDescriptor(source, prop);
1171    if (typeof desc.value == "object") {
1172      if (!(prop in dest)) {
1173        dest[prop] = {};
1174      }
1175      deepCopy(dest[prop], source[prop]);
1176    } else {
1177      Object.defineProperty(dest, prop, desc);
1178    }
1179  }
1180}
1181
1182function getChild(map, key) {
1183  let child = map.children.get(key);
1184  if (!child) {
1185    child = {
1186      modules: new Set(),
1187      children: new Map(),
1188    };
1189
1190    map.children.set(key, child);
1191  }
1192  return child;
1193}
1194
1195function getPath(map, path) {
1196  for (let key of path) {
1197    map = getChild(map, key);
1198  }
1199  return map;
1200}
1201
1202function mergePaths(dest, source) {
1203  for (let name of source.modules) {
1204    dest.modules.add(name);
1205  }
1206
1207  for (let [name, child] of source.children.entries()) {
1208    mergePaths(getChild(dest, name), child);
1209  }
1210}
1211
1212/**
1213 * Manages loading and accessing a set of APIs for a specific extension
1214 * context.
1215 *
1216 * @param {BaseContext} context
1217 *        The context to manage APIs for.
1218 * @param {SchemaAPIManager} apiManager
1219 *        The API manager holding the APIs to manage.
1220 * @param {object} root
1221 *        The root object into which APIs will be injected.
1222 */
1223class CanOfAPIs {
1224  constructor(context, apiManager, root) {
1225    this.context = context;
1226    this.scopeName = context.envType;
1227    this.apiManager = apiManager;
1228    this.root = root;
1229
1230    this.apiPaths = new Map();
1231
1232    this.apis = new Map();
1233  }
1234
1235  /**
1236   * Synchronously loads and initializes an ExtensionAPI instance.
1237   *
1238   * @param {string} name
1239   *        The name of the API to load.
1240   */
1241  loadAPI(name) {
1242    if (this.apis.has(name)) {
1243      return;
1244    }
1245
1246    let { extension } = this.context;
1247
1248    let api = this.apiManager.getAPI(name, extension, this.scopeName);
1249    if (!api) {
1250      return;
1251    }
1252
1253    this.apis.set(name, api);
1254
1255    deepCopy(this.root, api.getAPI(this.context));
1256  }
1257
1258  /**
1259   * Asynchronously loads and initializes an ExtensionAPI instance.
1260   *
1261   * @param {string} name
1262   *        The name of the API to load.
1263   */
1264  async asyncLoadAPI(name) {
1265    if (this.apis.has(name)) {
1266      return;
1267    }
1268
1269    let { extension } = this.context;
1270    if (!Schemas.checkPermissions(name, extension)) {
1271      return;
1272    }
1273
1274    let api = await this.apiManager.asyncGetAPI(
1275      name,
1276      extension,
1277      this.scopeName
1278    );
1279    // Check again, because async;
1280    if (this.apis.has(name)) {
1281      return;
1282    }
1283
1284    this.apis.set(name, api);
1285
1286    deepCopy(this.root, api.getAPI(this.context));
1287  }
1288
1289  /**
1290   * Finds the API at the given path from the root object, and
1291   * synchronously loads the API that implements it if it has not
1292   * already been loaded.
1293   *
1294   * @param {string} path
1295   *        The "."-separated path to find.
1296   * @returns {*}
1297   */
1298  findAPIPath(path) {
1299    if (this.apiPaths.has(path)) {
1300      return this.apiPaths.get(path);
1301    }
1302
1303    let obj = this.root;
1304    let modules = this.apiManager.modulePaths;
1305
1306    let parts = path.split(".");
1307    for (let [i, key] of parts.entries()) {
1308      if (!obj) {
1309        return;
1310      }
1311      modules = getChild(modules, key);
1312
1313      for (let name of modules.modules) {
1314        if (!this.apis.has(name)) {
1315          this.loadAPI(name);
1316        }
1317      }
1318
1319      if (!(key in obj) && i < parts.length - 1) {
1320        obj[key] = {};
1321      }
1322      obj = obj[key];
1323    }
1324
1325    this.apiPaths.set(path, obj);
1326    return obj;
1327  }
1328
1329  /**
1330   * Finds the API at the given path from the root object, and
1331   * asynchronously loads the API that implements it if it has not
1332   * already been loaded.
1333   *
1334   * @param {string} path
1335   *        The "."-separated path to find.
1336   * @returns {Promise<*>}
1337   */
1338  async asyncFindAPIPath(path) {
1339    if (this.apiPaths.has(path)) {
1340      return this.apiPaths.get(path);
1341    }
1342
1343    let obj = this.root;
1344    let modules = this.apiManager.modulePaths;
1345
1346    let parts = path.split(".");
1347    for (let [i, key] of parts.entries()) {
1348      if (!obj) {
1349        return;
1350      }
1351      modules = getChild(modules, key);
1352
1353      for (let name of modules.modules) {
1354        if (!this.apis.has(name)) {
1355          await this.asyncLoadAPI(name);
1356        }
1357      }
1358
1359      if (!(key in obj) && i < parts.length - 1) {
1360        obj[key] = {};
1361      }
1362
1363      if (typeof obj[key] === "function") {
1364        obj = obj[key].bind(obj);
1365      } else {
1366        obj = obj[key];
1367      }
1368    }
1369
1370    this.apiPaths.set(path, obj);
1371    return obj;
1372  }
1373}
1374
1375/**
1376 * @class APIModule
1377 * @abstract
1378 *
1379 * @property {string} url
1380 *       The URL of the script which contains the module's
1381 *       implementation. This script must define a global property
1382 *       matching the modules name, which must be a class constructor
1383 *       which inherits from {@link ExtensionAPI}.
1384 *
1385 * @property {string} schema
1386 *       The URL of the JSON schema which describes the module's API.
1387 *
1388 * @property {Array<string>} scopes
1389 *       The list of scope names into which the API may be loaded.
1390 *
1391 * @property {Array<string>} manifest
1392 *       The list of top-level manifest properties which will trigger
1393 *       the module to be loaded, and its `onManifestEntry` method to be
1394 *       called.
1395 *
1396 * @property {Array<string>} events
1397 *       The list events which will trigger the module to be loaded, and
1398 *       its appropriate event handler method to be called. Currently
1399 *       only accepts "startup".
1400 *
1401 * @property {Array<string>} permissions
1402 *       An optional list of permissions, any of which must be present
1403 *       in order for the module to load.
1404 *
1405 * @property {Array<Array<string>>} paths
1406 *       A list of paths from the root API object which, when accessed,
1407 *       will cause the API module to be instantiated and injected.
1408 */
1409
1410/**
1411 * This object loads the ext-*.js scripts that define the extension API.
1412 *
1413 * This class instance is shared with the scripts that it loads, so that the
1414 * ext-*.js scripts and the instantiator can communicate with each other.
1415 */
1416class SchemaAPIManager extends EventEmitter {
1417  /**
1418   * @param {string} processType
1419   *     "main" - The main, one and only chrome browser process.
1420   *     "addon" - An addon process.
1421   *     "content" - A content process.
1422   *     "devtools" - A devtools process.
1423   * @param {SchemaRoot} schema
1424   */
1425  constructor(processType, schema) {
1426    super();
1427    this.processType = processType;
1428    this.global = null;
1429    if (schema) {
1430      this.schema = schema;
1431    }
1432
1433    this.modules = new Map();
1434    this.modulePaths = { children: new Map(), modules: new Set() };
1435    this.manifestKeys = new Map();
1436    this.eventModules = new DefaultMap(() => new Set());
1437    this.settingsModules = new Set();
1438
1439    this._modulesJSONLoaded = false;
1440
1441    this.schemaURLs = new Map();
1442
1443    this.apis = new DefaultWeakMap(() => new Map());
1444
1445    this._scriptScopes = [];
1446  }
1447
1448  onStartup(extension) {
1449    let promises = [];
1450    for (let apiName of this.eventModules.get("startup")) {
1451      promises.push(
1452        extension.apiManager.asyncGetAPI(apiName, extension).then(api => {
1453          if (api) {
1454            api.onStartup();
1455          }
1456        })
1457      );
1458    }
1459
1460    return Promise.all(promises);
1461  }
1462
1463  async loadModuleJSON(urls) {
1464    let promises = urls.map(url => fetch(url).then(resp => resp.json()));
1465
1466    return this.initModuleJSON(await Promise.all(promises));
1467  }
1468
1469  initModuleJSON(blobs) {
1470    for (let json of blobs) {
1471      this.registerModules(json);
1472    }
1473
1474    this._modulesJSONLoaded = true;
1475
1476    return new StructuredCloneHolder({
1477      modules: this.modules,
1478      modulePaths: this.modulePaths,
1479      manifestKeys: this.manifestKeys,
1480      eventModules: this.eventModules,
1481      settingsModules: this.settingsModules,
1482      schemaURLs: this.schemaURLs,
1483    });
1484  }
1485
1486  initModuleData(moduleData) {
1487    if (!this._modulesJSONLoaded) {
1488      let data = moduleData.deserialize({}, true);
1489
1490      this.modules = data.modules;
1491      this.modulePaths = data.modulePaths;
1492      this.manifestKeys = data.manifestKeys;
1493      this.eventModules = new DefaultMap(() => new Set(), data.eventModules);
1494      this.settingsModules = new Set(data.settingsModules);
1495      this.schemaURLs = data.schemaURLs;
1496    }
1497
1498    this._modulesJSONLoaded = true;
1499  }
1500
1501  /**
1502   * Registers a set of ExtensionAPI modules to be lazily loaded and
1503   * managed by this manager.
1504   *
1505   * @param {object} obj
1506   *        An object containing property for eacy API module to be
1507   *        registered. Each value should be an object implementing the
1508   *        APIModule interface.
1509   */
1510  registerModules(obj) {
1511    for (let [name, details] of Object.entries(obj)) {
1512      details.namespaceName = name;
1513
1514      if (this.modules.has(name)) {
1515        throw new Error(`Module '${name}' already registered`);
1516      }
1517      this.modules.set(name, details);
1518
1519      if (details.schema) {
1520        let content =
1521          details.scopes &&
1522          (details.scopes.includes("content_parent") ||
1523            details.scopes.includes("content_child"));
1524        this.schemaURLs.set(details.schema, { content });
1525      }
1526
1527      for (let event of details.events || []) {
1528        this.eventModules.get(event).add(name);
1529      }
1530
1531      if (details.settings) {
1532        this.settingsModules.add(name);
1533      }
1534
1535      for (let key of details.manifest || []) {
1536        if (this.manifestKeys.has(key)) {
1537          throw new Error(
1538            `Manifest key '${key}' already registered by '${this.manifestKeys.get(
1539              key
1540            )}'`
1541          );
1542        }
1543
1544        this.manifestKeys.set(key, name);
1545      }
1546
1547      for (let path of details.paths || []) {
1548        getPath(this.modulePaths, path).modules.add(name);
1549      }
1550    }
1551  }
1552
1553  /**
1554   * Emits an `onManifestEntry` event for the top-level manifest entry
1555   * on all relevant {@link ExtensionAPI} instances for the given
1556   * extension.
1557   *
1558   * The API modules will be synchronously loaded if they have not been
1559   * loaded already.
1560   *
1561   * @param {Extension} extension
1562   *        The extension for which to emit the events.
1563   * @param {string} entry
1564   *        The name of the top-level manifest entry.
1565   *
1566   * @returns {*}
1567   */
1568  emitManifestEntry(extension, entry) {
1569    let apiName = this.manifestKeys.get(entry);
1570    if (apiName) {
1571      let api = extension.apiManager.getAPI(apiName, extension);
1572      return api.onManifestEntry(entry);
1573    }
1574  }
1575  /**
1576   * Emits an `onManifestEntry` event for the top-level manifest entry
1577   * on all relevant {@link ExtensionAPI} instances for the given
1578   * extension.
1579   *
1580   * The API modules will be asynchronously loaded if they have not been
1581   * loaded already.
1582   *
1583   * @param {Extension} extension
1584   *        The extension for which to emit the events.
1585   * @param {string} entry
1586   *        The name of the top-level manifest entry.
1587   *
1588   * @returns {Promise<*>}
1589   */
1590  async asyncEmitManifestEntry(extension, entry) {
1591    let apiName = this.manifestKeys.get(entry);
1592    if (apiName) {
1593      let api = await extension.apiManager.asyncGetAPI(apiName, extension);
1594      return api.onManifestEntry(entry);
1595    }
1596  }
1597
1598  /**
1599   * Returns the {@link ExtensionAPI} instance for the given API module,
1600   * for the given extension, in the given scope, synchronously loading
1601   * and instantiating it if necessary.
1602   *
1603   * @param {string} name
1604   *        The name of the API module to load.
1605   * @param {Extension} extension
1606   *        The extension for which to load the API.
1607   * @param {string} [scope = null]
1608   *        The scope type for which to retrieve the API, or null if not
1609   *        being retrieved for a particular scope.
1610   *
1611   * @returns {ExtensionAPI?}
1612   */
1613  getAPI(name, extension, scope = null) {
1614    if (!this._checkGetAPI(name, extension, scope)) {
1615      return;
1616    }
1617
1618    let apis = this.apis.get(extension);
1619    if (apis.has(name)) {
1620      return apis.get(name);
1621    }
1622
1623    let module = this.loadModule(name);
1624
1625    let api = new module(extension);
1626    apis.set(name, api);
1627    return api;
1628  }
1629  /**
1630   * Returns the {@link ExtensionAPI} instance for the given API module,
1631   * for the given extension, in the given scope, asynchronously loading
1632   * and instantiating it if necessary.
1633   *
1634   * @param {string} name
1635   *        The name of the API module to load.
1636   * @param {Extension} extension
1637   *        The extension for which to load the API.
1638   * @param {string} [scope = null]
1639   *        The scope type for which to retrieve the API, or null if not
1640   *        being retrieved for a particular scope.
1641   *
1642   * @returns {Promise<ExtensionAPI>?}
1643   */
1644  async asyncGetAPI(name, extension, scope = null) {
1645    if (!this._checkGetAPI(name, extension, scope)) {
1646      return;
1647    }
1648
1649    let apis = this.apis.get(extension);
1650    if (apis.has(name)) {
1651      return apis.get(name);
1652    }
1653
1654    let module = await this.asyncLoadModule(name);
1655
1656    // Check again, because async.
1657    if (apis.has(name)) {
1658      return apis.get(name);
1659    }
1660
1661    let api = new module(extension);
1662    apis.set(name, api);
1663    return api;
1664  }
1665
1666  /**
1667   * Synchronously loads an API module, if not already loaded, and
1668   * returns its ExtensionAPI constructor.
1669   *
1670   * @param {string} name
1671   *        The name of the module to load.
1672   *
1673   * @returns {class}
1674   */
1675  loadModule(name) {
1676    let module = this.modules.get(name);
1677    if (module.loaded) {
1678      return this.global[name];
1679    }
1680
1681    this._checkLoadModule(module, name);
1682
1683    this.initGlobal();
1684
1685    Services.scriptloader.loadSubScript(module.url, this.global);
1686
1687    module.loaded = true;
1688
1689    return this.global[name];
1690  }
1691  /**
1692   * aSynchronously loads an API module, if not already loaded, and
1693   * returns its ExtensionAPI constructor.
1694   *
1695   * @param {string} name
1696   *        The name of the module to load.
1697   *
1698   * @returns {Promise<class>}
1699   */
1700  asyncLoadModule(name) {
1701    let module = this.modules.get(name);
1702    if (module.loaded) {
1703      return Promise.resolve(this.global[name]);
1704    }
1705    if (module.asyncLoaded) {
1706      return module.asyncLoaded;
1707    }
1708
1709    this._checkLoadModule(module, name);
1710
1711    module.asyncLoaded = ChromeUtils.compileScript(module.url).then(script => {
1712      this.initGlobal();
1713      script.executeInGlobal(this.global);
1714
1715      module.loaded = true;
1716
1717      return this.global[name];
1718    });
1719
1720    return module.asyncLoaded;
1721  }
1722
1723  asyncLoadSettingsModules() {
1724    return Promise.all(
1725      Array.from(this.settingsModules).map(apiName =>
1726        this.asyncLoadModule(apiName)
1727      )
1728    );
1729  }
1730
1731  getModule(name) {
1732    return this.modules.get(name);
1733  }
1734
1735  /**
1736   * Checks whether the given API module may be loaded for the given
1737   * extension, in the given scope.
1738   *
1739   * @param {string} name
1740   *        The name of the API module to check.
1741   * @param {Extension} extension
1742   *        The extension for which to check the API.
1743   * @param {string} [scope = null]
1744   *        The scope type for which to check the API, or null if not
1745   *        being checked for a particular scope.
1746   *
1747   * @returns {boolean}
1748   *        Whether the module may be loaded.
1749   */
1750  _checkGetAPI(name, extension, scope = null) {
1751    let module = this.getModule(name);
1752
1753    if (
1754      module.permissions &&
1755      !module.permissions.some(perm => extension.hasPermission(perm))
1756    ) {
1757      return false;
1758    }
1759
1760    if (!scope) {
1761      return true;
1762    }
1763
1764    if (!module.scopes.includes(scope)) {
1765      return false;
1766    }
1767
1768    if (!Schemas.checkPermissions(module.namespaceName, extension)) {
1769      return false;
1770    }
1771
1772    return true;
1773  }
1774
1775  _checkLoadModule(module, name) {
1776    if (!module) {
1777      throw new Error(`Module '${name}' does not exist`);
1778    }
1779    if (module.asyncLoaded) {
1780      throw new Error(`Module '${name}' currently being lazily loaded`);
1781    }
1782    if (this.global && this.global[name]) {
1783      throw new Error(
1784        `Module '${name}' conflicts with existing global property`
1785      );
1786    }
1787  }
1788
1789  /**
1790   * Create a global object that is used as the shared global for all ext-*.js
1791   * scripts that are loaded via `loadScript`.
1792   *
1793   * @returns {object} A sandbox that is used as the global by `loadScript`.
1794   */
1795  _createExtGlobal() {
1796    let global = Cu.Sandbox(
1797      Services.scriptSecurityManager.getSystemPrincipal(),
1798      {
1799        wantXrays: false,
1800        wantGlobalProperties: ["ChromeUtils"],
1801        sandboxName: `Namespace of ext-*.js scripts for ${this.processType} (from: resource://gre/modules/ExtensionCommon.jsm)`,
1802      }
1803    );
1804
1805    Object.assign(global, {
1806      Cc,
1807      ChromeWorker,
1808      Ci,
1809      Cr,
1810      Cu,
1811      ExtensionAPI,
1812      ExtensionAPIPersistent,
1813      ExtensionCommon,
1814      MatchGlob,
1815      MatchPattern,
1816      MatchPatternSet,
1817      Services,
1818      StructuredCloneHolder,
1819      WebExtensionPolicy,
1820      XPCOMUtils,
1821      extensions: this,
1822      global,
1823    });
1824
1825    ChromeUtils.import("resource://gre/modules/AppConstants.jsm", global);
1826
1827    XPCOMUtils.defineLazyGetter(global, "console", getConsole);
1828
1829    XPCOMUtils.defineLazyModuleGetters(global, {
1830      ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm",
1831      XPCOMUtils: "resource://gre/modules/XPCOMUtils.jsm",
1832    });
1833
1834    return global;
1835  }
1836
1837  initGlobal() {
1838    if (!this.global) {
1839      this.global = this._createExtGlobal();
1840    }
1841  }
1842
1843  /**
1844   * Load an ext-*.js script. The script runs in its own scope, if it wishes to
1845   * share state with another script it can assign to the `global` variable. If
1846   * it wishes to communicate with this API manager, use `extensions`.
1847   *
1848   * @param {string} scriptUrl The URL of the ext-*.js script.
1849   */
1850  loadScript(scriptUrl) {
1851    // Create the object in the context of the sandbox so that the script runs
1852    // in the sandbox's context instead of here.
1853    let scope = Cu.createObjectIn(this.global);
1854
1855    Services.scriptloader.loadSubScript(scriptUrl, scope);
1856
1857    // Save the scope to avoid it being garbage collected.
1858    this._scriptScopes.push(scope);
1859  }
1860}
1861
1862class LazyAPIManager extends SchemaAPIManager {
1863  constructor(processType, moduleData, schemaURLs) {
1864    super(processType);
1865
1866    this.initialized = false;
1867
1868    this.initModuleData(moduleData);
1869
1870    this.schemaURLs = schemaURLs;
1871  }
1872}
1873
1874defineLazyGetter(LazyAPIManager.prototype, "schema", function() {
1875  let root = new SchemaRoot(Schemas.rootSchema, this.schemaURLs);
1876  root.parseSchemas();
1877  return root;
1878});
1879
1880class MultiAPIManager extends SchemaAPIManager {
1881  constructor(processType, children) {
1882    super(processType);
1883
1884    this.initialized = false;
1885
1886    this.children = children;
1887  }
1888
1889  async lazyInit() {
1890    if (!this.initialized) {
1891      this.initialized = true;
1892
1893      for (let child of this.children) {
1894        if (child.lazyInit) {
1895          let res = child.lazyInit();
1896          if (res && typeof res.then === "function") {
1897            await res;
1898          }
1899        }
1900
1901        mergePaths(this.modulePaths, child.modulePaths);
1902      }
1903    }
1904  }
1905
1906  onStartup(extension) {
1907    return Promise.all(this.children.map(child => child.onStartup(extension)));
1908  }
1909
1910  getModule(name) {
1911    for (let child of this.children) {
1912      if (child.modules.has(name)) {
1913        return child.modules.get(name);
1914      }
1915    }
1916  }
1917
1918  loadModule(name) {
1919    for (let child of this.children) {
1920      if (child.modules.has(name)) {
1921        return child.loadModule(name);
1922      }
1923    }
1924  }
1925
1926  asyncLoadModule(name) {
1927    for (let child of this.children) {
1928      if (child.modules.has(name)) {
1929        return child.asyncLoadModule(name);
1930      }
1931    }
1932  }
1933}
1934
1935defineLazyGetter(MultiAPIManager.prototype, "schema", function() {
1936  let bases = this.children.map(child => child.schema);
1937
1938  // All API manager schema roots should derive from the global schema root,
1939  // so it doesn't need its own entry.
1940  if (bases[bases.length - 1] === Schemas) {
1941    bases.pop();
1942  }
1943
1944  if (bases.length === 1) {
1945    bases = bases[0];
1946  }
1947  return new SchemaRoot(bases, new Map());
1948});
1949
1950function LocaleData(data) {
1951  this.defaultLocale = data.defaultLocale;
1952  this.selectedLocale = data.selectedLocale;
1953  this.locales = data.locales || new Map();
1954  this.warnedMissingKeys = new Set();
1955
1956  // Map(locale-name -> Map(message-key -> localized-string))
1957  //
1958  // Contains a key for each loaded locale, each of which is a
1959  // Map of message keys to their localized strings.
1960  this.messages = data.messages || new Map();
1961
1962  if (data.builtinMessages) {
1963    this.messages.set(this.BUILTIN, data.builtinMessages);
1964  }
1965}
1966
1967LocaleData.prototype = {
1968  // Representation of the object to send to content processes. This
1969  // should include anything the content process might need.
1970  serialize() {
1971    return {
1972      defaultLocale: this.defaultLocale,
1973      selectedLocale: this.selectedLocale,
1974      messages: this.messages,
1975      locales: this.locales,
1976    };
1977  },
1978
1979  BUILTIN: "@@BUILTIN_MESSAGES",
1980
1981  has(locale) {
1982    return this.messages.has(locale);
1983  },
1984
1985  // https://developer.chrome.com/extensions/i18n
1986  localizeMessage(message, substitutions = [], options = {}) {
1987    let defaultOptions = {
1988      defaultValue: "",
1989      cloneScope: null,
1990    };
1991
1992    let locales = this.availableLocales;
1993    if (options.locale) {
1994      locales = new Set(
1995        [this.BUILTIN, options.locale, this.defaultLocale].filter(locale =>
1996          this.messages.has(locale)
1997        )
1998      );
1999    }
2000
2001    options = Object.assign(defaultOptions, options);
2002
2003    // Message names are case-insensitive, so normalize them to lower-case.
2004    message = message.toLowerCase();
2005    for (let locale of locales) {
2006      let messages = this.messages.get(locale);
2007      if (messages.has(message)) {
2008        let str = messages.get(message);
2009
2010        if (!str.includes("$")) {
2011          return str;
2012        }
2013
2014        if (!Array.isArray(substitutions)) {
2015          substitutions = [substitutions];
2016        }
2017
2018        let replacer = (matched, index, dollarSigns) => {
2019          if (index) {
2020            // This is not quite Chrome-compatible. Chrome consumes any number
2021            // of digits following the $, but only accepts 9 substitutions. We
2022            // accept any number of substitutions.
2023            index = parseInt(index, 10) - 1;
2024            return index in substitutions ? substitutions[index] : "";
2025          }
2026          // For any series of contiguous `$`s, the first is dropped, and
2027          // the rest remain in the output string.
2028          return dollarSigns;
2029        };
2030        return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer);
2031      }
2032    }
2033
2034    // Check for certain pre-defined messages.
2035    if (message == "@@ui_locale") {
2036      return this.uiLocale;
2037    } else if (message.startsWith("@@bidi_")) {
2038      let rtl = Services.locale.isAppLocaleRTL;
2039
2040      if (message == "@@bidi_dir") {
2041        return rtl ? "rtl" : "ltr";
2042      } else if (message == "@@bidi_reversed_dir") {
2043        return rtl ? "ltr" : "rtl";
2044      } else if (message == "@@bidi_start_edge") {
2045        return rtl ? "right" : "left";
2046      } else if (message == "@@bidi_end_edge") {
2047        return rtl ? "left" : "right";
2048      }
2049    }
2050
2051    if (!this.warnedMissingKeys.has(message)) {
2052      let error = `Unknown localization message ${message}`;
2053      if (options.cloneScope) {
2054        error = new options.cloneScope.Error(error);
2055      }
2056      Cu.reportError(error);
2057      this.warnedMissingKeys.add(message);
2058    }
2059    return options.defaultValue;
2060  },
2061
2062  // Localize a string, replacing all |__MSG_(.*)__| tokens with the
2063  // matching string from the current locale, as determined by
2064  // |this.selectedLocale|.
2065  //
2066  // This may not be called before calling either |initLocale| or
2067  // |initAllLocales|.
2068  localize(str, locale = this.selectedLocale) {
2069    if (!str) {
2070      return str;
2071    }
2072
2073    return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
2074      return this.localizeMessage(message, [], {
2075        locale,
2076        defaultValue: matched,
2077      });
2078    });
2079  },
2080
2081  // Validates the contents of a locale JSON file, normalizes the
2082  // messages into a Map of message key -> localized string pairs.
2083  addLocale(locale, messages, extension) {
2084    let result = new Map();
2085
2086    let isPlainObject = obj =>
2087      obj &&
2088      typeof obj === "object" &&
2089      ChromeUtils.getClassName(obj) === "Object";
2090
2091    // Chrome does not document the semantics of its localization
2092    // system very well. It handles replacements by pre-processing
2093    // messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their
2094    // replacements. Later, it processes the resulting string for
2095    // |$[0-9]| replacements.
2096    //
2097    // Again, it does not document this, but it accepts any number
2098    // of sequential |$|s, and replaces them with that number minus
2099    // 1. It also accepts |$| followed by any number of sequential
2100    // digits, but refuses to process a localized string which
2101    // provides more than 9 substitutions.
2102    if (!isPlainObject(messages)) {
2103      extension.packagingError(`Invalid locale data for ${locale}`);
2104      return result;
2105    }
2106
2107    for (let key of Object.keys(messages)) {
2108      let msg = messages[key];
2109
2110      if (!isPlainObject(msg) || typeof msg.message != "string") {
2111        extension.packagingError(
2112          `Invalid locale message data for ${locale}, message ${JSON.stringify(
2113            key
2114          )}`
2115        );
2116        continue;
2117      }
2118
2119      // Substitutions are case-insensitive, so normalize all of their names
2120      // to lower-case.
2121      let placeholders = new Map();
2122      if ("placeholders" in msg && isPlainObject(msg.placeholders)) {
2123        for (let key of Object.keys(msg.placeholders)) {
2124          placeholders.set(key.toLowerCase(), msg.placeholders[key]);
2125        }
2126      }
2127
2128      let replacer = (match, name) => {
2129        let replacement = placeholders.get(name.toLowerCase());
2130        if (isPlainObject(replacement) && "content" in replacement) {
2131          return replacement.content;
2132        }
2133        return "";
2134      };
2135
2136      let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer);
2137
2138      // Message names are also case-insensitive, so normalize them to lower-case.
2139      result.set(key.toLowerCase(), value);
2140    }
2141
2142    this.messages.set(locale, result);
2143    return result;
2144  },
2145
2146  get acceptLanguages() {
2147    let result = Services.prefs.getComplexValue(
2148      "intl.accept_languages",
2149      Ci.nsIPrefLocalizedString
2150    ).data;
2151    return result.split(/\s*,\s*/g);
2152  },
2153
2154  get uiLocale() {
2155    return Services.locale.appLocaleAsBCP47;
2156  },
2157};
2158
2159defineLazyGetter(LocaleData.prototype, "availableLocales", function() {
2160  return new Set(
2161    [this.BUILTIN, this.selectedLocale, this.defaultLocale].filter(locale =>
2162      this.messages.has(locale)
2163    )
2164  );
2165});
2166
2167/**
2168 * This is a generic class for managing event listeners.
2169 *
2170 * @example
2171 * new EventManager({
2172 *   context,
2173 *   name: "api.subAPI",
2174 *   register:  fire => {
2175 *     let listener = (...) => {
2176 *       // Fire any listeners registered with addListener.
2177 *       fire.async(arg1, arg2);
2178 *     };
2179 *     // Register the listener.
2180 *     SomehowRegisterListener(listener);
2181 *     return () => {
2182 *       // Return a way to unregister the listener.
2183 *       SomehowUnregisterListener(listener);
2184 *     };
2185 *   }
2186 * }).api()
2187 *
2188 * The result is an object with addListener, removeListener, and
2189 * hasListener methods. `context` is an add-on scope (either an
2190 * ExtensionContext in the chrome process or ExtensionContext in a
2191 * content process).
2192 */
2193class EventManager {
2194  /*
2195   * A persistent event must provide module and name.  Additionally the
2196   * module must implement primeListeners in the ExtensionAPI class.
2197   *
2198   * A startup blocking event must also add the startupBlocking flag in
2199   * ext-toolkit.json or ext-browser.json.
2200   *
2201   * Listeners synchronously added from a background extension context
2202   * will be persisted, for a persistent background script only the
2203   * "startup blocking" events will be persisted.
2204   *
2205   * EventManager instances created in a child process can't persist any listener.
2206   *
2207   * @param {object} params
2208   *        Parameters that control this EventManager.
2209   * @param {BaseContext} params.context
2210   *        An object representing the extension instance using this event.
2211   * @param {string} params.module
2212   *        The API module name, required for persistent events.
2213   * @param {string} params.event
2214   *        The API event name, required for persistent events.
2215   * @param {ExtensionAPI} params.extensionApi
2216   *        The API intance.  If the API uses the ExtensionAPIPersistent class, some simplification is
2217   *        possible by passing the api (self or this) and the internal register function will be used.
2218   * @param {string} [params.name]
2219   *        A name used only for debugging.  If not provided, name is built from module and event.
2220   * @param {functon} params.register
2221   *        A function called whenever a new listener is added.
2222   * @param {boolean} [params.inputHandling=false]
2223   *        If true, the "handling user input" flag is set while handlers
2224   *        for this event are executing.
2225   */
2226  constructor(params) {
2227    let {
2228      context,
2229      module,
2230      event,
2231      name,
2232      register,
2233      extensionApi,
2234      inputHandling = false,
2235    } = params;
2236    this.context = context;
2237    this.module = module;
2238    this.event = event;
2239    this.name = name;
2240    this.register = register;
2241    this.inputHandling = inputHandling;
2242    if (!name) {
2243      this.name = `${module}.${event}`;
2244    }
2245
2246    if (!this.register && extensionApi instanceof ExtensionAPIPersistent) {
2247      this.register = fire => {
2248        return extensionApi.registerEventListener({ context, event, fire });
2249      };
2250    }
2251    if (!this.register) {
2252      throw new Error(
2253        `EventManager requires register method for ${this.name}.`
2254      );
2255    }
2256
2257    this.canPersistEvents =
2258      module &&
2259      event &&
2260      ["background", "background_worker"].includes(this.context.viewType) &&
2261      this.context.envType == "addon_parent";
2262
2263    if (this.canPersistEvents) {
2264      let { extension } = context;
2265      if (extension.persistentBackground) {
2266        // Persistent backgrounds will only persist startup blocking APIs.
2267        let api_module = extension.apiManager.getModule(this.module);
2268        if (!api_module?.startupBlocking) {
2269          this.canPersistEvents = false;
2270        }
2271      } else {
2272        // Event pages will persist all APIs that implement primeListener.
2273        // The api is already loaded so this does not have performance effect.
2274        let api = extension.apiManager.getAPI(
2275          this.module,
2276          extension,
2277          "addon_parent"
2278        );
2279
2280        // If the api doesn't implement primeListener we do not persist the events.
2281        if (!api?.primeListener) {
2282          this.canPersistEvents = false;
2283        }
2284      }
2285    }
2286
2287    this.unregister = new Map();
2288    this.remove = new Map();
2289  }
2290
2291  /*
2292   * Information about listeners to persistent events is associated with
2293   * the extension to which they belong.  Any extension thas has such
2294   * listeners has a property called `persistentListeners` that is a
2295   * 3-level Map.  The first 2 keys are the module name (e.g., webRequest)
2296   * and the name of the event within the module (e.g., onBeforeRequest).
2297   * The third level of the map is used to track multiple listeners for
2298   * the same event, these listeners are distinguished by the extra arguments
2299   * passed to addListener().  For quick lookups, the key to the third Map
2300   * is the result of calling uneval() on the array of extra arguments.
2301   *
2302   * The value stored in the Map is a plain object with a property called
2303   * `params` that is the original (ie, not uneval()ed) extra arguments to
2304   * addListener().  For a primed listener (i.e., the stub listener created
2305   * during browser startup before the extension background page is started,
2306   * the object also has a `primed` property that holds the things needed
2307   * to handle events during startup and eventually connect the listener
2308   * with a callback registered from the extension.
2309   *
2310   * @param {Extension} extension
2311   * @returns {boolean} True if the extension had any persistent listeners.
2312   */
2313  static _initPersistentListeners(extension) {
2314    if (extension.persistentListeners) {
2315      return !!extension.persistentListeners.size;
2316    }
2317
2318    let listeners = new DefaultMap(() => new DefaultMap(() => new Map()));
2319    extension.persistentListeners = listeners;
2320
2321    let persistentListeners = extension.startupData?.persistentListeners;
2322    if (!persistentListeners) {
2323      return false;
2324    }
2325
2326    let found = false;
2327    for (let [module, entry] of Object.entries(persistentListeners)) {
2328      for (let [event, paramlists] of Object.entries(entry)) {
2329        for (let paramlist of paramlists) {
2330          let key = uneval(paramlist);
2331          listeners
2332            .get(module)
2333            .get(event)
2334            .set(key, { params: paramlist });
2335          found = true;
2336        }
2337      }
2338    }
2339    return found;
2340  }
2341
2342  // Extract just the information needed at startup for all persistent
2343  // listeners, and arrange for it to be saved.  This should be called
2344  // whenever the set of persistent listeners for an extension changes.
2345  static _writePersistentListeners(extension) {
2346    let startupListeners = {};
2347    for (let [module, moduleEntry] of extension.persistentListeners) {
2348      startupListeners[module] = {};
2349      for (let [event, eventEntry] of moduleEntry) {
2350        startupListeners[module][event] = Array.from(
2351          eventEntry.values(),
2352          listener => listener.params
2353        );
2354      }
2355    }
2356
2357    extension.startupData.persistentListeners = startupListeners;
2358    extension.saveStartupData();
2359  }
2360
2361  // Set up "primed" event listeners for any saved event listeners
2362  // in an extension's startup data.
2363  // This function is only called during browser startup, it stores details
2364  // about all primed listeners in the extension's persistentListeners Map.
2365  static primeListeners(extension, isInStartup = false) {
2366    if (!EventManager._initPersistentListeners(extension)) {
2367      return;
2368    }
2369
2370    for (let [module, moduleEntry] of extension.persistentListeners) {
2371      // If we're in startup, we only want to continue attempting to prime a
2372      // subset of events that should be startup blocking.
2373      if (isInStartup) {
2374        let api_module = extension.apiManager.getModule(module);
2375        if (!api_module.startupBlocking) {
2376          continue;
2377        }
2378      }
2379
2380      let api = extension.apiManager.getAPI(module, extension, "addon_parent");
2381
2382      // If an extension is upgraded and a permission, such as webRequest, is
2383      // removed, we will have been called but the API is no longer available.
2384      if (!api?.primeListener) {
2385        // The runtime module no longer implements primed listeners, drop them.
2386        extension.persistentListeners.delete(module);
2387        EventManager._writePersistentListeners(extension);
2388        continue;
2389      }
2390      for (let [event, listeners] of moduleEntry) {
2391        for (let [key, listener] of listeners) {
2392          let primed = { pendingEvents: [] };
2393
2394          let fireEvent = (...args) =>
2395            new Promise((resolve, reject) => {
2396              if (!listener.primed) {
2397                reject(new Error("primed listener not re-registered"));
2398                return;
2399              }
2400              primed.pendingEvents.push({ args, resolve, reject });
2401              extension.emit("background-script-event");
2402            });
2403
2404          let fire = {
2405            wakeup: () => extension.wakeupBackground(),
2406            sync: fireEvent,
2407            async: fireEvent,
2408          };
2409
2410          try {
2411            let handler = api.primeListener(
2412              event,
2413              fire,
2414              listener.params,
2415              isInStartup
2416            );
2417            if (handler) {
2418              listener.primed = primed;
2419              Object.assign(primed, handler);
2420            }
2421          } catch (e) {
2422            Cu.reportError(
2423              `Error priming listener ${module}.${event}: ${e} :: ${e.stack}`
2424            );
2425            // Force this listener to be cleared.
2426            listener.error = true;
2427          }
2428          // If an attempt to prime a listener failed, ensure it is cleared now.
2429          // If a module is a startup blocking module, not all listeners may
2430          // get primed during early startup.  For that reason, we don't clear
2431          // persisted listeners during early startup.  At the end of background
2432          // execution any listeners that were not renewed will be cleared.
2433          if (listener.error || (!isInStartup && !listener.primed)) {
2434            EventManager.clearPersistentListener(extension, module, event, key);
2435          }
2436        }
2437      }
2438    }
2439  }
2440
2441  /**
2442   * This is called as a result of background script startup-finished and shutdown.
2443   *
2444   * After startup, it removes any remaining primed listeners.  These exist if the
2445   * listener was not renewed during startup.  In this case the persisted listener
2446   * data is also removed.
2447   *
2448   * During shutdown, care should be taken to set clearPersistent to false.
2449   * persisted listener data should NOT be cleared during shutdown.
2450   *
2451   * @param {Extension} extension
2452   * @param {boolean} clearPersistent whether the persisted listener data should be cleared.
2453   */
2454  static clearPrimedListeners(extension, clearPersistent = true) {
2455    if (!extension.persistentListeners) {
2456      return;
2457    }
2458
2459    for (let [module, moduleEntry] of extension.persistentListeners) {
2460      for (let [event, listeners] of moduleEntry) {
2461        for (let [key, listener] of listeners) {
2462          let { primed } = listener;
2463          // When a primed listener is renewed, primed is set to null
2464          // When a new listener has beed added, primed is undefined.
2465          // In both cases, we do not want to clear the persisted listener data.
2466          if (!primed) {
2467            continue;
2468          }
2469
2470          // When a primed listener was not renewed, primed will still be truthy.
2471          // These need to be cleared on shutdown (important for event pages), but
2472          // we only clear the persisted listener data after the startup of a background.
2473          // Release any pending events and unregister the primed handler.
2474          listener.primed = null;
2475
2476          for (let evt of primed.pendingEvents) {
2477            evt.reject(new Error("listener not re-registered"));
2478          }
2479          primed.unregister();
2480
2481          // Clear any persisted events that were not renewed, should typically
2482          // only be done at the end of the background page load.
2483          if (clearPersistent) {
2484            EventManager.clearPersistentListener(extension, module, event, key);
2485          }
2486        }
2487      }
2488    }
2489  }
2490
2491  // Record the fact that there is a listener for the given event in
2492  // the given extension.  `args` is an Array containing any extra
2493  // arguments that were passed to addListener().
2494  static savePersistentListener(extension, module, event, args = []) {
2495    EventManager._initPersistentListeners(extension);
2496    let key = uneval(args);
2497    extension.persistentListeners
2498      .get(module)
2499      .get(event)
2500      .set(key, { params: args });
2501    EventManager._writePersistentListeners(extension);
2502  }
2503
2504  // Remove the record for the given event listener from the extension's
2505  // startup data.  `key` must be a string, the result of calling uneval()
2506  // on the array of extra arguments originally passed to addListener().
2507  static clearPersistentListener(extension, module, event, key = uneval([])) {
2508    let listeners = extension.persistentListeners.get(module).get(event);
2509    listeners.delete(key);
2510
2511    if (listeners.size == 0) {
2512      let moduleEntry = extension.persistentListeners.get(module);
2513      moduleEntry.delete(event);
2514      if (moduleEntry.size == 0) {
2515        extension.persistentListeners.delete(module);
2516      }
2517    }
2518
2519    EventManager._writePersistentListeners(extension);
2520  }
2521
2522  addListener(callback, ...args) {
2523    if (this.unregister.has(callback)) {
2524      return;
2525    }
2526    this.context.logActivity("api_call", `${this.name}.addListener`, { args });
2527
2528    let shouldFire = () => {
2529      if (this.context.unloaded) {
2530        dump(`${this.name} event fired after context unloaded.\n`);
2531      } else if (!this.context.active) {
2532        dump(`${this.name} event fired while context is inactive.\n`);
2533      } else if (this.unregister.has(callback)) {
2534        return true;
2535      }
2536      return false;
2537    };
2538
2539    let fire = {
2540      sync: (...args) => {
2541        if (shouldFire()) {
2542          let result = this.context.applySafe(callback, args);
2543          this.context.logActivity("api_event", this.name, { args, result });
2544          return result;
2545        }
2546      },
2547      async: (...args) => {
2548        return Promise.resolve().then(() => {
2549          if (shouldFire()) {
2550            let result = this.context.applySafe(callback, args);
2551            this.context.logActivity("api_event", this.name, { args, result });
2552            return result;
2553          }
2554        });
2555      },
2556      raw: (...args) => {
2557        if (!shouldFire()) {
2558          throw new Error("Called raw() on unloaded/inactive context");
2559        }
2560        let result = Reflect.apply(callback, null, args);
2561        this.context.logActivity("api_event", this.name, { args, result });
2562        return result;
2563      },
2564      asyncWithoutClone: (...args) => {
2565        return Promise.resolve().then(() => {
2566          if (shouldFire()) {
2567            let result = this.context.applySafeWithoutClone(callback, args);
2568            this.context.logActivity("api_event", this.name, { args, result });
2569            return result;
2570          }
2571        });
2572      },
2573    };
2574
2575    let { extension } = this.context;
2576    let { module, event } = this;
2577
2578    let unregister = null;
2579    let recordStartupData = false;
2580
2581    // If this is a persistent event, check for a listener that was already
2582    // created during startup.  If there is one, use it and don't create a
2583    // new one.
2584    if (this.canPersistEvents) {
2585      // Once a background is started, listenerPromises is set to null. At
2586      // that point, we stop recording startup data.
2587      recordStartupData = !!this.context.listenerPromises;
2588
2589      let key = uneval(args);
2590      EventManager._initPersistentListeners(extension);
2591      let listener = extension.persistentListeners
2592        .get(module)
2593        .get(event)
2594        .get(key);
2595
2596      if (listener) {
2597        // During startup only a subset of persisted listeners are primed.  As
2598        // well, each API determines whether to prime a specific listener.
2599        let { primed } = listener;
2600        if (primed) {
2601          listener.primed = null;
2602
2603          primed.convert(fire, this.context);
2604          unregister = primed.unregister;
2605
2606          for (let evt of primed.pendingEvents) {
2607            evt.resolve(fire.async(...evt.args));
2608          }
2609        }
2610
2611        recordStartupData = false;
2612        this.remove.set(callback, () => {
2613          EventManager.clearPersistentListener(
2614            extension,
2615            module,
2616            event,
2617            uneval(args)
2618          );
2619        });
2620      }
2621    }
2622
2623    if (!unregister) {
2624      unregister = this.register(fire, ...args);
2625    }
2626
2627    this.unregister.set(callback, unregister);
2628    this.context.callOnClose(this);
2629
2630    // If this is a new listener for a persistent event, record
2631    // the details for subsequent startups.
2632    if (recordStartupData) {
2633      EventManager.savePersistentListener(extension, module, event, args);
2634      this.remove.set(callback, () => {
2635        EventManager.clearPersistentListener(
2636          extension,
2637          module,
2638          event,
2639          uneval(args)
2640        );
2641      });
2642    }
2643  }
2644
2645  removeListener(callback, clearPersistentListener = true) {
2646    if (!this.unregister.has(callback)) {
2647      return;
2648    }
2649    this.context.logActivity("api_call", `${this.name}.removeListener`, {
2650      args: [],
2651    });
2652
2653    let unregister = this.unregister.get(callback);
2654    this.unregister.delete(callback);
2655    try {
2656      unregister();
2657    } catch (e) {
2658      Cu.reportError(e);
2659    }
2660
2661    if (clearPersistentListener && this.remove.has(callback)) {
2662      let cleanup = this.remove.get(callback);
2663      this.remove.delete(callback);
2664      cleanup();
2665    }
2666
2667    if (this.unregister.size == 0) {
2668      this.context.forgetOnClose(this);
2669    }
2670  }
2671
2672  hasListener(callback) {
2673    return this.unregister.has(callback);
2674  }
2675
2676  revoke() {
2677    for (let callback of this.unregister.keys()) {
2678      this.removeListener(callback, false);
2679    }
2680  }
2681
2682  close() {
2683    this.revoke();
2684  }
2685
2686  api() {
2687    return {
2688      addListener: (...args) => this.addListener(...args),
2689      removeListener: (...args) => this.removeListener(...args),
2690      hasListener: (...args) => this.hasListener(...args),
2691      setUserInput: this.inputHandling,
2692      [Schemas.REVOKE]: () => this.revoke(),
2693    };
2694  }
2695}
2696
2697// Simple API for event listeners where events never fire.
2698function ignoreEvent(context, name) {
2699  return {
2700    addListener: function(callback) {
2701      let id = context.extension.id;
2702      let frame = Components.stack.caller;
2703      let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`;
2704      let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
2705        Ci.nsIScriptError
2706      );
2707      scriptError.init(
2708        msg,
2709        frame.filename,
2710        null,
2711        frame.lineNumber,
2712        frame.columnNumber,
2713        Ci.nsIScriptError.warningFlag,
2714        "content javascript"
2715      );
2716      Services.console.logMessage(scriptError);
2717    },
2718    removeListener: function(callback) {},
2719    hasListener: function(callback) {},
2720  };
2721}
2722
2723const stylesheetMap = new DefaultMap(url => {
2724  let uri = Services.io.newURI(url);
2725  return styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET);
2726});
2727
2728ExtensionCommon = {
2729  BaseContext,
2730  CanOfAPIs,
2731  EventManager,
2732  ExtensionAPI,
2733  EventEmitter,
2734  LocalAPIImplementation,
2735  LocaleData,
2736  NoCloneSpreadArgs,
2737  SchemaAPIInterface,
2738  SchemaAPIManager,
2739  SpreadArgs,
2740  checkLoadURL,
2741  defineLazyGetter,
2742  getConsole,
2743  ignoreEvent,
2744  instanceOf,
2745  makeWidgetId,
2746  normalizeTime,
2747  runSafeSyncWithoutClone,
2748  stylesheetMap,
2749  withHandlingUserInput,
2750
2751  MultiAPIManager,
2752  LazyAPIManager,
2753};
2754