Bug 1885565 - Part 1: Add mozac_ic_avatar_circle_24 to ui-icons r=android-reviewers...
[gecko.git] / toolkit / components / extensions / ExtensionParent.sys.mjs
blobb4812a702a2acc92de2909e78e1efadea182015b
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 code for managing APIs that need to run in the
9  * parent process, and handles the parent side of operations that need
10  * to be proxied from ExtensionChild.sys.mjs.
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   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
21   AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
22   BroadcastConduit: "resource://gre/modules/ConduitsParent.sys.mjs",
23   DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
24   DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
25   ExtensionActivityLog: "resource://gre/modules/ExtensionActivityLog.sys.mjs",
26   ExtensionData: "resource://gre/modules/Extension.sys.mjs",
27   GeckoViewConnection: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
28   MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.sys.mjs",
29   NativeApp: "resource://gre/modules/NativeMessaging.sys.mjs",
30   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
31   Schemas: "resource://gre/modules/Schemas.sys.mjs",
32   getErrorNameForTelemetry: "resource://gre/modules/ExtensionTelemetry.sys.mjs",
33 });
35 XPCOMUtils.defineLazyServiceGetters(lazy, {
36   aomStartup: [
37     "@mozilla.org/addons/addon-manager-startup;1",
38     "amIAddonManagerStartup",
39   ],
40 });
42 import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
43 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
45 const DUMMY_PAGE_URI = Services.io.newURI(
46   "chrome://extensions/content/dummy.xhtml"
49 var { BaseContext, CanOfAPIs, SchemaAPIManager, SpreadArgs, redefineGetter } =
50   ExtensionCommon;
52 var {
53   DefaultMap,
54   DefaultWeakMap,
55   ExtensionError,
56   promiseDocumentLoaded,
57   promiseEvent,
58   promiseObserved,
59 } = ExtensionUtils;
61 const ERROR_NO_RECEIVERS =
62   "Could not establish connection. Receiving end does not exist.";
64 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
65 const CATEGORY_EXTENSION_MODULES = "webextension-modules";
66 const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
67 const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
69 let schemaURLs = new Set();
71 schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
73 let GlobalManager;
74 let ParentAPIManager;
76 function verifyActorForContext(actor, context) {
77   if (JSWindowActorParent.isInstance(actor)) {
78     let target = actor.browsingContext.top.embedderElement;
79     if (context.parentMessageManager !== target.messageManager) {
80       throw new Error("Got message on unexpected message manager");
81     }
82   } else if (JSProcessActorParent.isInstance(actor)) {
83     if (actor.manager.remoteType !== context.extension.remoteType) {
84       throw new Error("Got message from unexpected process");
85     }
86   }
89 // This object loads the ext-*.js scripts that define the extension API.
90 let apiManager = new (class extends SchemaAPIManager {
91   constructor() {
92     super("main", lazy.Schemas);
93     this.initialized = null;
95     /* eslint-disable mozilla/balanced-listeners */
96     this.on("startup", (e, extension) => {
97       return extension.apiManager.onStartup(extension);
98     });
100     this.on("update", async (e, { id, resourceURI, isPrivileged }) => {
101       let modules = this.eventModules.get("update");
102       if (modules.size == 0) {
103         return;
104       }
106       let extension = new lazy.ExtensionData(resourceURI, isPrivileged);
107       await extension.loadManifest();
109       return Promise.all(
110         Array.from(modules).map(async apiName => {
111           let module = await this.asyncLoadModule(apiName);
112           module.onUpdate(id, extension.manifest);
113         })
114       );
115     });
117     this.on("uninstall", (e, { id }) => {
118       let modules = this.eventModules.get("uninstall");
119       return Promise.all(
120         Array.from(modules).map(async apiName => {
121           let module = await this.asyncLoadModule(apiName);
122           return module.onUninstall(id);
123         })
124       );
125     });
126     /* eslint-enable mozilla/balanced-listeners */
128     // Handle any changes that happened during startup
129     let disabledIds = lazy.AddonManager.getStartupChanges(
130       lazy.AddonManager.STARTUP_CHANGE_DISABLED
131     );
132     if (disabledIds.length) {
133       this._callHandlers(disabledIds, "disable", "onDisable");
134     }
136     let uninstalledIds = lazy.AddonManager.getStartupChanges(
137       lazy.AddonManager.STARTUP_CHANGE_UNINSTALLED
138     );
139     if (uninstalledIds.length) {
140       this._callHandlers(uninstalledIds, "uninstall", "onUninstall");
141     }
142   }
144   getModuleJSONURLs() {
145     return Array.from(
146       Services.catMan.enumerateCategory(CATEGORY_EXTENSION_MODULES),
147       ({ value }) => value
148     );
149   }
151   // Loads all the ext-*.js scripts currently registered.
152   lazyInit() {
153     if (this.initialized) {
154       return this.initialized;
155     }
157     let modulesPromise = StartupCache.other.get(["parentModules"], () =>
158       this.loadModuleJSON(this.getModuleJSONURLs())
159     );
161     let scriptURLs = [];
162     for (let { value } of Services.catMan.enumerateCategory(
163       CATEGORY_EXTENSION_SCRIPTS
164     )) {
165       scriptURLs.push(value);
166     }
168     let promise = (async () => {
169       let scripts = await Promise.all(
170         scriptURLs.map(url => ChromeUtils.compileScript(url))
171       );
173       this.initModuleData(await modulesPromise);
175       this.initGlobal();
176       for (let script of scripts) {
177         script.executeInGlobal(this.global);
178       }
180       // Load order matters here. The base manifest defines types which are
181       // extended by other schemas, so needs to be loaded first.
182       return lazy.Schemas.load(BASE_SCHEMA).then(() => {
183         let promises = [];
184         for (let { value } of Services.catMan.enumerateCategory(
185           CATEGORY_EXTENSION_SCHEMAS
186         )) {
187           promises.push(lazy.Schemas.load(value));
188         }
189         for (let [url, { content }] of this.schemaURLs) {
190           promises.push(lazy.Schemas.load(url, content));
191         }
192         for (let url of schemaURLs) {
193           promises.push(lazy.Schemas.load(url));
194         }
195         return Promise.all(promises).then(() => {
196           lazy.Schemas.updateSharedSchemas();
197         });
198       });
199     })();
201     Services.mm.addMessageListener("Extension:GetFrameData", this);
203     this.initialized = promise;
204     return this.initialized;
205   }
207   receiveMessage({ target }) {
208     let data = GlobalManager.frameData.get(target) || {};
209     Object.assign(data, this.global.tabTracker.getBrowserData(target));
210     return data;
211   }
213   // Call static handlers for the given event on the given extension ids,
214   // and set up a shutdown blocker to ensure they all complete.
215   _callHandlers(ids, event, method) {
216     let promises = Array.from(this.eventModules.get(event))
217       .map(async modName => {
218         let module = await this.asyncLoadModule(modName);
219         return ids.map(id => module[method](id));
220       })
221       .flat();
222     if (event === "disable") {
223       promises.push(...ids.map(id => this.emit("disable", id)));
224     }
225     if (event === "enabling") {
226       promises.push(...ids.map(id => this.emit("enabling", id)));
227     }
229     lazy.AsyncShutdown.profileBeforeChange.addBlocker(
230       `Extension API ${event} handlers for ${ids.join(",")}`,
231       Promise.all(promises)
232     );
233   }
234 })();
237  * @typedef {object} ParentPort
238  * @property {boolean} [native]
239  * @property {string} [senderChildId]
240  * @property {function(StructuredCloneHolder): any} onPortMessage
241  * @property {Function} onPortDisconnect
242  */
244 // Receives messages related to the extension messaging API and forwards them
245 // to relevant child messengers.  Also handles Native messaging and GeckoView.
246 /** @typedef {typeof ProxyMessenger} NativeMessenger */
247 const ProxyMessenger = {
248   /** @type {Map<number, Partial<ParentPort>&Promise<ParentPort>>} */
249   ports: new Map(),
251   init() {
252     this.conduit = new lazy.BroadcastConduit(ProxyMessenger, {
253       id: "ProxyMessenger",
254       reportOnClosed: "portId",
255       recv: ["PortConnect", "PortMessage", "NativeMessage", "RuntimeMessage"],
256       cast: ["PortConnect", "PortMessage", "PortDisconnect", "RuntimeMessage"],
257     });
258   },
260   openNative(nativeApp, sender) {
261     let context = ParentAPIManager.getContextById(sender.childId);
262     if (context.extension.hasPermission("geckoViewAddons")) {
263       return new lazy.GeckoViewConnection(
264         this.getSender(context.extension, sender),
265         sender.actor.browsingContext.top.embedderElement,
266         nativeApp,
267         context.extension.hasPermission("nativeMessagingFromContent")
268       );
269     } else if (sender.verified) {
270       return new lazy.NativeApp(context, nativeApp);
271     }
272     sender = this.getSender(context.extension, sender);
273     throw new Error(`Native messaging not allowed: ${JSON.stringify(sender)}`);
274   },
276   recvNativeMessage({ nativeApp, holder }, { sender }) {
277     const app = this.openNative(nativeApp, sender);
279     // Track in-flight NativeApp sendMessage requests as
280     // a NativeApp port destroyed when the request
281     // has been handled.
282     const promiseSendMessage = app.sendMessage(holder);
283     const sendMessagePort = {
284       native: true,
285       senderChildId: sender.childId,
286     };
287     this.trackNativeAppPort(sendMessagePort);
288     const untrackSendMessage = () => this.untrackNativeAppPort(sendMessagePort);
289     promiseSendMessage.then(untrackSendMessage, untrackSendMessage);
291     return promiseSendMessage;
292   },
294   getSender(extension, source) {
295     let sender = {
296       contextId: source.id,
297       id: source.extensionId,
298       envType: source.envType,
299       url: source.url,
300     };
302     if (JSWindowActorParent.isInstance(source.actor)) {
303       let browser = source.actor.browsingContext.top.embedderElement;
304       let data =
305         browser && apiManager.global.tabTracker.getBrowserData(browser);
306       if (data?.tabId > 0) {
307         sender.tab = extension.tabManager.get(data.tabId, null)?.convert();
308         // frameId is documented to only be set if sender.tab is set.
309         sender.frameId = source.frameId;
310       }
311     }
313     return sender;
314   },
316   getTopBrowsingContextId(tabId) {
317     // If a tab alredy has content scripts, no need to check private browsing.
318     let tab = apiManager.global.tabTracker.getTab(tabId, null);
319     if (!tab || (tab.browser || tab).getAttribute("pending") === "true") {
320       // No receivers in discarded tabs, so bail early to keep the browser lazy.
321       throw new ExtensionError(ERROR_NO_RECEIVERS);
322     }
323     let browser = tab.linkedBrowser || tab.browser;
324     return browser.browsingContext.id;
325   },
327   // TODO: Rework/simplify this and getSender/getTopBC after bug 1580766.
328   async normalizeArgs(arg, sender) {
329     arg.extensionId = arg.extensionId || sender.extensionId;
330     let extension = GlobalManager.extensionMap.get(arg.extensionId);
331     if (!extension) {
332       return Promise.reject({ message: ERROR_NO_RECEIVERS });
333     }
334     // TODO bug 1852317: This should not be unconditional.
335     await extension.wakeupBackground?.();
337     arg.sender = this.getSender(extension, sender);
338     arg.topBC = arg.tabId && this.getTopBrowsingContextId(arg.tabId);
339     return arg.tabId ? "tab" : "messenger";
340   },
342   async recvRuntimeMessage(arg, { sender }) {
343     arg.firstResponse = true;
344     let kind = await this.normalizeArgs(arg, sender);
345     let result = await this.conduit.castRuntimeMessage(kind, arg);
346     if (!result) {
347       // "throw new ExtensionError" cannot be used because then the stack of the
348       // sendMessage call would not be added to the error object generated by
349       // context.normalizeError. Test coverage by test_ext_error_location.js.
350       return Promise.reject({ message: ERROR_NO_RECEIVERS });
351     }
352     return result.value;
353   },
355   async recvPortConnect(arg, { sender }) {
356     if (arg.native) {
357       let port = this.openNative(arg.name, sender).onConnect(arg.portId, this);
358       port.senderChildId = sender.childId;
359       port.native = true;
360       this.ports.set(arg.portId, port);
361       this.trackNativeAppPort(port);
362       return;
363     }
365     // PortMessages that follow will need to wait for the port to be opened.
366     /** @type {callback} */
367     let resolvePort;
368     this.ports.set(arg.portId, new Promise(res => (resolvePort = res)));
370     let kind = await this.normalizeArgs(arg, sender);
371     let all = await this.conduit.castPortConnect(kind, arg);
372     resolvePort();
374     // If there are no active onConnect listeners.
375     if (!all.some(x => x.value)) {
376       throw new ExtensionError(ERROR_NO_RECEIVERS);
377     }
378   },
380   async recvPortMessage({ holder }, { sender }) {
381     if (sender.native) {
382       // If the nativeApp port connect fails (e.g. if triggered by a content
383       // script), the portId may not be in the map (because it did throw in
384       // the openNative method).
385       return this.ports.get(sender.portId)?.onPortMessage(holder);
386     }
387     // NOTE: the following await make sure we await for promised ports
388     // (ports that were not yet open when added to the Map,
389     // see recvPortConnect).
390     await this.ports.get(sender.portId);
391     this.sendPortMessage(sender.portId, holder, !sender.source);
392   },
394   recvConduitClosed(sender) {
395     let app = this.ports.get(sender.portId);
396     if (this.ports.delete(sender.portId) && sender.native) {
397       this.untrackNativeAppPort(app);
398       return app.onPortDisconnect();
399     }
400     this.sendPortDisconnect(sender.portId, null, !sender.source);
401   },
403   sendPortMessage(portId, holder, source = true) {
404     this.conduit.castPortMessage("port", { portId, source, holder });
405   },
407   sendPortDisconnect(portId, error, source = true) {
408     let port = this.ports.get(portId);
409     this.untrackNativeAppPort(port);
410     this.conduit.castPortDisconnect("port", { portId, source, error });
411     this.ports.delete(portId);
412   },
414   trackNativeAppPort(port) {
415     if (!port?.native) {
416       return;
417     }
419     try {
420       let context = ParentAPIManager.getContextById(port.senderChildId);
421       context?.trackNativeAppPort(port);
422     } catch {
423       // getContextById will throw if the context has been destroyed
424       // in the meantime.
425     }
426   },
428   untrackNativeAppPort(port) {
429     if (!port?.native) {
430       return;
431     }
433     try {
434       let context = ParentAPIManager.getContextById(port.senderChildId);
435       context?.untrackNativeAppPort(port);
436     } catch {
437       // getContextById will throw if the context has been destroyed
438       // in the meantime.
439     }
440   },
442 ProxyMessenger.init();
444 // Responsible for loading extension APIs into the right globals.
445 GlobalManager = {
446   // Map[extension ID -> Extension]. Determines which extension is
447   // responsible for content under a particular extension ID.
448   extensionMap: new Map(),
449   initialized: false,
451   /** @type {WeakMap<Browser, object>} Extension Context init data. */
452   frameData: new WeakMap(),
454   init(extension) {
455     if (this.extensionMap.size == 0) {
456       apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
457       this.initialized = true;
458     }
459     this.extensionMap.set(extension.id, extension);
460   },
462   uninit(extension) {
463     this.extensionMap.delete(extension.id);
465     if (this.extensionMap.size == 0 && this.initialized) {
466       apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
467       this.initialized = false;
468     }
469   },
471   _onExtensionBrowser(type, browser, data = {}) {
472     data.viewType = browser.getAttribute("webextension-view-type");
473     if (data.viewType) {
474       GlobalManager.frameData.set(browser, data);
475     }
476   },
478   getExtension(extensionId) {
479     return this.extensionMap.get(extensionId);
480   },
484  * The proxied parent side of a context in ExtensionChild.sys.mjs, for the
485  * parent side of a proxied API.
486  */
487 class ProxyContextParent extends BaseContext {
488   constructor(envType, extension, params, browsingContext, principal) {
489     super(envType, extension);
491     this.childId = params.childId;
492     this.uri = Services.io.newURI(params.url);
494     this.incognito = params.incognito;
496     this.listenerPromises = new Set();
498     // browsingContext is null when subclassed by BackgroundWorkerContextParent.
499     const xulBrowser = browsingContext?.top.embedderElement;
500     // This message manager is used by ParentAPIManager to send messages and to
501     // close the ProxyContext if the underlying message manager closes. This
502     // message manager object may change when `xulBrowser` swaps docshells, e.g.
503     // when a tab is moved to a different window.
504     // TODO: Is xulBrowser correct for ContentScriptContextParent? Messages
505     // through the xulBrowser won't reach cross-process iframes.
506     this.messageManagerProxy =
507       xulBrowser && new lazy.MessageManagerProxy(xulBrowser);
509     Object.defineProperty(this, "principal", {
510       value: principal,
511       enumerable: true,
512       configurable: true,
513     });
515     this.listenerProxies = new Map();
517     this.pendingEventBrowser = null;
518     this.callContextData = null;
520     // Set of active NativeApp ports.
521     this.activeNativePorts = new WeakSet();
523     // Set of pending queryRunListener promises.
524     this.runListenerPromises = new Set();
526     apiManager.emit("proxy-context-load", this);
527   }
529   get isProxyContextParent() {
530     return true;
531   }
533   trackRunListenerPromise(runListenerPromise) {
534     if (
535       // The extension was already shutdown.
536       !this.extension ||
537       // Not a non persistent background script context.
538       !this.isBackgroundContext ||
539       this.extension.persistentBackground
540     ) {
541       return;
542     }
543     const clearFromSet = () =>
544       this.runListenerPromises.delete(runListenerPromise);
545     runListenerPromise.then(clearFromSet, clearFromSet);
546     this.runListenerPromises.add(runListenerPromise);
547   }
549   clearPendingRunListenerPromises() {
550     this.runListenerPromises.clear();
551   }
553   get pendingRunListenerPromisesCount() {
554     return this.runListenerPromises.size;
555   }
557   trackNativeAppPort(port) {
558     if (
559       // Not a native port.
560       !port?.native ||
561       // Not a non persistent background script context.
562       !this.isBackgroundContext ||
563       this.extension?.persistentBackground ||
564       // The extension was already shutdown.
565       !this.extension
566     ) {
567       return;
568     }
569     this.activeNativePorts.add(port);
570   }
572   untrackNativeAppPort(port) {
573     this.activeNativePorts.delete(port);
574   }
576   get hasActiveNativeAppPorts() {
577     return !!ChromeUtils.nondeterministicGetWeakSetKeys(this.activeNativePorts)
578       .length;
579   }
581   /**
582    * Call the `callable` parameter with `context.callContextData` set to the value passed
583    * as the first parameter of this method.
584    *
585    * `context.callContextData` is expected to:
586    * - don't be set when context.withCallContextData is being called
587    * - be set back to null right after calling the `callable` function, without
588    *   awaiting on any async code that the function may be running internally
589    *
590    * The callable method itself is responsabile of eventually retrieve the value initially set
591    * on the `context.callContextData` before any code executed asynchronously (e.g. from a
592    * callback or after awaiting internally on a promise if the `callable` function was async).
593    *
594    * @param {object} callContextData
595    * @param {boolean} callContextData.isHandlingUserInput
596    * @param {Function} callable
597    *
598    * @returns {any} Returns the value returned by calling the `callable` method.
599    */
600   withCallContextData({ isHandlingUserInput }, callable) {
601     if (this.callContextData) {
602       Cu.reportError(
603         `Unexpected pre-existing callContextData on "${this.extension?.policy.debugName}" contextId ${this.contextId}`
604       );
605     }
607     try {
608       this.callContextData = {
609         isHandlingUserInput,
610       };
611       return callable();
612     } finally {
613       this.callContextData = null;
614     }
615   }
617   async withPendingBrowser(browser, callable) {
618     let savedBrowser = this.pendingEventBrowser;
619     this.pendingEventBrowser = browser;
620     try {
621       let result = await callable();
622       return result;
623     } finally {
624       this.pendingEventBrowser = savedBrowser;
625     }
626   }
628   logActivity() {
629     // The base class will throw so we catch any subclasses that do not implement.
630     // We do not want to throw here, but we also do not log here.
631   }
633   get cloneScope() {
634     return this.sandbox;
635   }
637   applySafe(callback, args) {
638     // There's no need to clone when calling listeners for a proxied
639     // context.
640     return this.applySafeWithoutClone(callback, args);
641   }
643   get xulBrowser() {
644     return this.messageManagerProxy?.eventTarget;
645   }
647   get parentMessageManager() {
648     // TODO bug 1595186: Replace use of parentMessageManager.
649     return this.messageManagerProxy?.messageManager;
650   }
652   shutdown() {
653     this.unload();
654   }
656   unload() {
657     if (this.unloaded) {
658       return;
659     }
661     this.messageManagerProxy?.dispose();
663     super.unload();
664     apiManager.emit("proxy-context-unload", this);
665   }
667   get apiCan() {
668     const apiCan = new CanOfAPIs(this, this.extension.apiManager, {});
669     return redefineGetter(this, "apiCan", apiCan);
670   }
672   get apiObj() {
673     return redefineGetter(this, "apiObj", this.apiCan.root);
674   }
676   get sandbox() {
677     // Note: Blob and URL globals are used in ext-contentScripts.js.
678     const sandbox = Cu.Sandbox(this.principal, {
679       sandboxName: this.uri.spec,
680       wantGlobalProperties: ["Blob", "URL"],
681     });
682     return redefineGetter(this, "sandbox", sandbox);
683   }
687  * The parent side of proxied API context for extension content script
688  * running in ExtensionContent.sys.mjs.
689  */
690 class ContentScriptContextParent extends ProxyContextParent {}
693  * The parent side of proxied API context for extension page, such as a
694  * background script, a tab page, or a popup, running in
695  * ExtensionChild.sys.mjs.
696  */
697 class ExtensionPageContextParent extends ProxyContextParent {
698   constructor(envType, extension, params, browsingContext) {
699     super(envType, extension, params, browsingContext, extension.principal);
701     this.viewType = params.viewType;
702     this.isTopContext = browsingContext.top === browsingContext;
704     this.extension.views.add(this);
706     extension.emit("extension-proxy-context-load", this);
707   }
709   // The window that contains this context. This may change due to moving tabs.
710   get appWindow() {
711     let win = this.xulBrowser.ownerGlobal;
712     return win.browsingContext.topChromeWindow;
713   }
715   get currentWindow() {
716     if (this.viewType !== "background") {
717       return this.appWindow;
718     }
719     return undefined;
720   }
722   get tabId() {
723     let { tabTracker } = apiManager.global;
724     let data = tabTracker.getBrowserData(this.xulBrowser);
725     if (data.tabId >= 0) {
726       return data.tabId;
727     }
728     return undefined;
729   }
731   unload() {
732     super.unload();
733     this.extension.views.delete(this);
734   }
736   shutdown() {
737     apiManager.emit("page-shutdown", this);
738     super.shutdown();
739   }
743  * The parent side of proxied API context for devtools extension page, such as a
744  * devtools pages and panels running in ExtensionChild.sys.mjs.
745  */
746 class DevToolsExtensionPageContextParent extends ExtensionPageContextParent {
747   constructor(...params) {
748     super(...params);
750     // Set all attributes that are lazily defined to `null` here.
751     //
752     // Note that we can't do that for `this._devToolsToolbox` because it will
753     // be defined when calling our parent constructor and so would override it back to `null`.
754     this._devToolsCommands = null;
755     this._onNavigatedListeners = null;
757     this._onResourceAvailable = this._onResourceAvailable.bind(this);
758   }
760   set devToolsToolbox(toolbox) {
761     if (this._devToolsToolbox) {
762       throw new Error("Cannot set the context DevTools toolbox twice");
763     }
765     this._devToolsToolbox = toolbox;
766   }
768   get devToolsToolbox() {
769     return this._devToolsToolbox;
770   }
772   async addOnNavigatedListener(listener) {
773     if (!this._onNavigatedListeners) {
774       this._onNavigatedListeners = new Set();
776       await this.devToolsToolbox.resourceCommand.watchResources(
777         [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
778         {
779           onAvailable: this._onResourceAvailable,
780           ignoreExistingResources: true,
781         }
782       );
783     }
785     this._onNavigatedListeners.add(listener);
786   }
788   removeOnNavigatedListener(listener) {
789     if (this._onNavigatedListeners) {
790       this._onNavigatedListeners.delete(listener);
791     }
792   }
794   /**
795    * The returned "commands" object, exposing modules implemented from devtools/shared/commands.
796    * Each attribute being a static interface to communicate with the server backend.
797    *
798    * @returns {Promise<object>}
799    */
800   async getDevToolsCommands() {
801     // Ensure that we try to instantiate a commands only once,
802     // even if createCommandsForTabForWebExtension is async.
803     if (this._devToolsCommandsPromise) {
804       return this._devToolsCommandsPromise;
805     }
806     if (this._devToolsCommands) {
807       return this._devToolsCommands;
808     }
810     this._devToolsCommandsPromise = (async () => {
811       const commands =
812         await lazy.DevToolsShim.createCommandsForTabForWebExtension(
813           this.devToolsToolbox.commands.descriptorFront.localTab
814         );
815       await commands.targetCommand.startListening();
816       this._devToolsCommands = commands;
817       this._devToolsCommandsPromise = null;
818       return commands;
819     })();
820     return this._devToolsCommandsPromise;
821   }
823   unload() {
824     // Bail if the toolbox reference was already cleared.
825     if (!this.devToolsToolbox) {
826       return;
827     }
829     if (this._onNavigatedListeners) {
830       this.devToolsToolbox.resourceCommand.unwatchResources(
831         [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
832         { onAvailable: this._onResourceAvailable }
833       );
834     }
836     if (this._devToolsCommands) {
837       this._devToolsCommands.destroy();
838       this._devToolsCommands = null;
839     }
841     if (this._onNavigatedListeners) {
842       this._onNavigatedListeners.clear();
843       this._onNavigatedListeners = null;
844     }
846     this._devToolsToolbox = null;
848     super.unload();
849   }
851   async _onResourceAvailable(resources) {
852     for (const resource of resources) {
853       const { targetFront } = resource;
854       if (targetFront.isTopLevel && resource.name === "dom-complete") {
855         for (const listener of this._onNavigatedListeners) {
856           listener(targetFront.url);
857         }
858       }
859     }
860   }
864  * The parent side of proxied API context for extension background service
865  * worker script.
866  */
867 class BackgroundWorkerContextParent extends ProxyContextParent {
868   constructor(envType, extension, params) {
869     // TODO: split out from ProxyContextParent a base class that
870     // doesn't expect a browsingContext and one for contexts that are
871     // expected to have a browsingContext associated.
872     super(envType, extension, params, null, extension.principal);
874     this.viewType = params.viewType;
875     this.workerDescriptorId = params.workerDescriptorId;
877     this.extension.views.add(this);
879     extension.emit("extension-proxy-context-load", this);
880   }
883 ParentAPIManager = {
884   proxyContexts: new Map(),
886   init() {
887     // TODO: Bug 1595186 - remove/replace all usage of MessageManager below.
888     Services.obs.addObserver(this, "message-manager-close");
890     this.conduit = new lazy.BroadcastConduit(this, {
891       id: "ParentAPIManager",
892       reportOnClosed: "childId",
893       recv: [
894         "CreateProxyContext",
895         "ContextLoaded",
896         "APICall",
897         "AddListener",
898         "RemoveListener",
899       ],
900       send: ["CallResult"],
901       query: ["RunListener", "StreamFilterSuspendCancel"],
902     });
903   },
905   attachMessageManager(extension, processMessageManager) {
906     extension.parentMessageManager = processMessageManager;
907   },
909   async observe(subject, topic) {
910     if (topic === "message-manager-close") {
911       let mm = subject;
912       for (let [childId, context] of this.proxyContexts) {
913         if (context.parentMessageManager === mm) {
914           this.closeProxyContext(childId);
915         }
916       }
918       // Reset extension message managers when their child processes shut down.
919       for (let extension of GlobalManager.extensionMap.values()) {
920         if (extension.parentMessageManager === mm) {
921           extension.parentMessageManager = null;
922         }
923       }
924     }
925   },
927   shutdownExtension(extensionId, reason) {
928     if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(reason)) {
929       apiManager._callHandlers([extensionId], "disable", "onDisable");
930     }
932     for (let [childId, context] of this.proxyContexts) {
933       if (context.extension.id == extensionId) {
934         context.shutdown();
935         this.proxyContexts.delete(childId);
936       }
937     }
938   },
940   queryStreamFilterSuspendCancel(childId) {
941     return this.conduit.queryStreamFilterSuspendCancel(childId);
942   },
944   recvCreateProxyContext(data, { actor, sender }) {
945     let { envType, extensionId, childId, principal } = data;
947     if (this.proxyContexts.has(childId)) {
948       throw new Error(
949         "A WebExtension context with the given ID already exists!"
950       );
951     }
953     let extension = GlobalManager.getExtension(extensionId);
954     if (!extension) {
955       throw new Error(`No WebExtension found with ID ${extensionId}`);
956     }
958     let context;
959     if (envType == "addon_parent" || envType == "devtools_parent") {
960       if (!sender.verified) {
961         throw new Error(`Bad sender context envType: ${sender.envType}`);
962       }
964       let isBackgroundWorker = false;
965       if (JSWindowActorParent.isInstance(actor)) {
966         const target = actor.browsingContext.top.embedderElement;
967         let processMessageManager =
968           target.messageManager.processMessageManager ||
969           Services.ppmm.getChildAt(0);
971         if (!extension.parentMessageManager) {
972           if (target.remoteType === extension.remoteType) {
973             this.attachMessageManager(extension, processMessageManager);
974           }
975         }
977         if (processMessageManager !== extension.parentMessageManager) {
978           throw new Error(
979             "Attempt to create privileged extension parent from incorrect child process"
980           );
981         }
982       } else if (JSProcessActorParent.isInstance(actor)) {
983         if (actor.manager.remoteType !== extension.remoteType) {
984           throw new Error(
985             "Attempt to create privileged extension parent from incorrect child process"
986           );
987         }
989         if (envType !== "addon_parent") {
990           throw new Error(
991             `Unexpected envType ${envType} on an extension process actor`
992           );
993         }
994         if (data.viewType !== "background_worker") {
995           throw new Error(
996             `Unexpected viewType ${data.viewType} on an extension process actor`
997           );
998         }
999         isBackgroundWorker = true;
1000       } else {
1001         // Unreacheable: JSWindowActorParent and JSProcessActorParent are the
1002         // only actors.
1003         throw new Error(
1004           "Attempt to create privileged extension parent via incorrect actor"
1005         );
1006       }
1008       if (isBackgroundWorker) {
1009         context = new BackgroundWorkerContextParent(envType, extension, data);
1010       } else if (envType == "addon_parent") {
1011         context = new ExtensionPageContextParent(
1012           envType,
1013           extension,
1014           data,
1015           actor.browsingContext
1016         );
1017       } else if (envType == "devtools_parent") {
1018         context = new DevToolsExtensionPageContextParent(
1019           envType,
1020           extension,
1021           data,
1022           actor.browsingContext
1023         );
1024       }
1025     } else if (envType == "content_parent") {
1026       // Note: actor is always a JSWindowActorParent, with a browsingContext.
1027       context = new ContentScriptContextParent(
1028         envType,
1029         extension,
1030         data,
1031         actor.browsingContext,
1032         principal
1033       );
1034     } else {
1035       throw new Error(`Invalid WebExtension context envType: ${envType}`);
1036     }
1037     this.proxyContexts.set(childId, context);
1038   },
1040   recvContextLoaded(data, { actor }) {
1041     let context = this.getContextById(data.childId);
1042     verifyActorForContext(actor, context);
1043     const { extension } = context;
1044     extension.emit("extension-proxy-context-load:completed", context);
1045   },
1047   recvConduitClosed(sender) {
1048     this.closeProxyContext(sender.id);
1049   },
1051   closeProxyContext(childId) {
1052     let context = this.proxyContexts.get(childId);
1053     if (context) {
1054       context.unload();
1055       this.proxyContexts.delete(childId);
1056     }
1057   },
1059   /**
1060    * Call the given function and also log the call as appropriate
1061    * (i.e., with activity logging and/or profiler markers)
1062    *
1063    * @param {BaseContext} context The context making this call.
1064    * @param {object} data Additional data about the call.
1065    * @param {Function} callable The actual implementation to invoke.
1066    */
1067   async callAndLog(context, data, callable) {
1068     let { id } = context.extension;
1069     // If we were called via callParentAsyncFunction we don't want
1070     // to log again, check for the flag.
1071     const { alreadyLogged } = data.options || {};
1072     if (!alreadyLogged) {
1073       lazy.ExtensionActivityLog.log(
1074         id,
1075         context.viewType,
1076         "api_call",
1077         data.path,
1078         {
1079           args: data.args,
1080         }
1081       );
1082     }
1084     let start = Cu.now();
1085     try {
1086       return callable();
1087     } finally {
1088       ChromeUtils.addProfilerMarker(
1089         "ExtensionParent",
1090         { startTime: start },
1091         `${id}, api_call: ${data.path}`
1092       );
1093     }
1094   },
1096   async recvAPICall(data, { actor }) {
1097     let context = this.getContextById(data.childId);
1098     let target = actor.browsingContext?.top.embedderElement;
1100     verifyActorForContext(actor, context);
1102     let reply = result => {
1103       if (target && !context.parentMessageManager) {
1104         Services.console.logStringMessage(
1105           "Cannot send function call result: other side closed connection " +
1106             `(call data: ${uneval({ path: data.path, args: data.args })})`
1107         );
1108         return;
1109       }
1111       this.conduit.sendCallResult(data.childId, {
1112         childId: data.childId,
1113         callId: data.callId,
1114         path: data.path,
1115         ...result,
1116       });
1117     };
1119     try {
1120       if (
1121         context.isBackgroundContext &&
1122         !context.extension.persistentBackground
1123       ) {
1124         context.extension.emit("background-script-reset-idle", {
1125           reason: "parentApiCall",
1126           path: data.path,
1127         });
1128       }
1130       let args = data.args;
1131       let { isHandlingUserInput = false } = data.options || {};
1132       let pendingBrowser = context.pendingEventBrowser;
1133       let fun = await context.apiCan.asyncFindAPIPath(data.path);
1134       let result = this.callAndLog(context, data, () => {
1135         return context.withPendingBrowser(pendingBrowser, () =>
1136           context.withCallContextData({ isHandlingUserInput }, () =>
1137             fun(...args)
1138           )
1139         );
1140       });
1142       if (data.callId) {
1143         result = result || Promise.resolve();
1145         result.then(
1146           result => {
1147             result = result instanceof SpreadArgs ? [...result] : [result];
1149             let holder = new StructuredCloneHolder(
1150               `ExtensionParent/${context.extension.id}/recvAPICall/${data.path}`,
1151               null,
1152               result
1153             );
1155             reply({ result: holder });
1156           },
1157           error => {
1158             error = context.normalizeError(error);
1159             reply({
1160               error: { message: error.message, fileName: error.fileName },
1161             });
1162           }
1163         );
1164       }
1165     } catch (e) {
1166       if (data.callId) {
1167         let error = context.normalizeError(e);
1168         reply({ error: { message: error.message } });
1169       } else {
1170         Cu.reportError(e);
1171       }
1172     }
1173   },
1175   async recvAddListener(data, { actor }) {
1176     let context = this.getContextById(data.childId);
1178     verifyActorForContext(actor, context);
1180     let { childId, alreadyLogged = false } = data;
1181     let handlingUserInput = false;
1183     let listener = async (...listenerArgs) => {
1184       let startTime = Cu.now();
1185       // Extract urgentSend flag to avoid deserializing args holder later.
1186       let urgentSend = false;
1187       if (listenerArgs[0] && data.path.startsWith("webRequest.")) {
1188         urgentSend = listenerArgs[0].urgentSend;
1189         delete listenerArgs[0].urgentSend;
1190       }
1191       let runListenerPromise = this.conduit.queryRunListener(childId, {
1192         childId,
1193         handlingUserInput,
1194         listenerId: data.listenerId,
1195         path: data.path,
1196         urgentSend,
1197         get args() {
1198           return new StructuredCloneHolder(
1199             `ExtensionParent/${context.extension.id}/recvAddListener/${data.path}`,
1200             null,
1201             listenerArgs
1202           );
1203         },
1204       });
1205       context.trackRunListenerPromise(runListenerPromise);
1207       const result = await runListenerPromise;
1208       let rv = result && result.deserialize(globalThis);
1209       ChromeUtils.addProfilerMarker(
1210         "ExtensionParent",
1211         { startTime },
1212         `${context.extension.id}, api_event: ${data.path}`
1213       );
1214       lazy.ExtensionActivityLog.log(
1215         context.extension.id,
1216         context.viewType,
1217         "api_event",
1218         data.path,
1219         { args: listenerArgs, result: rv }
1220       );
1221       return rv;
1222     };
1224     context.listenerProxies.set(data.listenerId, listener);
1226     let args = data.args;
1227     let promise = context.apiCan.asyncFindAPIPath(data.path);
1229     // Store pending listener additions so we can be sure they're all
1230     // fully initialize before we consider extension startup complete.
1231     if (context.isBackgroundContext && context.listenerPromises) {
1232       const { listenerPromises } = context;
1233       listenerPromises.add(promise);
1234       let remove = () => {
1235         listenerPromises.delete(promise);
1236       };
1237       promise.then(remove, remove);
1238     }
1240     let handler = await promise;
1241     if (handler.setUserInput) {
1242       handlingUserInput = true;
1243     }
1244     handler.addListener(listener, ...args);
1245     if (!alreadyLogged) {
1246       lazy.ExtensionActivityLog.log(
1247         context.extension.id,
1248         context.viewType,
1249         "api_call",
1250         `${data.path}.addListener`,
1251         { args }
1252       );
1253     }
1254   },
1256   async recvRemoveListener(data) {
1257     let context = this.getContextById(data.childId);
1258     let listener = context.listenerProxies.get(data.listenerId);
1260     let handler = await context.apiCan.asyncFindAPIPath(data.path);
1261     handler.removeListener(listener);
1263     let { alreadyLogged = false } = data;
1264     if (!alreadyLogged) {
1265       lazy.ExtensionActivityLog.log(
1266         context.extension.id,
1267         context.viewType,
1268         "api_call",
1269         `${data.path}.removeListener`,
1270         { args: [] }
1271       );
1272     }
1273   },
1275   getContextById(childId) {
1276     let context = this.proxyContexts.get(childId);
1277     if (!context) {
1278       throw new Error("WebExtension context not found!");
1279     }
1280     return context;
1281   },
1284 ParentAPIManager.init();
1287  * A hidden window which contains the extension pages that are not visible
1288  * (i.e., background pages and devtools pages), and is also used by
1289  * ExtensionDebuggingUtils to contain the browser elements used by the
1290  * addon debugger to connect to the devtools actors running in the same
1291  * process of the target extension (and be able to stay connected across
1292  *  the addon reloads).
1293  */
1294 class HiddenXULWindow {
1295   constructor() {
1296     this._windowlessBrowser = null;
1297     this.unloaded = false;
1298     this.waitInitialized = this.initWindowlessBrowser();
1299   }
1301   shutdown() {
1302     if (this.unloaded) {
1303       throw new Error(
1304         "Unable to shutdown an unloaded HiddenXULWindow instance"
1305       );
1306     }
1308     this.unloaded = true;
1310     this.waitInitialized = null;
1312     if (!this._windowlessBrowser) {
1313       Cu.reportError("HiddenXULWindow was shut down while it was loading.");
1314       // initWindowlessBrowser will close windowlessBrowser when possible.
1315       return;
1316     }
1318     this._windowlessBrowser.close();
1319     this._windowlessBrowser = null;
1320   }
1322   get chromeDocument() {
1323     return this._windowlessBrowser.document;
1324   }
1326   /**
1327    * Private helper that create a HTMLDocument in a windowless browser.
1328    *
1329    * @returns {Promise<void>}
1330    *          A promise which resolves when the windowless browser is ready.
1331    */
1332   async initWindowlessBrowser() {
1333     if (this.waitInitialized) {
1334       throw new Error("HiddenXULWindow already initialized");
1335     }
1337     // The invisible page is currently wrapped in a XUL window to fix an issue
1338     // with using the canvas API from a background page (See Bug 1274775).
1339     let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
1341     // The windowless browser is a thin wrapper around a docShell that keeps
1342     // its related resources alive. It implements nsIWebNavigation and
1343     // forwards its methods to the underlying docShell. That .docShell
1344     // needs `QueryInterface(nsIWebNavigation)` to give us access to the
1345     // webNav methods that are already available on the windowless browser.
1346     let chromeShell = windowlessBrowser.docShell;
1347     chromeShell.QueryInterface(Ci.nsIWebNavigation);
1349     if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
1350       let attrs = chromeShell.getOriginAttributes();
1351       attrs.privateBrowsingId = 1;
1352       chromeShell.setOriginAttributes(attrs);
1353     }
1355     windowlessBrowser.browsingContext.useGlobalHistory = false;
1356     chromeShell.loadURI(DUMMY_PAGE_URI, {
1357       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
1358     });
1360     await promiseObserved(
1361       "chrome-document-global-created",
1362       win => win.document == chromeShell.document
1363     );
1364     await promiseDocumentLoaded(windowlessBrowser.document);
1365     if (this.unloaded) {
1366       windowlessBrowser.close();
1367       return;
1368     }
1369     this._windowlessBrowser = windowlessBrowser;
1370   }
1372   /**
1373    * Creates the browser XUL element that will contain the WebExtension Page.
1374    *
1375    * @param {object} xulAttributes
1376    *        An object that contains the xul attributes to set of the newly
1377    *        created browser XUL element.
1378    *
1379    * @returns {Promise<XULElement>}
1380    *          A Promise which resolves to the newly created browser XUL element.
1381    */
1382   async createBrowserElement(xulAttributes) {
1383     if (!xulAttributes || Object.keys(xulAttributes).length === 0) {
1384       throw new Error("missing mandatory xulAttributes parameter");
1385     }
1387     await this.waitInitialized;
1389     const chromeDoc = this.chromeDocument;
1391     const browser = chromeDoc.createXULElement("browser");
1392     browser.setAttribute("type", "content");
1393     browser.setAttribute("disableglobalhistory", "true");
1394     browser.setAttribute("messagemanagergroup", "webext-browsers");
1395     browser.setAttribute("manualactiveness", "true");
1397     for (const [name, value] of Object.entries(xulAttributes)) {
1398       if (value != null) {
1399         browser.setAttribute(name, value);
1400       }
1401     }
1403     let awaitFrameLoader;
1405     if (browser.getAttribute("remote") === "true") {
1406       awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
1407     }
1409     chromeDoc.documentElement.appendChild(browser);
1411     // Forcibly flush layout so that we get a pres shell soon enough, see
1412     // bug 1274775.
1413     browser.getBoundingClientRect();
1414     await awaitFrameLoader;
1416     // FIXME(emilio): This unconditionally active frame seems rather
1417     // unfortunate, but matches previous behavior.
1418     browser.docShellIsActive = true;
1420     return browser;
1421   }
1424 const SharedWindow = {
1425   _window: null,
1426   _count: 0,
1428   acquire() {
1429     if (this._window == null) {
1430       if (this._count != 0) {
1431         throw new Error(
1432           `Shared window already exists with count ${this._count}`
1433         );
1434       }
1436       this._window = new HiddenXULWindow();
1437     }
1439     this._count++;
1440     return this._window;
1441   },
1443   release() {
1444     if (this._count < 1) {
1445       throw new Error(`Releasing shared window with count ${this._count}`);
1446     }
1448     this._count--;
1449     if (this._count == 0) {
1450       this._window.shutdown();
1451       this._window = null;
1452     }
1453   },
1457  * This is a base class used by the ext-backgroundPage and ext-devtools API implementations
1458  * to inherits the shared boilerplate code needed to create a parent document for the hidden
1459  * extension pages (e.g. the background page, the devtools page) in the BackgroundPage and
1460  * DevToolsPage classes.
1462  * @param {Extension} extension
1463  *        The Extension which owns the hidden extension page created (used to decide
1464  *        if the hidden extension page parent doc is going to be a windowlessBrowser or
1465  *        a visible XUL window).
1466  * @param {string} viewType
1467  *        The viewType of the WebExtension page that is going to be loaded
1468  *        in the created browser element (e.g. "background" or "devtools_page").
1469  */
1470 class HiddenExtensionPage {
1471   constructor(extension, viewType) {
1472     if (!extension || !viewType) {
1473       throw new Error("extension and viewType parameters are mandatory");
1474     }
1476     this.extension = extension;
1477     this.viewType = viewType;
1478     this.browser = null;
1479     this.unloaded = false;
1480   }
1482   /**
1483    * Destroy the created parent document.
1484    */
1485   shutdown() {
1486     if (this.unloaded) {
1487       throw new Error(
1488         "Unable to shutdown an unloaded HiddenExtensionPage instance"
1489       );
1490     }
1492     this.unloaded = true;
1494     if (this.browser) {
1495       this._releaseBrowser();
1496     }
1497   }
1499   _releaseBrowser() {
1500     this.browser.remove();
1501     this.browser = null;
1502     SharedWindow.release();
1503   }
1505   /**
1506    * Creates the browser XUL element that will contain the WebExtension Page.
1507    *
1508    * @returns {Promise<XULElement>}
1509    *          A Promise which resolves to the newly created browser XUL element.
1510    */
1511   async createBrowserElement() {
1512     if (this.browser) {
1513       throw new Error("createBrowserElement called twice");
1514     }
1516     let window = SharedWindow.acquire();
1517     try {
1518       this.browser = await window.createBrowserElement({
1519         "webextension-view-type": this.viewType,
1520         remote: this.extension.remote ? "true" : null,
1521         remoteType: this.extension.remoteType,
1522         initialBrowsingContextGroupId: this.extension.browsingContextGroupId,
1523       });
1524     } catch (e) {
1525       SharedWindow.release();
1526       throw e;
1527     }
1529     if (this.unloaded) {
1530       this._releaseBrowser();
1531       throw new Error("Extension shut down before browser element was created");
1532     }
1534     return this.browser;
1535   }
1539  * This object provides utility functions needed by the devtools actors to
1540  * be able to connect and debug an extension (which can run in the main or in
1541  * a child extension process).
1542  */
1543 const DebugUtils = {
1544   // A lazily created hidden XUL window, which contains the browser elements
1545   // which are used to connect the webextension patent actor to the extension process.
1546   hiddenXULWindow: null,
1548   // Map<extensionId, Promise<XULElement>>
1549   debugBrowserPromises: new Map(),
1550   // DefaultWeakMap<Promise<browser XULElement>, Set<WebExtensionParentActor>>
1551   debugActors: new DefaultWeakMap(() => new Set()),
1553   _extensionUpdatedWatcher: null,
1554   watchExtensionUpdated() {
1555     if (!this._extensionUpdatedWatcher) {
1556       // Watch the updated extension objects.
1557       this._extensionUpdatedWatcher = async (evt, extension) => {
1558         const browserPromise = this.debugBrowserPromises.get(extension.id);
1559         if (browserPromise) {
1560           const browser = await browserPromise;
1561           if (
1562             browser.isRemoteBrowser !== extension.remote &&
1563             this.debugBrowserPromises.get(extension.id) === browserPromise
1564           ) {
1565             // If the cached browser element is not anymore of the same
1566             // remote type of the extension, remove it.
1567             this.debugBrowserPromises.delete(extension.id);
1568             browser.remove();
1569           }
1570         }
1571       };
1573       apiManager.on("ready", this._extensionUpdatedWatcher);
1574     }
1575   },
1577   unwatchExtensionUpdated() {
1578     if (this._extensionUpdatedWatcher) {
1579       apiManager.off("ready", this._extensionUpdatedWatcher);
1580       delete this._extensionUpdatedWatcher;
1581     }
1582   },
1584   getExtensionManifestWarnings(id) {
1585     const addon = GlobalManager.extensionMap.get(id);
1586     if (addon) {
1587       return addon.warnings;
1588     }
1589     return [];
1590   },
1592   /**
1593    * Determine if the extension does have a non-persistent background script
1594    * (either an event page or a background service worker):
1595    *
1596    * Based on this the DevTools client will determine if this extension should provide
1597    * to the extension developers a button to forcefully terminate the background
1598    * script.
1599    *
1600    * @param {string} addonId
1601    *   The id of the addon
1602    *
1603    * @returns {void|boolean}
1604    *   - undefined => does not apply (no background script in the manifest)
1605    *   - true => the background script is persistent.
1606    *   - false => the background script is an event page or a service worker.
1607    */
1608   hasPersistentBackgroundScript(addonId) {
1609     const policy = WebExtensionPolicy.getByID(addonId);
1611     // The addon doesn't have any background script or we
1612     // can't be sure yet.
1613     if (
1614       policy?.extension?.type !== "extension" ||
1615       !policy?.extension?.manifest?.background
1616     ) {
1617       return undefined;
1618     }
1620     return policy.extension.persistentBackground;
1621   },
1623   /**
1624    * Determine if the extension background page is running.
1625    *
1626    * Based on this the DevTools client will show the status of the background
1627    * script in about:debugging.
1628    *
1629    * @param {string} addonId
1630    *   The id of the addon
1631    *
1632    * @returns {void|boolean}
1633    *   - undefined => does not apply (no background script in the manifest)
1634    *   - true => the background script is running.
1635    *   - false => the background script is stopped.
1636    */
1637   isBackgroundScriptRunning(addonId) {
1638     const policy = WebExtensionPolicy.getByID(addonId);
1640     // The addon doesn't have any background script or we
1641     // can't be sure yet.
1642     if (!(this.hasPersistentBackgroundScript(addonId) === false)) {
1643       return undefined;
1644     }
1646     const views = policy?.extension?.views || [];
1647     for (const view of views) {
1648       if (
1649         view.viewType === "background" ||
1650         (view.viewType === "background_worker" && !view.unloaded)
1651       ) {
1652         return true;
1653       }
1654     }
1656     return false;
1657   },
1659   async terminateBackgroundScript(addonId) {
1660     // Terminate the background if the extension does have
1661     // a non-persistent background script (event page or background
1662     // service worker).
1663     if (this.hasPersistentBackgroundScript(addonId) === false) {
1664       const policy = WebExtensionPolicy.getByID(addonId);
1665       // When the event page is being terminated through the Devtools
1666       // action, we should terminate it even if there are DevTools
1667       // toolboxes attached to the extension.
1668       return policy.extension.terminateBackground({
1669         ignoreDevToolsAttached: true,
1670       });
1671     }
1672     throw Error(`Unable to terminate background script for ${addonId}`);
1673   },
1675   /**
1676    * Determine whether a devtools toolbox attached to the extension.
1677    *
1678    * This method is called by the background page idle timeout handler,
1679    * to inhibit terminating the event page when idle while the extension
1680    * developer is debugging the extension through the Addon Debugging window
1681    * (similarly to how service workers are kept alive while the devtools are
1682    * attached).
1683    *
1684    * @param {string} id
1685    *        The id of the extension.
1686    *
1687    * @returns {boolean}
1688    *          true when a devtools toolbox is attached to an extension with
1689    *          the given id, false otherwise.
1690    */
1691   hasDevToolsAttached(id) {
1692     return this.debugBrowserPromises.has(id);
1693   },
1695   /**
1696    * Retrieve a XUL browser element which has been configured to be able to connect
1697    * the devtools actor with the process where the extension is running.
1698    *
1699    * @param {WebExtensionParentActor} webExtensionParentActor
1700    *        The devtools actor that is retrieving the browser element.
1701    *
1702    * @returns {Promise<XULElement>}
1703    *          A promise which resolves to the configured browser XUL element.
1704    */
1705   async getExtensionProcessBrowser(webExtensionParentActor) {
1706     const extensionId = webExtensionParentActor.addonId;
1707     const extension = GlobalManager.getExtension(extensionId);
1708     if (!extension) {
1709       throw new Error(`Extension not found: ${extensionId}`);
1710     }
1712     const createBrowser = () => {
1713       if (!this.hiddenXULWindow) {
1714         this.hiddenXULWindow = new HiddenXULWindow();
1715         this.watchExtensionUpdated();
1716       }
1718       return this.hiddenXULWindow.createBrowserElement({
1719         "webextension-addon-debug-target": extensionId,
1720         remote: extension.remote ? "true" : null,
1721         remoteType: extension.remoteType,
1722         initialBrowsingContextGroupId: extension.browsingContextGroupId,
1723       });
1724     };
1726     let browserPromise = this.debugBrowserPromises.get(extensionId);
1728     // Create a new promise if there is no cached one in the map.
1729     if (!browserPromise) {
1730       browserPromise = createBrowser();
1731       this.debugBrowserPromises.set(extensionId, browserPromise);
1732       browserPromise.then(browser => {
1733         browserPromise.browser = browser;
1734       });
1735       browserPromise.catch(e => {
1736         Cu.reportError(e);
1737         this.debugBrowserPromises.delete(extensionId);
1738       });
1739     }
1741     this.debugActors.get(browserPromise).add(webExtensionParentActor);
1743     return browserPromise;
1744   },
1746   getFrameLoader(extensionId) {
1747     let promise = this.debugBrowserPromises.get(extensionId);
1748     return promise && promise.browser && promise.browser.frameLoader;
1749   },
1751   /**
1752    * Given the devtools actor that has retrieved an addon debug browser element,
1753    * it destroys the XUL browser element, and it also destroy the hidden XUL window
1754    * if it is not currently needed.
1755    *
1756    * @param {WebExtensionParentActor} webExtensionParentActor
1757    *        The devtools actor that has retrieved an addon debug browser element.
1758    */
1759   async releaseExtensionProcessBrowser(webExtensionParentActor) {
1760     const extensionId = webExtensionParentActor.addonId;
1761     const browserPromise = this.debugBrowserPromises.get(extensionId);
1763     if (browserPromise) {
1764       const actorsSet = this.debugActors.get(browserPromise);
1765       actorsSet.delete(webExtensionParentActor);
1766       if (actorsSet.size === 0) {
1767         this.debugActors.delete(browserPromise);
1768         this.debugBrowserPromises.delete(extensionId);
1769         await browserPromise.then(browser => browser.remove());
1770       }
1771     }
1773     if (this.debugBrowserPromises.size === 0 && this.hiddenXULWindow) {
1774       this.hiddenXULWindow.shutdown();
1775       this.hiddenXULWindow = null;
1776       this.unwatchExtensionUpdated();
1777     }
1778   },
1782  * Returns a Promise which resolves with the message data when the given message
1783  * was received by the message manager. The promise is rejected if the message
1784  * manager was closed before a message was received.
1786  * @param {nsIMessageListenerManager} messageManager
1787  *        The message manager on which to listen for messages.
1788  * @param {string} messageName
1789  *        The message to listen for.
1790  * @returns {Promise<*>}
1791  */
1792 function promiseMessageFromChild(messageManager, messageName) {
1793   return new Promise((resolve, reject) => {
1794     let unregister;
1795     function listener(message) {
1796       unregister();
1797       resolve(message.data);
1798     }
1799     function observer(subject) {
1800       if (subject === messageManager) {
1801         unregister();
1802         reject(
1803           new Error(
1804             `Message manager was disconnected before receiving ${messageName}`
1805           )
1806         );
1807       }
1808     }
1809     unregister = () => {
1810       Services.obs.removeObserver(observer, "message-manager-close");
1811       messageManager.removeMessageListener(messageName, listener);
1812     };
1813     messageManager.addMessageListener(messageName, listener);
1814     Services.obs.addObserver(observer, "message-manager-close");
1815   });
1818 // This should be called before browser.loadURI is invoked.
1819 async function promiseBackgroundViewLoaded(browser) {
1820   let { childId } = await promiseMessageFromChild(
1821     browser.messageManager,
1822     "Extension:BackgroundViewLoaded"
1823   );
1824   if (childId) {
1825     return ParentAPIManager.getContextById(childId);
1826   }
1830  * This helper is used to subscribe a listener (e.g. in the ext-devtools API implementation)
1831  * to be called for every ExtensionProxyContext created for an extension page given
1832  * its related extension, viewType and browser element (both the top level context and any context
1833  * created for the extension urls running into its iframe descendants).
1835  * @param {object} params
1836  * @param {object} params.extension
1837  *        The Extension on which we are going to listen for the newly created ExtensionProxyContext.
1838  * @param {string} params.viewType
1839  *        The viewType of the WebExtension page that we are watching (e.g. "background" or
1840  *        "devtools_page").
1841  * @param {XULElement} params.browser
1842  *        The browser element of the WebExtension page that we are watching.
1843  * @param {Function} onExtensionProxyContextLoaded
1844  *        The callback that is called when a new context has been loaded (as `callback(context)`);
1846  * @returns {Function}
1847  *          Unsubscribe the listener.
1848  */
1849 function watchExtensionProxyContextLoad(
1850   { extension, viewType, browser },
1851   onExtensionProxyContextLoaded
1852 ) {
1853   if (typeof onExtensionProxyContextLoaded !== "function") {
1854     throw new Error("Missing onExtensionProxyContextLoaded handler");
1855   }
1857   const listener = (event, context) => {
1858     if (context.viewType == viewType && context.xulBrowser == browser) {
1859       onExtensionProxyContextLoaded(context);
1860     }
1861   };
1863   extension.on("extension-proxy-context-load", listener);
1865   return () => {
1866     extension.off("extension-proxy-context-load", listener);
1867   };
1871  * This helper is used to subscribe a listener (e.g. in the ext-backgroundPage)
1872  * to be called for every ExtensionProxyContext created for an extension
1873  * background service worker given its related extension.
1875  * @param {object} params
1876  * @param {object} params.extension
1877  *        The Extension on which we are going to listen for the newly created ExtensionProxyContext.
1878  * @param {Function} onExtensionWorkerContextLoaded
1879  *        The callback that is called when the worker script has been fully loaded (as `callback(context)`);
1881  * @returns {Function}
1882  *          Unsubscribe the listener.
1883  */
1884 function watchExtensionWorkerContextLoaded(
1885   { extension },
1886   onExtensionWorkerContextLoaded
1887 ) {
1888   if (typeof onExtensionWorkerContextLoaded !== "function") {
1889     throw new Error("Missing onExtensionWorkerContextLoaded handler");
1890   }
1892   const listener = (event, context) => {
1893     if (context.viewType == "background_worker") {
1894       onExtensionWorkerContextLoaded(context);
1895     }
1896   };
1898   extension.on("extension-proxy-context-load:completed", listener);
1900   return () => {
1901     extension.off("extension-proxy-context-load:completed", listener);
1902   };
1905 // Manages icon details for toolbar buttons in the |pageAction| and
1906 // |browserAction| APIs.
1907 let IconDetails = {
1908   DEFAULT_ICON: "chrome://mozapps/skin/extensions/extensionGeneric.svg",
1910   // WeakMap<Extension -> Map<url-string -> Map<iconType-string -> object>>>
1911   iconCache: new DefaultWeakMap(() => {
1912     return new DefaultMap(() => new DefaultMap(() => new Map()));
1913   }),
1915   // Normalizes the various acceptable input formats into an object
1916   // with icon size as key and icon URL as value.
1917   //
1918   // If a context is specified (function is called from an extension):
1919   // Throws an error if an invalid icon size was provided or the
1920   // extension is not allowed to load the specified resources.
1921   //
1922   // If no context is specified, instead of throwing an error, this
1923   // function simply logs a warning message.
1924   normalize(details, extension, context = null) {
1925     if (!details.imageData && details.path != null) {
1926       // Pick a cache key for the icon paths. If the path is a string,
1927       // use it directly. Otherwise, stringify the path object.
1928       let key = details.path;
1929       if (typeof key !== "string") {
1930         key = uneval(key);
1931       }
1933       let icons = this.iconCache
1934         .get(extension)
1935         .get(context && context.uri.spec)
1936         .get(details.iconType);
1938       let icon = icons.get(key);
1939       if (!icon) {
1940         icon = this._normalize(details, extension, context);
1941         icons.set(key, icon);
1942       }
1943       return icon;
1944     }
1946     return this._normalize(details, extension, context);
1947   },
1949   _normalize(details, extension, context = null) {
1950     let result = {};
1952     try {
1953       let { imageData, path, themeIcons } = details;
1955       if (imageData) {
1956         if (typeof imageData == "string") {
1957           imageData = { 19: imageData };
1958         }
1960         for (let size of Object.keys(imageData)) {
1961           result[size] = imageData[size];
1962         }
1963       }
1965       let baseURI = context ? context.uri : extension.baseURI;
1967       if (path != null) {
1968         if (typeof path != "object") {
1969           path = { 19: path };
1970         }
1972         for (let size of Object.keys(path)) {
1973           let url = path[size];
1974           if (url) {
1975             url = baseURI.resolve(path[size]);
1977             // The Chrome documentation specifies these parameters as
1978             // relative paths. We currently accept absolute URLs as well,
1979             // which means we need to check that the extension is allowed
1980             // to load them. This will throw an error if it's not allowed.
1981             this._checkURL(url, extension);
1982           }
1983           result[size] = url || this.DEFAULT_ICON;
1984         }
1985       }
1987       if (themeIcons) {
1988         themeIcons.forEach(({ size, light, dark }) => {
1989           let lightURL = baseURI.resolve(light);
1990           let darkURL = baseURI.resolve(dark);
1992           this._checkURL(lightURL, extension);
1993           this._checkURL(darkURL, extension);
1995           let defaultURL = result[size] || result[19]; // always fallback to default first
1996           result[size] = {
1997             default: defaultURL || darkURL, // Fallback to the dark url if no default is specified.
1998             light: lightURL,
1999             dark: darkURL,
2000           };
2001         });
2002       }
2003     } catch (e) {
2004       // Function is called from extension code, delegate error.
2005       if (context) {
2006         throw e;
2007       }
2008       // If there's no context, it's because we're handling this
2009       // as a manifest directive. Log a warning rather than
2010       // raising an error.
2011       extension.manifestError(`Invalid icon data: ${e}`);
2012     }
2014     return result;
2015   },
2017   // Checks if the extension is allowed to load the given URL with the specified principal.
2018   // This will throw an error if the URL is not allowed.
2019   _checkURL(url, extension) {
2020     if (!extension.checkLoadURL(url, { allowInheritsPrincipal: true })) {
2021       throw new ExtensionError(`Illegal URL ${url}`);
2022     }
2023   },
2025   // Returns the appropriate icon URL for the given icons object and the
2026   // screen resolution of the given window.
2027   getPreferredIcon(icons, extension, size = 16) {
2028     const DEFAULT = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
2030     let bestSize = null;
2031     if (icons[size]) {
2032       bestSize = size;
2033     } else if (icons[2 * size]) {
2034       bestSize = 2 * size;
2035     } else {
2036       let sizes = Object.keys(icons)
2037         .map(key => parseInt(key, 10))
2038         .sort((a, b) => a - b);
2040       bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
2041     }
2043     if (bestSize) {
2044       return { size: bestSize, icon: icons[bestSize] || DEFAULT };
2045     }
2047     return { size, icon: DEFAULT };
2048   },
2050   // These URLs should already be properly escaped, but make doubly sure CSS
2051   // string escape characters are escaped here, since they could lead to a
2052   // sandbox break.
2053   escapeUrl(url) {
2054     return url.replace(/[\\\s"]/g, encodeURIComponent);
2055   },
2058 class CacheStore {
2059   constructor(storeName) {
2060     this.storeName = storeName;
2061   }
2063   async getStore(path = null) {
2064     let data = await StartupCache.dataPromise;
2066     let store = data.get(this.storeName);
2067     if (!store) {
2068       store = new Map();
2069       data.set(this.storeName, store);
2070     }
2072     let key = path;
2073     if (Array.isArray(path)) {
2074       for (let elem of path.slice(0, -1)) {
2075         let next = store.get(elem);
2076         if (!next) {
2077           next = new Map();
2078           store.set(elem, next);
2079         }
2080         store = next;
2081       }
2082       key = path[path.length - 1];
2083     }
2085     return [store, key];
2086   }
2088   async get(path, createFunc) {
2089     let [store, key] = await this.getStore(path);
2091     let result = store.get(key);
2093     if (result === undefined) {
2094       result = await createFunc(path);
2095       store.set(key, result);
2096       StartupCache.save();
2097     }
2099     return result;
2100   }
2102   async set(path, value) {
2103     let [store, key] = await this.getStore(path);
2105     store.set(key, value);
2106     StartupCache.save();
2107   }
2109   async getAll() {
2110     let [store] = await this.getStore();
2112     return new Map(store);
2113   }
2115   async delete(path) {
2116     let [store, key] = await this.getStore(path);
2118     if (store.delete(key)) {
2119       StartupCache.save();
2120     }
2121   }
2124 // A cache to support faster initialization of extensions at browser startup.
2125 // All cached data is removed when the browser is updated.
2126 // Extension-specific data is removed when the add-on is updated.
2127 var StartupCache = {
2128   _ensureDirectoryPromise: null,
2129   _saveTask: null,
2131   _ensureDirectory() {
2132     if (this._ensureDirectoryPromise === null) {
2133       this._ensureDirectoryPromise = IOUtils.makeDirectory(
2134         PathUtils.parent(this.file),
2135         {
2136           ignoreExisting: true,
2137           createAncestors: true,
2138         }
2139       );
2140     }
2142     return this._ensureDirectoryPromise;
2143   },
2145   // When the application version changes, this file is removed by
2146   // RemoveComponentRegistries in nsAppRunner.cpp.
2147   file: PathUtils.join(
2148     Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
2149     "startupCache",
2150     "webext.sc.lz4"
2151   ),
2153   async _saveNow() {
2154     let data = new Uint8Array(lazy.aomStartup.encodeBlob(this._data));
2155     await this._ensureDirectoryPromise;
2156     await IOUtils.write(this.file, data, { tmpPath: `${this.file}.tmp` });
2158     Glean.extensions.startupCacheWriteBytelength.set(data.byteLength);
2159   },
2161   save() {
2162     this._ensureDirectory();
2164     if (!this._saveTask) {
2165       this._saveTask = new lazy.DeferredTask(() => this._saveNow(), 5000);
2167       IOUtils.profileBeforeChange.addBlocker(
2168         "Flush WebExtension StartupCache",
2169         async () => {
2170           await this._saveTask.finalize();
2171           this._saveTask = null;
2172         }
2173       );
2174     }
2176     return this._saveTask.arm();
2177   },
2179   _data: null,
2180   async _readData() {
2181     let result = new Map();
2182     try {
2183       Glean.extensions.startupCacheLoadTime.start();
2184       let { buffer } = await IOUtils.read(this.file);
2186       result = lazy.aomStartup.decodeBlob(buffer);
2187       Glean.extensions.startupCacheLoadTime.stop();
2188     } catch (e) {
2189       Glean.extensions.startupCacheLoadTime.cancel();
2190       if (!DOMException.isInstance(e) || e.name !== "NotFoundError") {
2191         Cu.reportError(e);
2192       }
2193       let error = lazy.getErrorNameForTelemetry(e);
2194       Glean.extensions.startupCacheReadErrors[error].add(1);
2195     }
2197     this._data = result;
2198     return result;
2199   },
2201   get dataPromise() {
2202     if (!this._dataPromise) {
2203       this._dataPromise = this._readData();
2204     }
2205     return this._dataPromise;
2206   },
2208   clearAddonData(id) {
2209     return Promise.all([
2210       this.general.delete(id),
2211       this.locales.delete(id),
2212       this.manifests.delete(id),
2213       this.permissions.delete(id),
2214       this.menus.delete(id),
2215     ]).catch(() => {
2216       // Ignore the error. It happens when we try to flush the add-on
2217       // data after the AddonManager has flushed the entire startup cache.
2218     });
2219   },
2221   observe(subject, topic) {
2222     if (topic === "startupcache-invalidate") {
2223       this._data = new Map();
2224       this._dataPromise = Promise.resolve(this._data);
2225     }
2226   },
2228   get(extension, path, createFunc) {
2229     return this.general.get(
2230       [extension.id, extension.version, ...path],
2231       createFunc
2232     );
2233   },
2235   delete(extension, path) {
2236     return this.general.delete([extension.id, extension.version, ...path]);
2237   },
2239   general: new CacheStore("general"),
2240   locales: new CacheStore("locales"),
2241   manifests: new CacheStore("manifests"),
2242   other: new CacheStore("other"),
2243   permissions: new CacheStore("permissions"),
2244   schemas: new CacheStore("schemas"),
2245   menus: new CacheStore("menus"),
2248 Services.obs.addObserver(StartupCache, "startupcache-invalidate");
2250 export var ExtensionParent = {
2251   GlobalManager,
2252   HiddenExtensionPage,
2253   IconDetails,
2254   ParentAPIManager,
2255   StartupCache,
2256   WebExtensionPolicy,
2257   apiManager,
2258   promiseBackgroundViewLoaded,
2259   watchExtensionProxyContextLoad,
2260   watchExtensionWorkerContextLoaded,
2261   DebugUtils,
2264 // browserPaintedPromise and browserStartupPromise are promises that
2265 // resolve after the first browser window is painted and after browser
2266 // windows have been restored, respectively. Alternatively,
2267 // browserStartupPromise also resolves from the extensions-late-startup
2268 // notification sent by Firefox Reality on desktop platforms, because it
2269 // doesn't support SessionStore.
2270 // _resetStartupPromises should only be called from outside this file in tests.
2271 ExtensionParent._resetStartupPromises = () => {
2272   ExtensionParent.browserPaintedPromise = promiseObserved(
2273     "browser-delayed-startup-finished"
2274   ).then(() => {});
2275   ExtensionParent.browserStartupPromise = Promise.race([
2276     promiseObserved("sessionstore-windows-restored"),
2277     promiseObserved("extensions-late-startup"),
2278   ]).then(() => {});
2280 ExtensionParent._resetStartupPromises();
2282 ChromeUtils.defineLazyGetter(ExtensionParent, "PlatformInfo", () => {
2283   return Object.freeze({
2284     os: (function () {
2285       let os = AppConstants.platform;
2286       if (os == "macosx") {
2287         os = "mac";
2288       }
2289       return os;
2290     })(),
2291     arch: (function () {
2292       let abi = Services.appinfo.XPCOMABI;
2293       let [arch] = abi.split("-");
2294       if (arch == "x86") {
2295         arch = "x86-32";
2296       } else if (arch == "x86_64") {
2297         arch = "x86-64";
2298       }
2299       return arch;
2300     })(),
2301   });