Bug 1871127 - Add tsconfig, basic types, and fix or ignore remaining type errors...
[gecko.git] / toolkit / components / extensions / ExtensionCommon.sys.mjs
blobbb92a34ae73691404d592cfc9ab41312dc5eee8e
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /**
8  * This module contains utilities and base classes for logic which is
9  * common between the parent and child process, and in particular
10  * between ExtensionParent.jsm and ExtensionChild.jsm.
11  */
13 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
15 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
17 const lazy = {};
19 ChromeUtils.defineESModuleGetters(lazy, {
20   ConsoleAPI: "resource://gre/modules/Console.sys.mjs",
21   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
22   SchemaRoot: "resource://gre/modules/Schemas.sys.mjs",
23   Schemas: "resource://gre/modules/Schemas.sys.mjs",
24 });
26 XPCOMUtils.defineLazyServiceGetter(
27   lazy,
28   "styleSheetService",
29   "@mozilla.org/content/style-sheet-service;1",
30   "nsIStyleSheetService"
33 const ScriptError = Components.Constructor(
34   "@mozilla.org/scripterror;1",
35   "nsIScriptError",
36   "initWithWindowID"
39 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
41 var {
42   DefaultMap,
43   DefaultWeakMap,
44   ExtensionError,
45   filterStack,
46   getInnerWindowID,
47   getUniqueId,
48 } = ExtensionUtils;
50 function getConsole() {
51   return new lazy.ConsoleAPI({
52     maxLogLevelPref: "extensions.webextensions.log.level",
53     prefix: "WebExtensions",
54   });
57 // Run a function and report exceptions.
58 function runSafeSyncWithoutClone(f, ...args) {
59   try {
60     return f(...args);
61   } catch (e) {
62     // This method is called with `this` unbound and it doesn't have
63     // access to a BaseContext instance and so we can't check if `e`
64     // is an instance of the extension context's Error constructor
65     // (like we do in BaseContext applySafeWithoutClone method).
66     dump(
67       `Extension error: ${e} ${e?.fileName} ${
68         e?.lineNumber
69       }\n[[Exception stack\n${
70         e?.stack ? filterStack(e) : undefined
71       }Current stack\n${filterStack(Error())}]]\n`
72     );
73     Cu.reportError(e);
74   }
77 // Return true if the given value is an instance of the given
78 // native type.
79 function instanceOf(value, type) {
80   return (
81     value &&
82     typeof value === "object" &&
83     ChromeUtils.getClassName(value) === type
84   );
87 /**
88  * Convert any of several different representations of a date/time to a Date object.
89  * Accepts several formats:
90  * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
91  * either a number or a string.
92  *
93  * @param {Date|string|number} date
94  *      The date to convert.
95  * @returns {Date}
96  *      A Date object
97  */
98 function normalizeTime(date) {
99   // Of all the formats we accept the "number of milliseconds since the epoch as a string"
100   // is an outlier, everything else can just be passed directly to the Date constructor.
101   return new Date(
102     typeof date == "string" && /^\d+$/.test(date) ? parseInt(date, 10) : date
103   );
106 function withHandlingUserInput(window, callable) {
107   let handle = window.windowUtils.setHandlingUserInput(true);
108   try {
109     return callable();
110   } finally {
111     handle.destruct();
112   }
116  * Defines a lazy getter for the given property on the given object. The
117  * first time the property is accessed, the return value of the getter
118  * is defined on the current `this` object with the given property name.
119  * Importantly, this means that a lazy getter defined on an object
120  * prototype will be invoked separately for each object instance that
121  * it's accessed on.
123  * Note: for better type inference, prefer redefineGetter() below.
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 {callback} getter
130  *        The function to call in order to generate the final property
131  *        value.
132  */
133 function defineLazyGetter(object, prop, getter) {
134   Object.defineProperty(object, prop, {
135     enumerable: true,
136     configurable: true,
137     get() {
138       return redefineGetter(this, prop, getter.call(this), true);
139     },
140     set(value) {
141       redefineGetter(this, prop, value, true);
142     },
143   });
147  * A more type-inference friendly version of defineLazyGetter() above.
148  * Call it from a real getter (and setter) for your class or object.
149  * On first run, it will redefine the property with the final value.
151  * @template Value
152  * @param {object} object
153  * @param {string | symbol} key
154  * @param {Value} value
155  * @returns {Value}
156  */
157 function redefineGetter(object, key, value, writable = false) {
158   Object.defineProperty(object, key, {
159     enumerable: true,
160     configurable: true,
161     writable,
162     value,
163   });
164   return value;
167 function checkLoadURI(uri, principal, options) {
168   let ssm = Services.scriptSecurityManager;
170   let flags = ssm.STANDARD;
171   if (!options.allowScript) {
172     flags |= ssm.DISALLOW_SCRIPT;
173   }
174   if (!options.allowInheritsPrincipal) {
175     flags |= ssm.DISALLOW_INHERIT_PRINCIPAL;
176   }
177   if (options.dontReportErrors) {
178     flags |= ssm.DONT_REPORT_ERRORS;
179   }
181   try {
182     ssm.checkLoadURIWithPrincipal(principal, uri, flags);
183   } catch (e) {
184     return false;
185   }
186   return true;
189 function checkLoadURL(url, principal, options) {
190   try {
191     return checkLoadURI(Services.io.newURI(url), principal, options);
192   } catch (e) {
193     return false; // newURI threw.
194   }
197 function makeWidgetId(id) {
198   id = id.toLowerCase();
199   // FIXME: This allows for collisions.
200   return id.replace(/[^a-z0-9_-]/g, "_");
204  * A sentinel class to indicate that an array of values should be
205  * treated as an array when used as a promise resolution value, but as a
206  * spread expression (...args) when passed to a callback.
207  */
208 class SpreadArgs extends Array {
209   constructor(args) {
210     super();
211     this.push(...args);
212   }
216  * Like SpreadArgs, but also indicates that the array values already
217  * belong to the target compartment, and should not be cloned before
218  * being passed.
220  * The `unwrappedValues` property contains an Array object which belongs
221  * to the target compartment, and contains the same unwrapped values
222  * passed the NoCloneSpreadArgs constructor.
223  */
224 class NoCloneSpreadArgs {
225   constructor(args) {
226     this.unwrappedValues = args;
227   }
229   [Symbol.iterator]() {
230     return this.unwrappedValues[Symbol.iterator]();
231   }
234 const LISTENERS = Symbol("listeners");
235 const ONCE_MAP = Symbol("onceMap");
237 class EventEmitter {
238   constructor() {
239     this[LISTENERS] = new Map();
240     this[ONCE_MAP] = new WeakMap();
241   }
243   /**
244    * Checks whether there is some listener for the given event.
245    *
246    * @param {string} event
247    *       The name of the event to listen for.
248    * @returns {boolean}
249    */
250   has(event) {
251     return this[LISTENERS].has(event);
252   }
254   /**
255    * Adds the given function as a listener for the given event.
256    *
257    * The listener function may optionally return a Promise which
258    * resolves when it has completed all operations which event
259    * dispatchers may need to block on.
260    *
261    * @param {string} event
262    *       The name of the event to listen for.
263    * @param {function(string, ...any): any} listener
264    *        The listener to call when events are emitted.
265    */
266   on(event, listener) {
267     let listeners = this[LISTENERS].get(event);
268     if (!listeners) {
269       listeners = new Set();
270       this[LISTENERS].set(event, listeners);
271     }
273     listeners.add(listener);
274   }
276   /**
277    * Removes the given function as a listener for the given event.
278    *
279    * @param {string} event
280    *       The name of the event to stop listening for.
281    * @param {function(string, ...any): any} listener
282    *        The listener function to remove.
283    */
284   off(event, listener) {
285     let set = this[LISTENERS].get(event);
286     if (set) {
287       set.delete(listener);
288       set.delete(this[ONCE_MAP].get(listener));
289       if (!set.size) {
290         this[LISTENERS].delete(event);
291       }
292     }
293   }
295   /**
296    * Adds the given function as a listener for the given event once.
297    *
298    * @param {string} event
299    *       The name of the event to listen for.
300    * @param {function(string, ...any): any} listener
301    *        The listener to call when events are emitted.
302    */
303   once(event, listener) {
304     let wrapper = (event, ...args) => {
305       this.off(event, wrapper);
306       this[ONCE_MAP].delete(listener);
308       return listener(event, ...args);
309     };
310     this[ONCE_MAP].set(listener, wrapper);
312     this.on(event, wrapper);
313   }
315   /**
316    * Triggers all listeners for the given event. If any listeners return
317    * a value, returns a promise which resolves when all returned
318    * promises have resolved. Otherwise, returns undefined.
319    *
320    * @param {string} event
321    *       The name of the event to emit.
322    * @param {any} args
323    *        Arbitrary arguments to pass to the listener functions, after
324    *        the event name.
325    * @returns {Promise?}
326    */
327   emit(event, ...args) {
328     let listeners = this[LISTENERS].get(event);
330     if (listeners) {
331       let promises = [];
333       for (let listener of listeners) {
334         try {
335           let result = listener(event, ...args);
336           if (result !== undefined) {
337             promises.push(result);
338           }
339         } catch (e) {
340           Cu.reportError(e);
341         }
342       }
344       if (promises.length) {
345         return Promise.all(promises);
346       }
347     }
348   }
352  * Base class for WebExtension APIs.  Each API creates a new class
353  * that inherits from this class, the derived class is instantiated
354  * once for each extension that uses the API.
355  */
356 class ExtensionAPI extends EventEmitter {
357   constructor(extension) {
358     super();
360     this.extension = extension;
362     extension.once("shutdown", (what, isAppShutdown) => {
363       if (this.onShutdown) {
364         this.onShutdown(isAppShutdown);
365       }
366       this.extension = null;
367     });
368   }
370   destroy() {}
372   /** @param {string} entryName */
373   onManifestEntry(entryName) {}
375   /** @param {boolean} isAppShutdown */
376   onShutdown(isAppShutdown) {}
378   /** @param {BaseContext} context */
379   getAPI(context) {
380     throw new Error("Not Implemented");
381   }
383   /** @param {string} id */
384   static onDisable(id) {}
386   /** @param {string} id */
387   static onUninstall(id) {}
389   /**
390    * @param {string} id
391    * @param {Record<string, JSONValue>} manifest
392    */
393   static onUpdate(id, manifest) {}
397  * Subclass to add APIs commonly used with persistent events.
398  * If a namespace uses events, it should use this subclass.
400  * this.apiNamespace = class extends ExtensionAPIPersistent {};
401  */
402 class ExtensionAPIPersistent extends ExtensionAPI {
403   /** @type {Record<string, callback>} */
404   PERSISTENT_EVENTS;
406   /**
407    * Check for event entry.
408    *
409    * @param {string} event The event name e.g. onStateChanged
410    * @returns {boolean}
411    */
412   hasEventRegistrar(event) {
413     return (
414       this.PERSISTENT_EVENTS && Object.hasOwn(this.PERSISTENT_EVENTS, event)
415     );
416   }
418   /**
419    * Get the event registration fuction
420    *
421    * @param {string} event The event name e.g. onStateChanged
422    * @returns {Function} register is used to start the listener
423    *                     register returns an object containing
424    *                     a convert and unregister function.
425    */
426   getEventRegistrar(event) {
427     if (this.hasEventRegistrar(event)) {
428       return this.PERSISTENT_EVENTS[event].bind(this);
429     }
430   }
432   /**
433    * Used when instantiating an EventManager instance to register the listener.
434    *
435    * @param {object}      options         Options used for event registration
436    * @param {BaseContext} options.context Extension Context passed when creating an EventManager instance.
437    * @param {string}      options.event   The eAPI vent name.
438    * @param {Function}    options.fire    The function passed to the listener to fire the event.
439    * @param {Array<any>}  params          An optional array of parameters received along with the
440    *                                      addListener request.
441    * @returns {Function}                  The unregister function used in the EventManager.
442    */
443   registerEventListener(options, params) {
444     const apiRegistar = this.getEventRegistrar(options.event);
445     return apiRegistar?.(options, params).unregister;
446   }
448   /**
449    * Used to prime a listener for when the background script is not running.
450    *
451    * @param {string} event The event name e.g. onStateChanged or captiveURL.onChange.
452    * @param {Function} fire The function passed to the listener to fire the event.
453    * @param {Array} params Params passed to the event listener.
454    * @param {boolean} isInStartup unused here but passed for subclass use.
455    * @returns {object} the unregister and convert functions used in the EventManager.
456    */
457   primeListener(event, fire, params, isInStartup) {
458     const apiRegistar = this.getEventRegistrar(event);
459     return apiRegistar?.({ fire, isInStartup }, params);
460   }
464  * This class contains the information we have about an individual
465  * extension.  It is never instantiated directly, instead subclasses
466  * for each type of process extend this class and add members that are
467  * relevant for that process.
469  * @abstract
470  */
471 class BaseContext {
472   /** @type {boolean} */
473   isTopContext;
474   /** @type {string} */
475   viewType;
477   constructor(envType, extension) {
478     this.envType = envType;
479     this.onClose = new Set();
480     this.checkedLastError = false;
481     this._lastError = null;
482     this.contextId = getUniqueId();
483     this.unloaded = false;
484     this.extension = extension;
485     this.manifestVersion = extension.manifestVersion;
486     this.jsonSandbox = null;
487     this.active = true;
488     this.incognito = null;
489     this.messageManager = null;
490     this.contentWindow = null;
491     this.innerWindowID = 0;
493     // These two properties are assigned in ContentScriptContextChild subclass
494     // to keep a copy of the content script sandbox Error and Promise globals
495     // (which are used by the WebExtensions internals) before any extension
496     // content script code had any chance to redefine them.
497     this.cloneScopeError = null;
498     this.cloneScopePromise = null;
499   }
501   get isProxyContextParent() {
502     return false;
503   }
505   get Error() {
506     // Return the copy stored in the context instance (when the context is an instance of
507     // ContentScriptContextChild or the global from extension page window otherwise).
508     return this.cloneScopeError || this.cloneScope.Error;
509   }
511   get Promise() {
512     // Return the copy stored in the context instance (when the context is an instance of
513     // ContentScriptContextChild or the global from extension page window otherwise).
514     return this.cloneScopePromise || this.cloneScope.Promise;
515   }
517   get privateBrowsingAllowed() {
518     return this.extension.privateBrowsingAllowed;
519   }
521   get isBackgroundContext() {
522     if (this.viewType === "background") {
523       if (this.isProxyContextParent) {
524         return !!this.isTopContext; // Set in ExtensionPageContextParent.
525       }
526       const { contentWindow } = this;
527       return !!contentWindow && contentWindow.top === contentWindow;
528     }
529     return this.viewType === "background_worker";
530   }
532   /**
533    * Whether the extension context is using the WebIDL bindings for the
534    * WebExtensions APIs.
535    * To be overridden in subclasses (e.g. WorkerContextChild) and to be
536    * optionally used in ExtensionAPI classes to customize the behavior of the
537    * API when the calls to the extension API are originated from the WebIDL
538    * bindings.
539    */
540   get useWebIDLBindings() {
541     return false;
542   }
544   canAccessWindow(window) {
545     return this.extension.canAccessWindow(window);
546   }
548   canAccessContainer(userContextId) {
549     return this.extension.canAccessContainer(userContextId);
550   }
552   /**
553    * Opens a conduit linked to this context, populating related address fields.
554    * Only available in child contexts with an associated contentWindow.
555    *
556    * @param {object} subject
557    * @param {ConduitAddress} address
558    * @returns {import("ConduitsChild.sys.mjs").PointConduit}
559    * @type {ConduitOpen}
560    */
561   openConduit(subject, address) {
562     let wgc = this.contentWindow.windowGlobalChild;
563     let conduit = wgc.getActor("Conduits").openConduit(subject, {
564       id: subject.id || getUniqueId(),
565       extensionId: this.extension.id,
566       envType: this.envType,
567       ...address,
568     });
569     this.callOnClose(conduit);
570     conduit.setCloseCallback(() => {
571       this.forgetOnClose(conduit);
572     });
573     return conduit;
574   }
576   setContentWindow(contentWindow) {
577     if (!this.canAccessWindow(contentWindow)) {
578       throw new Error(
579         "BaseContext attempted to load when extension is not allowed due to incognito settings."
580       );
581     }
583     this.innerWindowID = getInnerWindowID(contentWindow);
584     this.messageManager = contentWindow.docShell.messageManager;
586     if (this.incognito == null) {
587       this.incognito =
588         lazy.PrivateBrowsingUtils.isContentWindowPrivate(contentWindow);
589     }
591     let wgc = contentWindow.windowGlobalChild;
592     Object.defineProperty(this, "active", {
593       configurable: true,
594       enumerable: true,
595       get: () => wgc.isCurrentGlobal && !wgc.windowContext.isInBFCache,
596     });
597     Object.defineProperty(this, "contentWindow", {
598       configurable: true,
599       enumerable: true,
600       get: () => (this.active ? wgc.browsingContext.window : null),
601     });
602     this.callOnClose({
603       close: () => {
604         // Allow other "close" handlers to use these properties, until the next tick.
605         Promise.resolve().then(() => {
606           Object.defineProperty(this, "contentWindow", { value: null });
607           Object.defineProperty(this, "active", { value: false });
608           wgc = null;
609         });
610       },
611     });
612   }
614   // All child contexts must implement logActivity.  This is handled if the child
615   // context subclasses ExtensionBaseContextChild.  ProxyContextParent overrides
616   // this with a noop for parent contexts.
617   logActivity(type, name, data) {
618     throw new Error(`Not implemented for ${this.envType}`);
619   }
621   /** @type {object} */
622   get cloneScope() {
623     throw new Error("Not implemented");
624   }
626   /** @type {nsIPrincipal} */
627   get principal() {
628     throw new Error("Not implemented");
629   }
631   runSafe(callback, ...args) {
632     return this.applySafe(callback, args);
633   }
635   runSafeWithoutClone(callback, ...args) {
636     return this.applySafeWithoutClone(callback, args);
637   }
639   applySafe(callback, args, caller) {
640     if (this.unloaded) {
641       Cu.reportError("context.runSafe called after context unloaded", caller);
642     } else if (!this.active) {
643       Cu.reportError(
644         "context.runSafe called while context is inactive",
645         caller
646       );
647     } else {
648       try {
649         let { cloneScope } = this;
650         args = args.map(arg => Cu.cloneInto(arg, cloneScope));
651       } catch (e) {
652         Cu.reportError(e);
653         dump(
654           `runSafe failure: cloning into ${
655             this.cloneScope
656           }: ${e}\n\n${filterStack(Error())}`
657         );
658       }
660       return this.applySafeWithoutClone(callback, args, caller);
661     }
662   }
664   applySafeWithoutClone(callback, args, caller) {
665     if (this.unloaded) {
666       Cu.reportError(
667         "context.runSafeWithoutClone called after context unloaded",
668         caller
669       );
670     } else if (!this.active) {
671       Cu.reportError(
672         "context.runSafeWithoutClone called while context is inactive",
673         caller
674       );
675     } else {
676       try {
677         return Reflect.apply(callback, null, args);
678       } catch (e) {
679         // An extension listener may as well be throwing an object that isn't
680         // an instance of Error, in that case we have to use fallbacks for the
681         // error message, fileName, lineNumber and columnNumber properties.
682         const isError = e instanceof this.Error;
683         let message;
684         let fileName;
685         let lineNumber;
686         let columnNumber;
688         if (isError) {
689           message = `${e.name}: ${e.message}`;
690           lineNumber = e.lineNumber;
691           columnNumber = e.columnNumber;
692           fileName = e.fileName;
693         } else {
694           message = `uncaught exception: ${e}`;
696           try {
697             // TODO(Bug 1810582): the following fallback logic may go away once
698             // we introduced a better way to capture and log the exception in
699             // the right window and in all cases (included when the extension
700             // code is raising undefined or an object that isn't an instance of
701             // the Error constructor).
702             //
703             // Fallbacks for the error location:
704             // - the callback location if it is registered directly from the
705             //   extension code (and not wrapped by the child/ext-APINAMe.js
706             //   implementation, like e.g. browser.storage, browser.devtools.network
707             //   are doing and browser.menus).
708             // - if the location of the extension callback is not directly
709             //   available (e.g. browser.storage onChanged events, and similarly
710             //   for browser.devtools.network and browser.menus events):
711             //   - the extension page url if the context is an extension page
712             //   - the extension base url if the context is a content script
713             const cbLoc = Cu.getFunctionSourceLocation(callback);
714             fileName = cbLoc.filename;
715             lineNumber = cbLoc.lineNumber ?? lineNumber;
717             const extBaseUrl = this.extension.baseURI.resolve("/");
718             if (fileName.startsWith(extBaseUrl)) {
719               fileName = cbLoc.filename;
720               lineNumber = cbLoc.lineNumber ?? lineNumber;
721             } else {
722               fileName = this.contentWindow?.location?.href;
723               if (!fileName || !fileName.startsWith(extBaseUrl)) {
724                 fileName = extBaseUrl;
725               }
726             }
727           } catch {
728             // Ignore errors on retrieving the callback source location.
729           }
730         }
732         dump(
733           `Extension error: ${message} ${fileName} ${lineNumber}\n[[Exception stack\n${
734             isError ? filterStack(e) : undefined
735           }Current stack\n${filterStack(Error())}]]\n`
736         );
738         // If the error is coming from an extension context associated
739         // to a window (e.g. an extension page or extension content script).
740         //
741         // TODO(Bug 1810574): for the background service worker we will need to do
742         // something similar, but not tied to the innerWindowID because there
743         // wouldn't be one set for extension contexts related to the
744         // background service worker.
745         //
746         // TODO(Bug 1810582): change the error associated to the innerWindowID to also
747         // include a full stack from the original error.
748         if (!this.isProxyContextParent && this.contentWindow) {
749           Services.console.logMessage(
750             new ScriptError(
751               message,
752               fileName,
753               null,
754               lineNumber,
755               columnNumber,
756               Ci.nsIScriptError.errorFlag,
757               "content javascript",
758               this.innerWindowID
759             )
760           );
761         }
762         // Also report the original error object (because it also includes
763         // the full error stack).
764         Cu.reportError(e);
765       }
766     }
767   }
769   checkLoadURL(url, options = {}) {
770     // As an optimization, f the URL starts with the extension's base URL,
771     // don't do any further checks. It's always allowed to load it.
772     if (url.startsWith(this.extension.baseURL)) {
773       return true;
774     }
776     return checkLoadURL(url, this.principal, options);
777   }
779   /**
780    * Safely call JSON.stringify() on an object that comes from an
781    * extension.
782    *
783    * @param {[any, callback?, number?]} args for JSON.stringify()
784    * @returns {string} The stringified representation of obj
785    */
786   jsonStringify(...args) {
787     if (!this.jsonSandbox) {
788       this.jsonSandbox = Cu.Sandbox(this.principal, {
789         sameZoneAs: this.cloneScope,
790         wantXrays: false,
791       });
792     }
794     return Cu.waiveXrays(this.jsonSandbox.JSON).stringify(...args);
795   }
797   callOnClose(obj) {
798     this.onClose.add(obj);
799   }
801   forgetOnClose(obj) {
802     this.onClose.delete(obj);
803   }
805   get lastError() {
806     this.checkedLastError = true;
807     return this._lastError;
808   }
810   set lastError(val) {
811     this.checkedLastError = false;
812     this._lastError = val;
813   }
815   /**
816    * Normalizes the given error object for use by the target scope. If
817    * the target is an error object which belongs to that scope, it is
818    * returned as-is. If it is an ordinary object with a `message`
819    * property, it is converted into an error belonging to the target
820    * scope. If it is an Error object which does *not* belong to the
821    * clone scope, it is reported, and converted to an unexpected
822    * exception error.
823    *
824    * @param {Error|object} error
825    * @param {SavedFrame?} [caller]
826    * @returns {Error}
827    */
828   normalizeError(error, caller) {
829     if (error instanceof this.Error) {
830       return error;
831     }
832     let message, fileName;
833     if (error && typeof error === "object") {
834       const isPlain = ChromeUtils.getClassName(error) === "Object";
835       if (isPlain && error.mozWebExtLocation) {
836         caller = error.mozWebExtLocation;
837       }
838       if (isPlain && caller && (error.mozWebExtLocation || !error.fileName)) {
839         caller = Cu.cloneInto(caller, this.cloneScope);
840         return ChromeUtils.createError(error.message, caller);
841       }
843       if (
844         isPlain ||
845         error instanceof ExtensionError ||
846         this.principal.subsumes(Cu.getObjectPrincipal(error))
847       ) {
848         message = error.message;
849         fileName = error.fileName;
850       }
851     }
853     if (!message) {
854       Cu.reportError(error);
855       message = "An unexpected error occurred";
856     }
857     return new this.Error(message, fileName);
858   }
860   /**
861    * Sets the value of `.lastError` to `error`, calls the given
862    * callback, and reports an error if the value has not been checked
863    * when the callback returns.
864    *
865    * @param {object} error An object with a `message` property. May
866    *     optionally be an `Error` object belonging to the target scope.
867    * @param {SavedFrame?} caller
868    *        The optional caller frame which triggered this callback, to be used
869    *        in error reporting.
870    * @param {Function} callback The callback to call.
871    * @returns {*} The return value of callback.
872    */
873   withLastError(error, caller, callback) {
874     this.lastError = this.normalizeError(error);
875     try {
876       return callback();
877     } finally {
878       if (!this.checkedLastError) {
879         Cu.reportError(`Unchecked lastError value: ${this.lastError}`, caller);
880       }
881       this.lastError = null;
882     }
883   }
885   /**
886    * Captures the most recent stack frame which belongs to the extension.
887    *
888    * @returns {SavedFrame?}
889    */
890   getCaller() {
891     return ChromeUtils.getCallerLocation(this.principal);
892   }
894   /**
895    * Wraps the given promise so it can be safely returned to extension
896    * code in this context.
897    *
898    * If `callback` is provided, however, it is used as a completion
899    * function for the promise, and no promise is returned. In this case,
900    * the callback is called when the promise resolves or rejects. In the
901    * latter case, `lastError` is set to the rejection value, and the
902    * callback function must check `browser.runtime.lastError` or
903    * `extension.runtime.lastError` in order to prevent it being reported
904    * to the console.
905    *
906    * @param {Promise} promise The promise with which to wrap the
907    *     callback. May resolve to a `SpreadArgs` instance, in which case
908    *     each element will be used as a separate argument.
909    *
910    *     Unless the promise object belongs to the cloneScope global, its
911    *     resolution value is cloned into cloneScope prior to calling the
912    *     `callback` function or resolving the wrapped promise.
913    *
914    * @param {Function} [callback] The callback function to wrap
915    *
916    * @returns {Promise|undefined} If callback is null, a promise object
917    *     belonging to the target scope. Otherwise, undefined.
918    */
919   wrapPromise(promise, callback = null) {
920     let caller = this.getCaller();
921     let applySafe = this.applySafe.bind(this);
922     if (Cu.getGlobalForObject(promise) === this.cloneScope) {
923       applySafe = this.applySafeWithoutClone.bind(this);
924     }
926     if (callback) {
927       promise.then(
928         args => {
929           if (this.unloaded) {
930             Cu.reportError(`Promise resolved after context unloaded\n`, caller);
931           } else if (!this.active) {
932             Cu.reportError(
933               `Promise resolved while context is inactive\n`,
934               caller
935             );
936           } else if (args instanceof NoCloneSpreadArgs) {
937             this.applySafeWithoutClone(callback, args.unwrappedValues, caller);
938           } else if (args instanceof SpreadArgs) {
939             applySafe(callback, args, caller);
940           } else {
941             applySafe(callback, [args], caller);
942           }
943         },
944         error => {
945           this.withLastError(error, caller, () => {
946             if (this.unloaded) {
947               Cu.reportError(
948                 `Promise rejected after context unloaded\n`,
949                 caller
950               );
951             } else if (!this.active) {
952               Cu.reportError(
953                 `Promise rejected while context is inactive\n`,
954                 caller
955               );
956             } else {
957               this.applySafeWithoutClone(callback, [], caller);
958             }
959           });
960         }
961       );
962     } else {
963       return new this.Promise((resolve, reject) => {
964         promise.then(
965           value => {
966             if (this.unloaded) {
967               Cu.reportError(
968                 `Promise resolved after context unloaded\n`,
969                 caller
970               );
971             } else if (!this.active) {
972               Cu.reportError(
973                 `Promise resolved while context is inactive\n`,
974                 caller
975               );
976             } else if (value instanceof NoCloneSpreadArgs) {
977               let values = value.unwrappedValues;
978               this.applySafeWithoutClone(
979                 resolve,
980                 values.length == 1 ? [values[0]] : [values],
981                 caller
982               );
983             } else if (value instanceof SpreadArgs) {
984               applySafe(resolve, value.length == 1 ? value : [value], caller);
985             } else {
986               applySafe(resolve, [value], caller);
987             }
988           },
989           value => {
990             if (this.unloaded) {
991               Cu.reportError(
992                 `Promise rejected after context unloaded: ${
993                   value && value.message
994                 }\n`,
995                 caller
996               );
997             } else if (!this.active) {
998               Cu.reportError(
999                 `Promise rejected while context is inactive: ${
1000                   value && value.message
1001                 }\n`,
1002                 caller
1003               );
1004             } else {
1005               this.applySafeWithoutClone(
1006                 reject,
1007                 [this.normalizeError(value, caller)],
1008                 caller
1009               );
1010             }
1011           }
1012         );
1013       });
1014     }
1015   }
1017   unload() {
1018     this.unloaded = true;
1020     for (let obj of this.onClose) {
1021       obj.close();
1022     }
1023     this.onClose.clear();
1024   }
1026   /**
1027    * A simple proxy for unload(), for use with callOnClose().
1028    */
1029   close() {
1030     this.unload();
1031   }
1035  * An object that runs the implementation of a schema API. Instantiations of
1036  * this interfaces are used by Schemas.jsm.
1038  * @interface
1039  */
1040 class SchemaAPIInterface {
1041   /**
1042    * Calls this as a function that returns its return value.
1043    *
1044    * @abstract
1045    * @param {Array} args The parameters for the function.
1046    * @returns {*} The return value of the invoked function.
1047    */
1048   callFunction(args) {
1049     throw new Error("Not implemented");
1050   }
1052   /**
1053    * Calls this as a function and ignores its return value.
1054    *
1055    * @abstract
1056    * @param {Array} args The parameters for the function.
1057    */
1058   callFunctionNoReturn(args) {
1059     throw new Error("Not implemented");
1060   }
1062   /**
1063    * Calls this as a function that completes asynchronously.
1064    *
1065    * @abstract
1066    * @param {Array} args The parameters for the function.
1067    * @param {callback} [callback] The callback to be called when the function
1068    *     completes.
1069    * @param {boolean} [requireUserInput=false] If true, the function should
1070    *                  fail if the browser is not currently handling user input.
1071    * @returns {Promise|undefined} Must be void if `callback` is set, and a
1072    *     promise otherwise. The promise is resolved when the function completes.
1073    */
1074   callAsyncFunction(args, callback, requireUserInput = false) {
1075     throw new Error("Not implemented");
1076   }
1078   /**
1079    * Retrieves the value of this as a property.
1080    *
1081    * @abstract
1082    * @returns {*} The value of the property.
1083    */
1084   getProperty() {
1085     throw new Error("Not implemented");
1086   }
1088   /**
1089    * Assigns the value to this as property.
1090    *
1091    * @abstract
1092    * @param {string} value The new value of the property.
1093    */
1094   setProperty(value) {
1095     throw new Error("Not implemented");
1096   }
1098   /**
1099    * Registers a `listener` to this as an event.
1100    *
1101    * @abstract
1102    * @param {Function} listener The callback to be called when the event fires.
1103    * @param {Array} args Extra parameters for EventManager.addListener.
1104    * @see EventManager.addListener
1105    */
1106   addListener(listener, args) {
1107     throw new Error("Not implemented");
1108   }
1110   /**
1111    * Checks whether `listener` is listening to this as an event.
1112    *
1113    * @abstract
1114    * @param {Function} listener The event listener.
1115    * @returns {boolean} Whether `listener` is registered with this as an event.
1116    * @see EventManager.hasListener
1117    */
1118   hasListener(listener) {
1119     throw new Error("Not implemented");
1120   }
1122   /**
1123    * Unregisters `listener` from this as an event.
1124    *
1125    * @abstract
1126    * @param {Function} listener The event listener.
1127    * @see EventManager.removeListener
1128    */
1129   removeListener(listener) {
1130     throw new Error("Not implemented");
1131   }
1133   /**
1134    * Revokes the implementation object, and prevents any further method
1135    * calls from having external effects.
1136    *
1137    * @abstract
1138    */
1139   revoke() {
1140     throw new Error("Not implemented");
1141   }
1145  * An object that runs a locally implemented API.
1146  */
1147 class LocalAPIImplementation extends SchemaAPIInterface {
1148   /**
1149    * Constructs an implementation of the `name` method or property of `pathObj`.
1150    *
1151    * @param {object} pathObj The object containing the member with name `name`.
1152    * @param {string} name The name of the implemented member.
1153    * @param {BaseContext} context The context in which the schema is injected.
1154    */
1155   constructor(pathObj, name, context) {
1156     super();
1157     this.pathObj = pathObj;
1158     this.name = name;
1159     this.context = context;
1160   }
1162   revoke() {
1163     if (this.pathObj[this.name][lazy.Schemas.REVOKE]) {
1164       this.pathObj[this.name][lazy.Schemas.REVOKE]();
1165     }
1167     this.pathObj = null;
1168     this.name = null;
1169     this.context = null;
1170   }
1172   callFunction(args) {
1173     try {
1174       return this.pathObj[this.name](...args);
1175     } catch (e) {
1176       throw this.context.normalizeError(e);
1177     }
1178   }
1180   callFunctionNoReturn(args) {
1181     try {
1182       this.pathObj[this.name](...args);
1183     } catch (e) {
1184       throw this.context.normalizeError(e);
1185     }
1186   }
1188   callAsyncFunction(args, callback, requireUserInput) {
1189     let promise;
1190     try {
1191       if (requireUserInput) {
1192         if (!this.context.contentWindow.windowUtils.isHandlingUserInput) {
1193           throw new ExtensionError(
1194             `${this.name} may only be called from a user input handler`
1195           );
1196         }
1197       }
1198       promise = this.pathObj[this.name](...args) || Promise.resolve();
1199     } catch (e) {
1200       promise = Promise.reject(e);
1201     }
1202     return this.context.wrapPromise(promise, callback);
1203   }
1205   getProperty() {
1206     return this.pathObj[this.name];
1207   }
1209   setProperty(value) {
1210     this.pathObj[this.name] = value;
1211   }
1213   addListener(listener, args) {
1214     try {
1215       this.pathObj[this.name].addListener.call(null, listener, ...args);
1216     } catch (e) {
1217       throw this.context.normalizeError(e);
1218     }
1219   }
1221   hasListener(listener) {
1222     return this.pathObj[this.name].hasListener.call(null, listener);
1223   }
1225   removeListener(listener) {
1226     this.pathObj[this.name].removeListener.call(null, listener);
1227   }
1230 // Recursively copy properties from source to dest.
1231 function deepCopy(dest, source) {
1232   for (let prop in source) {
1233     let desc = Object.getOwnPropertyDescriptor(source, prop);
1234     if (typeof desc.value == "object") {
1235       if (!(prop in dest)) {
1236         dest[prop] = {};
1237       }
1238       deepCopy(dest[prop], source[prop]);
1239     } else {
1240       Object.defineProperty(dest, prop, desc);
1241     }
1242   }
1245 function getChild(map, key) {
1246   let child = map.children.get(key);
1247   if (!child) {
1248     child = {
1249       modules: new Set(),
1250       children: new Map(),
1251     };
1253     map.children.set(key, child);
1254   }
1255   return child;
1258 function getPath(map, path) {
1259   for (let key of path) {
1260     map = getChild(map, key);
1261   }
1262   return map;
1265 function mergePaths(dest, source) {
1266   for (let name of source.modules) {
1267     dest.modules.add(name);
1268   }
1270   for (let [name, child] of source.children.entries()) {
1271     mergePaths(getChild(dest, name), child);
1272   }
1276  * Manages loading and accessing a set of APIs for a specific extension
1277  * context.
1279  * @param {BaseContext} context
1280  *        The context to manage APIs for.
1281  * @param {SchemaAPIManager} apiManager
1282  *        The API manager holding the APIs to manage.
1283  * @param {object} root
1284  *        The root object into which APIs will be injected.
1285  */
1286 class CanOfAPIs {
1287   constructor(context, apiManager, root) {
1288     this.context = context;
1289     this.scopeName = context.envType;
1290     this.apiManager = apiManager;
1291     this.root = root;
1293     this.apiPaths = new Map();
1295     this.apis = new Map();
1296   }
1298   /**
1299    * Synchronously loads and initializes an ExtensionAPI instance.
1300    *
1301    * @param {string} name
1302    *        The name of the API to load.
1303    */
1304   loadAPI(name) {
1305     if (this.apis.has(name)) {
1306       return;
1307     }
1309     let { extension } = this.context;
1311     let api = this.apiManager.getAPI(name, extension, this.scopeName);
1312     if (!api) {
1313       return;
1314     }
1316     this.apis.set(name, api);
1318     deepCopy(this.root, api.getAPI(this.context));
1319   }
1321   /**
1322    * Asynchronously loads and initializes an ExtensionAPI instance.
1323    *
1324    * @param {string} name
1325    *        The name of the API to load.
1326    */
1327   async asyncLoadAPI(name) {
1328     if (this.apis.has(name)) {
1329       return;
1330     }
1332     let { extension } = this.context;
1333     if (!lazy.Schemas.checkPermissions(name, extension)) {
1334       return;
1335     }
1337     let api = await this.apiManager.asyncGetAPI(
1338       name,
1339       extension,
1340       this.scopeName
1341     );
1342     // Check again, because async;
1343     if (this.apis.has(name)) {
1344       return;
1345     }
1347     this.apis.set(name, api);
1349     deepCopy(this.root, api.getAPI(this.context));
1350   }
1352   /**
1353    * Finds the API at the given path from the root object, and
1354    * synchronously loads the API that implements it if it has not
1355    * already been loaded.
1356    *
1357    * @param {string} path
1358    *        The "."-separated path to find.
1359    * @returns {*}
1360    */
1361   findAPIPath(path) {
1362     if (this.apiPaths.has(path)) {
1363       return this.apiPaths.get(path);
1364     }
1366     let obj = this.root;
1367     let modules = this.apiManager.modulePaths;
1369     let parts = path.split(".");
1370     for (let [i, key] of parts.entries()) {
1371       if (!obj) {
1372         return;
1373       }
1374       modules = getChild(modules, key);
1376       for (let name of modules.modules) {
1377         if (!this.apis.has(name)) {
1378           this.loadAPI(name);
1379         }
1380       }
1382       if (!(key in obj) && i < parts.length - 1) {
1383         obj[key] = {};
1384       }
1385       obj = obj[key];
1386     }
1388     this.apiPaths.set(path, obj);
1389     return obj;
1390   }
1392   /**
1393    * Finds the API at the given path from the root object, and
1394    * asynchronously loads the API that implements it if it has not
1395    * already been loaded.
1396    *
1397    * @param {string} path
1398    *        The "."-separated path to find.
1399    * @returns {Promise<*>}
1400    */
1401   async asyncFindAPIPath(path) {
1402     if (this.apiPaths.has(path)) {
1403       return this.apiPaths.get(path);
1404     }
1406     let obj = this.root;
1407     let modules = this.apiManager.modulePaths;
1409     let parts = path.split(".");
1410     for (let [i, key] of parts.entries()) {
1411       if (!obj) {
1412         return;
1413       }
1414       modules = getChild(modules, key);
1416       for (let name of modules.modules) {
1417         if (!this.apis.has(name)) {
1418           await this.asyncLoadAPI(name);
1419         }
1420       }
1422       if (!(key in obj) && i < parts.length - 1) {
1423         obj[key] = {};
1424       }
1426       if (typeof obj[key] === "function") {
1427         obj = obj[key].bind(obj);
1428       } else {
1429         obj = obj[key];
1430       }
1431     }
1433     this.apiPaths.set(path, obj);
1434     return obj;
1435   }
1439  * @class APIModule
1440  * @abstract
1442  * @property {string} url
1443  *       The URL of the script which contains the module's
1444  *       implementation. This script must define a global property
1445  *       matching the modules name, which must be a class constructor
1446  *       which inherits from {@link ExtensionAPI}.
1448  * @property {string} schema
1449  *       The URL of the JSON schema which describes the module's API.
1451  * @property {Array<string>} scopes
1452  *       The list of scope names into which the API may be loaded.
1454  * @property {Array<string>} manifest
1455  *       The list of top-level manifest properties which will trigger
1456  *       the module to be loaded, and its `onManifestEntry` method to be
1457  *       called.
1459  * @property {Array<string>} events
1460  *       The list events which will trigger the module to be loaded, and
1461  *       its appropriate event handler method to be called. Currently
1462  *       only accepts "startup".
1464  * @property {Array<string>} permissions
1465  *       An optional list of permissions, any of which must be present
1466  *       in order for the module to load.
1468  * @property {Array<Array<string>>} paths
1469  *       A list of paths from the root API object which, when accessed,
1470  *       will cause the API module to be instantiated and injected.
1471  */
1474  * This object loads the ext-*.js scripts that define the extension API.
1476  * This class instance is shared with the scripts that it loads, so that the
1477  * ext-*.js scripts and the instantiator can communicate with each other.
1478  */
1479 class SchemaAPIManager extends EventEmitter {
1480   /**
1481    * @param {string} processType
1482    *     "main" - The main, one and only chrome browser process.
1483    *     "addon" - An addon process.
1484    *     "content" - A content process.
1485    *     "devtools" - A devtools process.
1486    * @param {import("Schemas.sys.mjs").SchemaRoot} [schema]
1487    */
1488   constructor(processType, schema) {
1489     super();
1490     this.processType = processType;
1491     this.global = null;
1492     if (schema) {
1493       this.schema = schema;
1494     }
1496     this.modules = new Map();
1497     this.modulePaths = { children: new Map(), modules: new Set() };
1498     this.manifestKeys = new Map();
1499     this.eventModules = new DefaultMap(() => new Set());
1500     this.settingsModules = new Set();
1502     this._modulesJSONLoaded = false;
1504     this.schemaURLs = new Map();
1506     this.apis = new DefaultWeakMap(() => new Map());
1508     this._scriptScopes = [];
1509   }
1511   onStartup(extension) {
1512     let promises = [];
1513     for (let apiName of this.eventModules.get("startup")) {
1514       promises.push(
1515         extension.apiManager.asyncGetAPI(apiName, extension).then(api => {
1516           if (api) {
1517             api.onStartup();
1518           }
1519         })
1520       );
1521     }
1523     return Promise.all(promises);
1524   }
1526   async loadModuleJSON(urls) {
1527     let promises = urls.map(url => fetch(url).then(resp => resp.json()));
1529     return this.initModuleJSON(await Promise.all(promises));
1530   }
1532   initModuleJSON(blobs) {
1533     for (let json of blobs) {
1534       this.registerModules(json);
1535     }
1537     this._modulesJSONLoaded = true;
1539     return new StructuredCloneHolder("SchemaAPIManager/initModuleJSON", null, {
1540       modules: this.modules,
1541       modulePaths: this.modulePaths,
1542       manifestKeys: this.manifestKeys,
1543       eventModules: this.eventModules,
1544       settingsModules: this.settingsModules,
1545       schemaURLs: this.schemaURLs,
1546     });
1547   }
1549   initModuleData(moduleData) {
1550     if (!this._modulesJSONLoaded) {
1551       let data = moduleData.deserialize({}, true);
1553       this.modules = data.modules;
1554       this.modulePaths = data.modulePaths;
1555       this.manifestKeys = data.manifestKeys;
1556       this.eventModules = new DefaultMap(() => new Set(), data.eventModules);
1557       this.settingsModules = new Set(data.settingsModules);
1558       this.schemaURLs = data.schemaURLs;
1559     }
1561     this._modulesJSONLoaded = true;
1562   }
1564   /**
1565    * Registers a set of ExtensionAPI modules to be lazily loaded and
1566    * managed by this manager.
1567    *
1568    * @param {object} obj
1569    *        An object containing property for eacy API module to be
1570    *        registered. Each value should be an object implementing the
1571    *        APIModule interface.
1572    */
1573   registerModules(obj) {
1574     for (let [name, details] of Object.entries(obj)) {
1575       details.namespaceName = name;
1577       if (this.modules.has(name)) {
1578         throw new Error(`Module '${name}' already registered`);
1579       }
1580       this.modules.set(name, details);
1582       if (details.schema) {
1583         let content =
1584           details.scopes &&
1585           (details.scopes.includes("content_parent") ||
1586             details.scopes.includes("content_child"));
1587         this.schemaURLs.set(details.schema, { content });
1588       }
1590       for (let event of details.events || []) {
1591         this.eventModules.get(event).add(name);
1592       }
1594       if (details.settings) {
1595         this.settingsModules.add(name);
1596       }
1598       for (let key of details.manifest || []) {
1599         if (this.manifestKeys.has(key)) {
1600           throw new Error(
1601             `Manifest key '${key}' already registered by '${this.manifestKeys.get(
1602               key
1603             )}'`
1604           );
1605         }
1607         this.manifestKeys.set(key, name);
1608       }
1610       for (let path of details.paths || []) {
1611         getPath(this.modulePaths, path).modules.add(name);
1612       }
1613     }
1614   }
1616   /**
1617    * Emits an `onManifestEntry` event for the top-level manifest entry
1618    * on all relevant {@link ExtensionAPI} instances for the given
1619    * extension.
1620    *
1621    * The API modules will be synchronously loaded if they have not been
1622    * loaded already.
1623    *
1624    * @param {Extension} extension
1625    *        The extension for which to emit the events.
1626    * @param {string} entry
1627    *        The name of the top-level manifest entry.
1628    *
1629    * @returns {*}
1630    */
1631   emitManifestEntry(extension, entry) {
1632     let apiName = this.manifestKeys.get(entry);
1633     if (apiName) {
1634       let api = extension.apiManager.getAPI(apiName, extension);
1635       return api.onManifestEntry(entry);
1636     }
1637   }
1638   /**
1639    * Emits an `onManifestEntry` event for the top-level manifest entry
1640    * on all relevant {@link ExtensionAPI} instances for the given
1641    * extension.
1642    *
1643    * The API modules will be asynchronously loaded if they have not been
1644    * loaded already.
1645    *
1646    * @param {Extension} extension
1647    *        The extension for which to emit the events.
1648    * @param {string} entry
1649    *        The name of the top-level manifest entry.
1650    *
1651    * @returns {Promise<*>}
1652    */
1653   async asyncEmitManifestEntry(extension, entry) {
1654     let apiName = this.manifestKeys.get(entry);
1655     if (apiName) {
1656       let api = await extension.apiManager.asyncGetAPI(apiName, extension);
1657       return api.onManifestEntry(entry);
1658     }
1659   }
1661   /**
1662    * Returns the {@link ExtensionAPI} instance for the given API module,
1663    * for the given extension, in the given scope, synchronously loading
1664    * and instantiating it if necessary.
1665    *
1666    * @param {string} name
1667    *        The name of the API module to load.
1668    * @param {Extension} extension
1669    *        The extension for which to load the API.
1670    * @param {string} [scope = null]
1671    *        The scope type for which to retrieve the API, or null if not
1672    *        being retrieved for a particular scope.
1673    *
1674    * @returns {ExtensionAPI?}
1675    */
1676   getAPI(name, extension, scope = null) {
1677     if (!this._checkGetAPI(name, extension, scope)) {
1678       return;
1679     }
1681     let apis = this.apis.get(extension);
1682     if (apis.has(name)) {
1683       return apis.get(name);
1684     }
1686     let module = this.loadModule(name);
1688     let api = new module(extension);
1689     apis.set(name, api);
1690     return api;
1691   }
1692   /**
1693    * Returns the {@link ExtensionAPI} instance for the given API module,
1694    * for the given extension, in the given scope, asynchronously loading
1695    * and instantiating it if necessary.
1696    *
1697    * @param {string} name
1698    *        The name of the API module to load.
1699    * @param {Extension} extension
1700    *        The extension for which to load the API.
1701    * @param {string} [scope = null]
1702    *        The scope type for which to retrieve the API, or null if not
1703    *        being retrieved for a particular scope.
1704    *
1705    * @returns {Promise<ExtensionAPI>?}
1706    */
1707   async asyncGetAPI(name, extension, scope = null) {
1708     if (!this._checkGetAPI(name, extension, scope)) {
1709       return;
1710     }
1712     let apis = this.apis.get(extension);
1713     if (apis.has(name)) {
1714       return apis.get(name);
1715     }
1717     let module = await this.asyncLoadModule(name);
1719     // Check again, because async.
1720     if (apis.has(name)) {
1721       return apis.get(name);
1722     }
1724     let api = new module(extension);
1725     apis.set(name, api);
1726     return api;
1727   }
1729   /**
1730    * Synchronously loads an API module, if not already loaded, and
1731    * returns its ExtensionAPI constructor.
1732    *
1733    * @param {string} name
1734    *        The name of the module to load.
1735    * @returns {typeof ExtensionAPI}
1736    */
1737   loadModule(name) {
1738     let module = this.modules.get(name);
1739     if (module.loaded) {
1740       return this.global[name];
1741     }
1743     this._checkLoadModule(module, name);
1745     this.initGlobal();
1747     Services.scriptloader.loadSubScript(module.url, this.global);
1749     module.loaded = true;
1751     return this.global[name];
1752   }
1753   /**
1754    * aSynchronously loads an API module, if not already loaded, and
1755    * returns its ExtensionAPI constructor.
1756    *
1757    * @param {string} name
1758    *        The name of the module to load.
1759    *
1760    * @returns {Promise<typeof ExtensionAPI>}
1761    */
1762   asyncLoadModule(name) {
1763     let module = this.modules.get(name);
1764     if (module.loaded) {
1765       return Promise.resolve(this.global[name]);
1766     }
1767     if (module.asyncLoaded) {
1768       return module.asyncLoaded;
1769     }
1771     this._checkLoadModule(module, name);
1773     module.asyncLoaded = ChromeUtils.compileScript(module.url).then(script => {
1774       this.initGlobal();
1775       script.executeInGlobal(this.global);
1777       module.loaded = true;
1779       return this.global[name];
1780     });
1782     return module.asyncLoaded;
1783   }
1785   asyncLoadSettingsModules() {
1786     return Promise.all(
1787       Array.from(this.settingsModules).map(apiName =>
1788         this.asyncLoadModule(apiName)
1789       )
1790     );
1791   }
1793   getModule(name) {
1794     return this.modules.get(name);
1795   }
1797   /**
1798    * Checks whether the given API module may be loaded for the given
1799    * extension, in the given scope.
1800    *
1801    * @param {string} name
1802    *        The name of the API module to check.
1803    * @param {Extension} extension
1804    *        The extension for which to check the API.
1805    * @param {string} [scope = null]
1806    *        The scope type for which to check the API, or null if not
1807    *        being checked for a particular scope.
1808    *
1809    * @returns {boolean}
1810    *        Whether the module may be loaded.
1811    */
1812   _checkGetAPI(name, extension, scope = null) {
1813     let module = this.getModule(name);
1814     if (!module) {
1815       // A module may not exist for a particular manifest version, but
1816       // we allow keys in the manifest.  An example is pageAction.
1817       return false;
1818     }
1820     if (
1821       module.permissions &&
1822       !module.permissions.some(perm => extension.hasPermission(perm))
1823     ) {
1824       return false;
1825     }
1827     if (!scope) {
1828       return true;
1829     }
1831     if (!module.scopes.includes(scope)) {
1832       return false;
1833     }
1835     if (!lazy.Schemas.checkPermissions(module.namespaceName, extension)) {
1836       return false;
1837     }
1839     return true;
1840   }
1842   _checkLoadModule(module, name) {
1843     if (!module) {
1844       throw new Error(`Module '${name}' does not exist`);
1845     }
1846     if (module.asyncLoaded) {
1847       throw new Error(`Module '${name}' currently being lazily loaded`);
1848     }
1849     if (this.global && this.global[name]) {
1850       throw new Error(
1851         `Module '${name}' conflicts with existing global property`
1852       );
1853     }
1854   }
1856   /**
1857    * Create a global object that is used as the shared global for all ext-*.js
1858    * scripts that are loaded via `loadScript`.
1859    *
1860    * @returns {object} A sandbox that is used as the global by `loadScript`.
1861    */
1862   _createExtGlobal() {
1863     let global = Cu.Sandbox(
1864       Services.scriptSecurityManager.getSystemPrincipal(),
1865       {
1866         wantXrays: false,
1867         wantGlobalProperties: ["ChromeUtils"],
1868         sandboxName: `Namespace of ext-*.js scripts for ${this.processType} (from: resource://gre/modules/ExtensionCommon.jsm)`,
1869       }
1870     );
1872     Object.assign(global, {
1873       AppConstants,
1874       Cc,
1875       ChromeWorker,
1876       Ci,
1877       Cr,
1878       Cu,
1879       ExtensionAPI,
1880       ExtensionAPIPersistent,
1881       ExtensionCommon,
1882       IOUtils,
1883       MatchGlob,
1884       MatchPattern,
1885       MatchPatternSet,
1886       PathUtils,
1887       Services,
1888       StructuredCloneHolder,
1889       WebExtensionPolicy,
1890       XPCOMUtils,
1891       extensions: this,
1892       global,
1893     });
1895     ChromeUtils.defineLazyGetter(global, "console", getConsole);
1896     // eslint-disable-next-line mozilla/lazy-getter-object-name
1897     ChromeUtils.defineESModuleGetters(global, {
1898       ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs",
1899     });
1901     return global;
1902   }
1904   initGlobal() {
1905     if (!this.global) {
1906       this.global = this._createExtGlobal();
1907     }
1908   }
1910   /**
1911    * Load an ext-*.js script. The script runs in its own scope, if it wishes to
1912    * share state with another script it can assign to the `global` variable. If
1913    * it wishes to communicate with this API manager, use `extensions`.
1914    *
1915    * @param {string} scriptUrl The URL of the ext-*.js script.
1916    */
1917   loadScript(scriptUrl) {
1918     // Create the object in the context of the sandbox so that the script runs
1919     // in the sandbox's context instead of here.
1920     let scope = Cu.createObjectIn(this.global);
1922     Services.scriptloader.loadSubScript(scriptUrl, scope);
1924     // Save the scope to avoid it being garbage collected.
1925     this._scriptScopes.push(scope);
1926   }
1929 class LazyAPIManager extends SchemaAPIManager {
1930   constructor(processType, moduleData, schemaURLs) {
1931     super(processType);
1933     /** @type {Promise | boolean} */
1934     this.initialized = false;
1936     this.initModuleData(moduleData);
1938     this.schemaURLs = schemaURLs;
1939   }
1941   lazyInit() {}
1944 defineLazyGetter(LazyAPIManager.prototype, "schema", function () {
1945   let root = new lazy.SchemaRoot(lazy.Schemas.rootSchema, this.schemaURLs);
1946   root.parseSchemas();
1947   return root;
1950 class MultiAPIManager extends SchemaAPIManager {
1951   constructor(processType, children) {
1952     super(processType);
1954     this.initialized = false;
1956     this.children = children;
1957   }
1959   async lazyInit() {
1960     if (!this.initialized) {
1961       this.initialized = true;
1963       for (let child of this.children) {
1964         if (child.lazyInit) {
1965           let res = child.lazyInit();
1966           if (res && typeof res.then === "function") {
1967             await res;
1968           }
1969         }
1971         mergePaths(this.modulePaths, child.modulePaths);
1972       }
1973     }
1974   }
1976   onStartup(extension) {
1977     return Promise.all(this.children.map(child => child.onStartup(extension)));
1978   }
1980   getModule(name) {
1981     for (let child of this.children) {
1982       if (child.modules.has(name)) {
1983         return child.modules.get(name);
1984       }
1985     }
1986   }
1988   loadModule(name) {
1989     for (let child of this.children) {
1990       if (child.modules.has(name)) {
1991         return child.loadModule(name);
1992       }
1993     }
1994   }
1996   asyncLoadModule(name) {
1997     for (let child of this.children) {
1998       if (child.modules.has(name)) {
1999         return child.asyncLoadModule(name);
2000       }
2001     }
2002   }
2005 defineLazyGetter(MultiAPIManager.prototype, "schema", function () {
2006   let bases = this.children.map(child => child.schema);
2008   // All API manager schema roots should derive from the global schema root,
2009   // so it doesn't need its own entry.
2010   if (bases[bases.length - 1] === lazy.Schemas) {
2011     bases.pop();
2012   }
2014   if (bases.length === 1) {
2015     bases = bases[0];
2016   }
2017   return new lazy.SchemaRoot(bases, new Map());
2020 export function LocaleData(data) {
2021   this.defaultLocale = data.defaultLocale;
2022   this.selectedLocale = data.selectedLocale;
2023   this.locales = data.locales || new Map();
2024   this.warnedMissingKeys = new Set();
2026   // Map(locale-name -> Map(message-key -> localized-string))
2027   //
2028   // Contains a key for each loaded locale, each of which is a
2029   // Map of message keys to their localized strings.
2030   this.messages = data.messages || new Map();
2032   if (data.builtinMessages) {
2033     this.messages.set(this.BUILTIN, data.builtinMessages);
2034   }
2037 LocaleData.prototype = {
2038   // Representation of the object to send to content processes. This
2039   // should include anything the content process might need.
2040   serialize() {
2041     return {
2042       defaultLocale: this.defaultLocale,
2043       selectedLocale: this.selectedLocale,
2044       messages: this.messages,
2045       locales: this.locales,
2046     };
2047   },
2049   BUILTIN: "@@BUILTIN_MESSAGES",
2051   has(locale) {
2052     return this.messages.has(locale);
2053   },
2055   // https://developer.chrome.com/extensions/i18n
2056   localizeMessage(message, substitutions = [], options = {}) {
2057     let defaultOptions = {
2058       defaultValue: "",
2059       cloneScope: null,
2060     };
2062     let locales = this.availableLocales;
2063     if (options.locale) {
2064       locales = new Set(
2065         [this.BUILTIN, options.locale, this.defaultLocale].filter(locale =>
2066           this.messages.has(locale)
2067         )
2068       );
2069     }
2071     options = Object.assign(defaultOptions, options);
2073     // Message names are case-insensitive, so normalize them to lower-case.
2074     message = message.toLowerCase();
2075     for (let locale of locales) {
2076       let messages = this.messages.get(locale);
2077       if (messages.has(message)) {
2078         let str = messages.get(message);
2080         if (!str.includes("$")) {
2081           return str;
2082         }
2084         if (!Array.isArray(substitutions)) {
2085           substitutions = [substitutions];
2086         }
2088         let replacer = (matched, index, dollarSigns) => {
2089           if (index) {
2090             // This is not quite Chrome-compatible. Chrome consumes any number
2091             // of digits following the $, but only accepts 9 substitutions. We
2092             // accept any number of substitutions.
2093             index = parseInt(index, 10) - 1;
2094             return index in substitutions ? substitutions[index] : "";
2095           }
2096           // For any series of contiguous `$`s, the first is dropped, and
2097           // the rest remain in the output string.
2098           return dollarSigns;
2099         };
2100         return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer);
2101       }
2102     }
2104     // Check for certain pre-defined messages.
2105     if (message == "@@ui_locale") {
2106       return this.uiLocale;
2107     } else if (message.startsWith("@@bidi_")) {
2108       let rtl = Services.locale.isAppLocaleRTL;
2110       if (message == "@@bidi_dir") {
2111         return rtl ? "rtl" : "ltr";
2112       } else if (message == "@@bidi_reversed_dir") {
2113         return rtl ? "ltr" : "rtl";
2114       } else if (message == "@@bidi_start_edge") {
2115         return rtl ? "right" : "left";
2116       } else if (message == "@@bidi_end_edge") {
2117         return rtl ? "left" : "right";
2118       }
2119     }
2121     if (!this.warnedMissingKeys.has(message)) {
2122       let error = `Unknown localization message ${message}`;
2123       if (options.cloneScope) {
2124         error = new options.cloneScope.Error(error);
2125       }
2126       Cu.reportError(error);
2127       this.warnedMissingKeys.add(message);
2128     }
2129     return options.defaultValue;
2130   },
2132   // Localize a string, replacing all |__MSG_(.*)__| tokens with the
2133   // matching string from the current locale, as determined by
2134   // |this.selectedLocale|.
2135   //
2136   // This may not be called before calling either |initLocale| or
2137   // |initAllLocales|.
2138   localize(str, locale = this.selectedLocale) {
2139     if (!str) {
2140       return str;
2141     }
2143     return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
2144       return this.localizeMessage(message, [], {
2145         locale,
2146         defaultValue: matched,
2147       });
2148     });
2149   },
2151   // Validates the contents of a locale JSON file, normalizes the
2152   // messages into a Map of message key -> localized string pairs.
2153   addLocale(locale, messages, extension) {
2154     let result = new Map();
2156     let isPlainObject = obj =>
2157       obj &&
2158       typeof obj === "object" &&
2159       ChromeUtils.getClassName(obj) === "Object";
2161     // Chrome does not document the semantics of its localization
2162     // system very well. It handles replacements by pre-processing
2163     // messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their
2164     // replacements. Later, it processes the resulting string for
2165     // |$[0-9]| replacements.
2166     //
2167     // Again, it does not document this, but it accepts any number
2168     // of sequential |$|s, and replaces them with that number minus
2169     // 1. It also accepts |$| followed by any number of sequential
2170     // digits, but refuses to process a localized string which
2171     // provides more than 9 substitutions.
2172     if (!isPlainObject(messages)) {
2173       extension.packagingError(`Invalid locale data for ${locale}`);
2174       return result;
2175     }
2177     for (let key of Object.keys(messages)) {
2178       let msg = messages[key];
2180       if (!isPlainObject(msg) || typeof msg.message != "string") {
2181         extension.packagingError(
2182           `Invalid locale message data for ${locale}, message ${JSON.stringify(
2183             key
2184           )}`
2185         );
2186         continue;
2187       }
2189       // Substitutions are case-insensitive, so normalize all of their names
2190       // to lower-case.
2191       let placeholders = new Map();
2192       if ("placeholders" in msg && isPlainObject(msg.placeholders)) {
2193         for (let key of Object.keys(msg.placeholders)) {
2194           placeholders.set(key.toLowerCase(), msg.placeholders[key]);
2195         }
2196       }
2198       let replacer = (match, name) => {
2199         let replacement = placeholders.get(name.toLowerCase());
2200         if (isPlainObject(replacement) && "content" in replacement) {
2201           return replacement.content;
2202         }
2203         return "";
2204       };
2206       let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer);
2208       // Message names are also case-insensitive, so normalize them to lower-case.
2209       result.set(key.toLowerCase(), value);
2210     }
2212     this.messages.set(locale, result);
2213     return result;
2214   },
2216   get acceptLanguages() {
2217     let result = Services.prefs.getComplexValue(
2218       "intl.accept_languages",
2219       Ci.nsIPrefLocalizedString
2220     ).data;
2221     return result.split(/\s*,\s*/g);
2222   },
2224   get uiLocale() {
2225     return Services.locale.appLocaleAsBCP47;
2226   },
2228   get availableLocales() {
2229     const locales = [this.BUILTIN, this.selectedLocale, this.defaultLocale];
2230     const value = new Set(locales.filter(locale => this.messages.has(locale)));
2231     return redefineGetter(this, "availableLocales", value);
2232   },
2236  * This is a generic class for managing event listeners.
2238  * @example
2239  * new EventManager({
2240  *   context,
2241  *   name: "api.subAPI",
2242  *   register:  fire => {
2243  *     let listener = (...) => {
2244  *       // Fire any listeners registered with addListener.
2245  *       fire.async(arg1, arg2);
2246  *     };
2247  *     // Register the listener.
2248  *     SomehowRegisterListener(listener);
2249  *     return () => {
2250  *       // Return a way to unregister the listener.
2251  *       SomehowUnregisterListener(listener);
2252  *     };
2253  *   }
2254  * }).api()
2256  * The result is an object with addListener, removeListener, and
2257  * hasListener methods. `context` is an add-on scope (either an
2258  * ExtensionContext in the chrome process or ExtensionContext in a
2259  * content process).
2260  */
2261 class EventManager {
2262   /*
2263    * A persistent event must provide module and name.  Additionally the
2264    * module must implement primeListeners in the ExtensionAPI class.
2265    *
2266    * A startup blocking event must also add the startupBlocking flag in
2267    * ext-toolkit.json or ext-browser.json.
2268    *
2269    * Listeners synchronously added from a background extension context
2270    * will be persisted, for a persistent background script only the
2271    * "startup blocking" events will be persisted.
2272    *
2273    * EventManager instances created in a child process can't persist any listener.
2274    *
2275    * @param {object} params
2276    *        Parameters that control this EventManager.
2277    * @param {BaseContext} params.context
2278    *        An object representing the extension instance using this event.
2279    * @param {string} params.module
2280    *        The API module name, required for persistent events.
2281    * @param {string} params.event
2282    *        The API event name, required for persistent events.
2283    * @param {ExtensionAPI} params.extensionApi
2284    *        The API intance.  If the API uses the ExtensionAPIPersistent class, some simplification is
2285    *        possible by passing the api (self or this) and the internal register function will be used.
2286    * @param {string} [params.name]
2287    *        A name used only for debugging.  If not provided, name is built from module and event.
2288    * @param {functon} params.register
2289    *        A function called whenever a new listener is added.
2290    * @param {boolean} [params.inputHandling=false]
2291    *        If true, the "handling user input" flag is set while handlers
2292    *        for this event are executing.
2293    */
2294   constructor(params) {
2295     let {
2296       context,
2297       module,
2298       event,
2299       name,
2300       register,
2301       extensionApi,
2302       inputHandling = false,
2303       resetIdleOnEvent = true,
2304     } = params;
2305     this.context = context;
2306     this.module = module;
2307     this.event = event;
2308     this.name = name;
2309     this.register = register;
2310     this.inputHandling = inputHandling;
2311     this.resetIdleOnEvent = resetIdleOnEvent;
2313     const isBackgroundParent =
2314       this.context.envType === "addon_parent" &&
2315       this.context.isBackgroundContext;
2317     // TODO(Bug 1844041): ideally we should restrict resetIdleOnEvent to
2318     // EventManager instances that belongs to the event page, but along
2319     // with that we should consider if calling sendMessage from an event
2320     // page should also reset idle timer, and so in the shorter term
2321     // here we are allowing listeners from other extension pages to
2322     // also reset the idle timer.
2323     const isAddonContext = ["addon_parent", "addon_child"].includes(
2324       this.context.envType
2325     );
2327     // Avoid resetIdleOnEvent overhead by only consider it when applicable.
2328     if (!isAddonContext || context.extension.persistentBackground) {
2329       this.resetIdleOnEvent = false;
2330     }
2332     if (!name) {
2333       this.name = `${module}.${event}`;
2334     }
2336     if (!this.register && extensionApi instanceof ExtensionAPIPersistent) {
2337       this.register = (fire, ...params) => {
2338         return extensionApi.registerEventListener(
2339           { context, event, fire },
2340           params
2341         );
2342       };
2343     }
2344     if (!this.register) {
2345       throw new Error(
2346         `EventManager requires register method for ${this.name}.`
2347       );
2348     }
2350     this.canPersistEvents = module && event && isBackgroundParent;
2352     if (this.canPersistEvents) {
2353       let { extension } = context;
2354       if (extension.persistentBackground) {
2355         // Persistent backgrounds will only persist startup blocking APIs.
2356         let api_module = extension.apiManager.getModule(this.module);
2357         if (!api_module?.startupBlocking) {
2358           this.canPersistEvents = false;
2359         }
2360       } else {
2361         // Event pages will persist all APIs that implement primeListener.
2362         // The api is already loaded so this does not have performance effect.
2363         let api = extension.apiManager.getAPI(
2364           this.module,
2365           extension,
2366           "addon_parent"
2367         );
2369         // If the api doesn't implement primeListener we do not persist the events.
2370         if (!api?.primeListener) {
2371           this.canPersistEvents = false;
2372         }
2373       }
2374     }
2376     this.unregister = new Map();
2377     this.remove = new Map();
2378   }
2380   /*
2381    * Information about listeners to persistent events is associated with
2382    * the extension to which they belong.  Any extension thas has such
2383    * listeners has a property called `persistentListeners` that is a
2384    * 3-level Map:
2385    *
2386    * - the first 2 keys are the module name (e.g., webRequest)
2387    *   and the name of the event within the module (e.g., onBeforeRequest).
2388    *
2389    * - the third level of the map is used to track multiple listeners for
2390    *   the same event, these listeners are distinguished by the extra arguments
2391    *   passed to addListener()
2392    *
2393    * - for quick lookups, the key to the third Map is the result of calling
2394    *   uneval() on the array of extra arguments.
2395    *
2396    * - the value stored in the Map or persistent listeners we keep in memory
2397    *   is a plain object with:
2398    *   - a property called `params` that is the original (ie, not uneval()ed)
2399    *     extra arguments to addListener()
2400    *   - and a property called `listeners` that is an array of plain object
2401    *     each representing a listener to be primed and a `primeId` autoincremented
2402    *     integer that represents each of the primed listeners that belongs to the
2403    *     group listeners with the same set of extra params.
2404    *   - a `nextPrimeId` property keeps track of the numeric primeId that should
2405    *     be assigned to new persistent listeners added for the same event and
2406    *     same set of extra params.
2407    *
2408    * For a primed listener (i.e., the stub listener created during browser startup
2409    * before the extension background page is started, and after an event page is
2410    * suspended on idle), the object will be later populated (by the callers of
2411    * EventManager.primeListeners) with an additional `primed` property that serves
2412    * as a placeholder listener, collecting all events that got emitted while the
2413    * background page was not yet started, and eventually replaced by a callback
2414    * registered from the extension code, once the background page scripts have been
2415    * executed (or dropped if the background page scripts do not register the same
2416    * listener anymore).
2417    *
2418    * @param {Extension} extension
2419    * @returns {boolean} True if the extension had any persistent listeners.
2420    */
2421   static _initPersistentListeners(extension) {
2422     if (extension.persistentListeners) {
2423       return !!extension.persistentListeners.size;
2424     }
2426     let listeners = new DefaultMap(() => new DefaultMap(() => new Map()));
2427     extension.persistentListeners = listeners;
2429     let persistentListeners = extension.startupData?.persistentListeners;
2430     if (!persistentListeners) {
2431       return false;
2432     }
2434     let found = false;
2435     for (let [module, savedModuleEntry] of Object.entries(
2436       persistentListeners
2437     )) {
2438       for (let [event, savedEventEntry] of Object.entries(savedModuleEntry)) {
2439         for (let paramList of savedEventEntry) {
2440           /* Before Bug 1795801 (Firefox < 113) each entry was related to a listener
2441            * registered with a different set of extra params (and so only one listener
2442            * could be persisted for the same set of extra params)
2443            *
2444            * After Bug 1795801 (Firefox >= 113) each entry still represents a listener
2445            * registered for that event, but multiple listeners registered with the same
2446            * set of extra params will be captured as multiple entries in the
2447            * paramsList array.
2448            *
2449            * NOTE: persisted listeners are stored in the startupData part of the Addon DB
2450            * and are expected to be preserved across Firefox and Addons upgrades and downgrades
2451            * (unlike the WebExtensions startupCache data which is cleared when Firefox or the
2452            * addon is updated) and so we are taking special care about forward and backward
2453            * compatibility of the persistentListeners on-disk format:
2454            *
2455            * - forward compatibility: when this new version of this startupData loading logic
2456            *   is loading the old persistentListeners on-disk format:
2457            *   - on the first run only one listener will be primed for each of the extra params
2458            *     recorded in the startupData (same as in older Firefox versions)
2459            *     and Bug 1795801 will still be hit, but once the background
2460            *     context is started once the startupData will be updated to
2461            *     include each of the listeners (indipendently if the set of
2462            *     extra params is the same as another listener already been
2463            *     persisted).
2464            *   - after the first run, all listeners will be primed separately, even if the extra
2465            *     params are the same as other listeners already primed, and so
2466            *     each of the listener will receive the pending events collected
2467            *     by their related primed listener and Bug 1795801 not to be hit anymore.
2468            *
2469            * - backward compatibility: when the old version of this startupData loading logic
2470            *   (https://searchfox.org/mozilla-central/rev/cd2121e7d8/toolkit/components/extensions/ExtensionCommon.jsm#2360-2371)
2471            *   is loading the new persistentListeners on-disk format, the last
2472            *   entry with the same set of extra params will be eventually overwritting the
2473            *   entry for another primed listener with the same extra params, Bug 1795801 will still
2474            *   be hit, but no actual change in behavior is expected.
2475            */
2476           let key = uneval(paramList);
2477           const eventEntry = listeners.get(module).get(event);
2479           if (eventEntry.has(key)) {
2480             const keyEntry = eventEntry.get(key);
2481             let primeId = keyEntry.nextPrimeId;
2482             keyEntry.listeners.push({ primeId });
2483             keyEntry.nextPrimeId++;
2484           } else {
2485             eventEntry.set(key, {
2486               params: paramList,
2487               nextPrimeId: 1,
2488               listeners: [{ primeId: 0 }],
2489             });
2490           }
2491           found = true;
2492         }
2493       }
2494     }
2495     return found;
2496   }
2498   // Extract just the information needed at startup for all persistent
2499   // listeners, and arrange for it to be saved.  This should be called
2500   // whenever the set of persistent listeners for an extension changes.
2501   static _writePersistentListeners(extension) {
2502     let startupListeners = {};
2503     for (let [module, moduleEntry] of extension.persistentListeners) {
2504       startupListeners[module] = {};
2505       for (let [event, eventEntry] of moduleEntry) {
2506         // Turn the per-event entries from the format they are being kept
2507         // in memory:
2508         //
2509         //   [
2510         //     { params: paramList1, listeners: [listener1, listener2, ...] },
2511         //     { params: paramList2, listeners: [listener3, listener3, ...] },
2512         //     ...
2513         //   ]
2514         //
2515         // into the format used for storing them on disk (in the startupData),
2516         // which is an array of the params for each listener (with the param list
2517         // included as many times as many listeners are persisted for the same
2518         // set of params):
2519         //
2520         //   [paramList1, paramList1, ..., paramList2, paramList2, ...]
2521         //
2522         // This format will also work as expected on older Firefox versions where
2523         // only one listener was being persisted for each set of params.
2524         startupListeners[module][event] = Array.from(
2525           eventEntry.values()
2526         ).flatMap(keyEntry => keyEntry.listeners.map(() => keyEntry.params));
2527       }
2528     }
2530     extension.startupData.persistentListeners = startupListeners;
2531     extension.saveStartupData();
2532   }
2534   // Set up "primed" event listeners for any saved event listeners
2535   // in an extension's startup data.
2536   // This function is only called during browser startup, it stores details
2537   // about all primed listeners in the extension's persistentListeners Map.
2538   static primeListeners(extension, isInStartup = false) {
2539     if (!EventManager._initPersistentListeners(extension)) {
2540       return;
2541     }
2543     for (let [module, moduleEntry] of extension.persistentListeners) {
2544       // If we're in startup, we only want to continue attempting to prime a
2545       // subset of events that should be startup blocking.
2546       if (isInStartup) {
2547         let api_module = extension.apiManager.getModule(module);
2548         if (!api_module.startupBlocking) {
2549           continue;
2550         }
2551       }
2553       let api = extension.apiManager.getAPI(module, extension, "addon_parent");
2555       // If an extension is upgraded and a permission, such as webRequest, is
2556       // removed, we will have been called but the API is no longer available.
2557       if (!api?.primeListener) {
2558         // The runtime module no longer implements primed listeners, drop them.
2559         extension.persistentListeners.delete(module);
2560         EventManager._writePersistentListeners(extension);
2561         continue;
2562       }
2563       for (let [event, eventEntry] of moduleEntry) {
2564         for (let [key, { params, listeners }] of eventEntry) {
2565           for (let listener of listeners) {
2566             // Reset the `listener.added` flag by setting it to `false` while
2567             // re-priming the listeners because the event page has suspended
2568             // and the previous converted listener is no longer listening.
2569             const listenerWasAdded = listener.added;
2570             listener.added = false;
2571             listener.params = params;
2572             let primed = { pendingEvents: [] };
2574             let fireEvent = (...args) =>
2575               new Promise((resolve, reject) => {
2576                 if (!listener.primed) {
2577                   reject(
2578                     new Error(
2579                       `primed listener ${module}.${event} not re-registered`
2580                     )
2581                   );
2582                   return;
2583                 }
2584                 primed.pendingEvents.push({ args, resolve, reject });
2585                 extension.emit("background-script-event");
2586               });
2588             let fire = {
2589               wakeup: () => extension.wakeupBackground(),
2590               sync: fireEvent,
2591               async: fireEvent,
2592               // fire.async for ProxyContextParent is already not cloning.
2593               raw: fireEvent,
2594             };
2596             try {
2597               let handler = api.primeListener(
2598                 event,
2599                 fire,
2600                 listener.params,
2601                 isInStartup
2602               );
2603               if (handler) {
2604                 listener.primed = primed;
2605                 Object.assign(primed, handler);
2606               }
2607             } catch (e) {
2608               Cu.reportError(
2609                 `Error priming listener ${module}.${event}: ${e} :: ${e.stack}`
2610               );
2611               // Force this listener to be cleared.
2612               listener.error = true;
2613             }
2615             // If an attempt to prime a listener failed, ensure it is cleared now.
2616             // If a module is a startup blocking module, not all listeners may
2617             // get primed during early startup.  For that reason, we don't clear
2618             // persisted listeners during early startup.  At the end of background
2619             // execution any listeners that were not renewed will be cleared.
2620             //
2621             // TODO(Bug 1797474): consider priming runtime.onStartup and
2622             // avoid to special handling it here.
2623             if (
2624               listener.error ||
2625               (!isInStartup &&
2626                 !(
2627                   (`${module}.${event}` === "runtime.onStartup" &&
2628                     listenerWasAdded) ||
2629                   listener.primed
2630                 ))
2631             ) {
2632               EventManager.clearPersistentListener(
2633                 extension,
2634                 module,
2635                 event,
2636                 key,
2637                 listener.primeId
2638               );
2639             }
2640           }
2641         }
2642       }
2643     }
2644   }
2646   /**
2647    * This is called as a result of background script startup-finished and shutdown.
2648    *
2649    * After startup, it removes any remaining primed listeners.  These exist if the
2650    * listener was not renewed during startup.  In this case the persisted listener
2651    * data is also removed.
2652    *
2653    * During shutdown, care should be taken to set clearPersistent to false.
2654    * persisted listener data should NOT be cleared during shutdown.
2655    *
2656    * @param {Extension} extension
2657    * @param {boolean} clearPersistent whether the persisted listener data should be cleared.
2658    */
2659   static clearPrimedListeners(extension, clearPersistent = true) {
2660     if (!extension.persistentListeners) {
2661       return;
2662     }
2664     for (let [module, moduleEntry] of extension.persistentListeners) {
2665       for (let [event, eventEntry] of moduleEntry) {
2666         for (let [key, { listeners }] of eventEntry) {
2667           for (let listener of listeners) {
2668             let { primed, added, primeId } = listener;
2669             // When a primed listener is added or renewed during initial
2670             // background execution we set an added flag.  If it was primed
2671             // when added, primed is set to null.
2672             if (added) {
2673               continue;
2674             }
2676             if (primed) {
2677               // When a primed listener was not renewed, primed will still be truthy.
2678               // These need to be cleared on shutdown (important for event pages), but
2679               // we only clear the persisted listener data after the startup of a background.
2680               // Release any pending events and unregister the primed handler.
2681               listener.primed = null;
2683               for (let evt of primed.pendingEvents) {
2684                 evt.reject(new Error("listener not re-registered"));
2685               }
2686               primed.unregister();
2687             }
2689             // Clear any persisted events that were not renewed, should typically
2690             // only be done at the end of the background page load.
2691             if (clearPersistent) {
2692               EventManager.clearPersistentListener(
2693                 extension,
2694                 module,
2695                 event,
2696                 key,
2697                 primeId
2698               );
2699             }
2700           }
2701         }
2702       }
2703     }
2704   }
2706   // Record the fact that there is a listener for the given event in
2707   // the given extension.  `args` is an Array containing any extra
2708   // arguments that were passed to addListener().
2709   static savePersistentListener(extension, module, event, args = []) {
2710     EventManager._initPersistentListeners(extension);
2711     let key = uneval(args);
2712     const eventEntry = extension.persistentListeners.get(module).get(event);
2714     let primeId;
2715     if (!eventEntry.has(key)) {
2716       // when writing, only args are written, other properties are dropped
2717       primeId = 0;
2718       eventEntry.set(key, {
2719         params: args,
2720         listeners: [{ added: true, primeId }],
2721         nextPrimeId: 1,
2722       });
2723     } else {
2724       const keyEntry = eventEntry.get(key);
2725       primeId = keyEntry.nextPrimeId;
2726       keyEntry.listeners.push({ added: true, primeId });
2727       keyEntry.nextPrimeId = primeId + 1;
2728     }
2730     EventManager._writePersistentListeners(extension);
2731     return [module, event, key, primeId];
2732   }
2734   // Remove the record for the given event listener from the extension's
2735   // startup data.  `key` must be a string, the result of calling uneval()
2736   // on the array of extra arguments originally passed to addListener().
2737   static clearPersistentListener(
2738     extension,
2739     module,
2740     event,
2741     key = uneval([]),
2742     primeId = undefined
2743   ) {
2744     let eventEntry = extension.persistentListeners.get(module).get(event);
2746     let keyEntry = eventEntry.get(key);
2748     if (primeId != undefined && keyEntry) {
2749       keyEntry.listeners = keyEntry.listeners.filter(
2750         listener => listener.primeId !== primeId
2751       );
2752     }
2754     if (primeId == undefined || keyEntry?.listeners.length === 0) {
2755       eventEntry.delete(key);
2756       if (eventEntry.size == 0) {
2757         let moduleEntry = extension.persistentListeners.get(module);
2758         moduleEntry.delete(event);
2759         if (moduleEntry.size == 0) {
2760           extension.persistentListeners.delete(module);
2761         }
2762       }
2763     }
2765     EventManager._writePersistentListeners(extension);
2766   }
2768   addListener(callback, ...args) {
2769     if (this.unregister.has(callback)) {
2770       return;
2771     }
2772     this.context.logActivity("api_call", `${this.name}.addListener`, { args });
2774     let shouldFire = () => {
2775       if (this.context.unloaded) {
2776         dump(`${this.name} event fired after context unloaded.\n`);
2777       } else if (!this.context.active) {
2778         dump(`${this.name} event fired while context is inactive.\n`);
2779       } else if (this.unregister.has(callback)) {
2780         return true;
2781       }
2782       return false;
2783     };
2785     let { extension } = this.context;
2786     const resetIdle = () => {
2787       if (this.resetIdleOnEvent) {
2788         extension?.emit("background-script-reset-idle", {
2789           reason: "event",
2790           eventName: this.name,
2791         });
2792       }
2793     };
2795     let fire = {
2796       // Bug 1754866 fire.sync doesn't match documentation.
2797       sync: (...args) => {
2798         if (shouldFire()) {
2799           resetIdle();
2800           let result = this.context.applySafe(callback, args);
2801           this.context.logActivity("api_event", this.name, { args, result });
2802           return result;
2803         }
2804       },
2805       async: (...args) => {
2806         return Promise.resolve().then(() => {
2807           if (shouldFire()) {
2808             resetIdle();
2809             let result = this.context.applySafe(callback, args);
2810             this.context.logActivity("api_event", this.name, { args, result });
2811             return result;
2812           }
2813         });
2814       },
2815       raw: (...args) => {
2816         if (!shouldFire()) {
2817           throw new Error("Called raw() on unloaded/inactive context");
2818         }
2819         resetIdle();
2820         let result = Reflect.apply(callback, null, args);
2821         this.context.logActivity("api_event", this.name, { args, result });
2822         return result;
2823       },
2824       asyncWithoutClone: (...args) => {
2825         return Promise.resolve().then(() => {
2826           if (shouldFire()) {
2827             resetIdle();
2828             let result = this.context.applySafeWithoutClone(callback, args);
2829             this.context.logActivity("api_event", this.name, { args, result });
2830             return result;
2831           }
2832         });
2833       },
2834     };
2836     let { module, event } = this;
2838     let unregister = null;
2839     let recordStartupData = false;
2841     // If this is a persistent event, check for a listener that was already
2842     // created during startup.  If there is one, use it and don't create a
2843     // new one.
2844     if (this.canPersistEvents) {
2845       // Once a background is started, listenerPromises is set to null. At
2846       // that point, we stop recording startup data.
2847       recordStartupData = !!this.context.listenerPromises;
2849       let key = uneval(args);
2850       EventManager._initPersistentListeners(extension);
2851       let keyEntry = extension.persistentListeners
2852         .get(module)
2853         .get(event)
2854         .get(key);
2856       // Get the first persistent listener which matches the module, event and extra arguments
2857       // and not added back by the extension yet, the persistent listener found may be either
2858       // primed or not (in particular API Events that belongs to APIs that should not be blocking
2859       // startup may have persistent listeners that are not primed during the first execution
2860       // of the background context happening as part of the applications startup, whereas they
2861       // will be primed when the background context will be suspended on the idle timeout).
2862       let listener = keyEntry?.listeners.find(listener => !listener.added);
2863       if (listener) {
2864         // During startup only a subset of persisted listeners are primed.  As
2865         // well, each API determines whether to prime a specific listener.
2866         let { primed } = listener;
2867         if (primed) {
2868           listener.primed = null;
2870           primed.convert(fire, this.context);
2871           unregister = primed.unregister;
2873           for (let evt of primed.pendingEvents) {
2874             evt.resolve(fire.async(...evt.args));
2875           }
2876         }
2877         listener.added = true;
2879         recordStartupData = false;
2880         this.remove.set(callback, () => {
2881           EventManager.clearPersistentListener(
2882             extension,
2883             module,
2884             event,
2885             uneval(args),
2886             listener.primeId
2887           );
2888         });
2889       }
2890     }
2892     if (!unregister) {
2893       unregister = this.register(fire, ...args);
2894     }
2896     this.unregister.set(callback, unregister);
2897     this.context.callOnClose(this);
2899     // If this is a new listener for a persistent event, record
2900     // the details for subsequent startups.
2901     if (recordStartupData) {
2902       const [, , , /* _module */ /* _event */ /* _key */ primeId] =
2903         EventManager.savePersistentListener(extension, module, event, args);
2904       this.remove.set(callback, () => {
2905         EventManager.clearPersistentListener(
2906           extension,
2907           module,
2908           event,
2909           uneval(args),
2910           primeId
2911         );
2912       });
2913     }
2914   }
2916   removeListener(callback, clearPersistentListener = true) {
2917     if (!this.unregister.has(callback)) {
2918       return;
2919     }
2920     this.context.logActivity("api_call", `${this.name}.removeListener`, {
2921       args: [],
2922     });
2924     let unregister = this.unregister.get(callback);
2925     this.unregister.delete(callback);
2926     try {
2927       unregister();
2928     } catch (e) {
2929       Cu.reportError(e);
2930     }
2932     if (clearPersistentListener && this.remove.has(callback)) {
2933       let cleanup = this.remove.get(callback);
2934       this.remove.delete(callback);
2935       cleanup();
2936     }
2938     if (this.unregister.size == 0) {
2939       this.context.forgetOnClose(this);
2940     }
2941   }
2943   hasListener(callback) {
2944     return this.unregister.has(callback);
2945   }
2947   revoke() {
2948     for (let callback of this.unregister.keys()) {
2949       this.removeListener(callback, false);
2950     }
2951   }
2953   close() {
2954     this.revoke();
2955   }
2957   api() {
2958     return {
2959       addListener: (...args) => this.addListener(...args),
2960       removeListener: (...args) => this.removeListener(...args),
2961       hasListener: (...args) => this.hasListener(...args),
2962       setUserInput: this.inputHandling,
2963       [lazy.Schemas.REVOKE]: () => this.revoke(),
2964     };
2965   }
2968 // Simple API for event listeners where events never fire.
2969 function ignoreEvent(context, name) {
2970   return {
2971     addListener: function (callback) {
2972       let id = context.extension.id;
2973       let frame = Components.stack.caller;
2974       let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`;
2975       let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
2976         Ci.nsIScriptError
2977       );
2978       scriptError.init(
2979         msg,
2980         frame.filename,
2981         null,
2982         frame.lineNumber,
2983         frame.columnNumber,
2984         Ci.nsIScriptError.warningFlag,
2985         "content javascript"
2986       );
2987       Services.console.logMessage(scriptError);
2988     },
2989     removeListener: function (callback) {},
2990     hasListener: function (callback) {},
2991   };
2994 const stylesheetMap = new DefaultMap(url => {
2995   let uri = Services.io.newURI(url);
2996   return lazy.styleSheetService.preloadSheet(
2997     uri,
2998     lazy.styleSheetService.AGENT_SHEET
2999   );
3003  * Updates the in-memory representation of extension host permissions, i.e.
3004  * policy.allowedOrigins.
3006  * @param {WebExtensionPolicy} policy
3007  *        A policy. All MatchPattern instances in policy.allowedOrigins are
3008  *        expected to have been constructed with ignorePath: true.
3009  * @param {string[]} origins
3010  *        A list of already-normalized origins, equivalent to using the
3011  *        MatchPattern constructor with ignorePath: true.
3012  * @param {boolean} isAdd
3013  *        Whether to add instead of removing the host permissions.
3014  */
3015 function updateAllowedOrigins(policy, origins, isAdd) {
3016   if (!origins.length) {
3017     // Nothing to modify.
3018     return;
3019   }
3020   let patternMap = new Map();
3021   for (let pattern of policy.allowedOrigins.patterns) {
3022     patternMap.set(pattern.pattern, pattern);
3023   }
3024   if (!isAdd) {
3025     for (let origin of origins) {
3026       patternMap.delete(origin);
3027     }
3028   } else {
3029     // In the parent process, policy.extension.restrictSchemes is available.
3030     // In the content process, we need to check the mozillaAddons permission,
3031     // which is only available if approved by the parent.
3032     const restrictSchemes =
3033       policy.extension?.restrictSchemes ??
3034       policy.hasPermission("mozillaAddons");
3035     for (let origin of origins) {
3036       if (patternMap.has(origin)) {
3037         continue;
3038       }
3039       patternMap.set(
3040         origin,
3041         new MatchPattern(origin, { restrictSchemes, ignorePath: true })
3042       );
3043     }
3044   }
3045   // patternMap contains only MatchPattern instances, so we don't need to set
3046   // the options parameter (with restrictSchemes, etc.) since that is only used
3047   // if the input is a string.
3048   policy.allowedOrigins = new MatchPatternSet(Array.from(patternMap.values()));
3051 export var ExtensionCommon = {
3052   BaseContext,
3053   CanOfAPIs,
3054   EventManager,
3055   ExtensionAPI,
3056   ExtensionAPIPersistent,
3057   EventEmitter,
3058   LocalAPIImplementation,
3059   LocaleData,
3060   NoCloneSpreadArgs,
3061   SchemaAPIInterface,
3062   SchemaAPIManager,
3063   SpreadArgs,
3064   checkLoadURI,
3065   checkLoadURL,
3066   defineLazyGetter,
3067   redefineGetter,
3068   getConsole,
3069   ignoreEvent,
3070   instanceOf,
3071   makeWidgetId,
3072   normalizeTime,
3073   runSafeSyncWithoutClone,
3074   stylesheetMap,
3075   updateAllowedOrigins,
3076   withHandlingUserInput,
3078   MultiAPIManager,
3079   LazyAPIManager,