Bug 1870926 [wpt PR 43734] - Remove experimental ::details-summary pseudo-element...
[gecko.git] / toolkit / components / extensions / ExtensionParent.sys.mjs
blobda90a62954da75ff552240c81fd2e447ef9c5466
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.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   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, 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.jsm, 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(type, name, data) {
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.jsm.
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.jsm.
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   }
721   get tabId() {
722     let { tabTracker } = apiManager.global;
723     let data = tabTracker.getBrowserData(this.xulBrowser);
724     if (data.tabId >= 0) {
725       return data.tabId;
726     }
727   }
729   unload() {
730     super.unload();
731     this.extension.views.delete(this);
732   }
734   shutdown() {
735     apiManager.emit("page-shutdown", this);
736     super.shutdown();
737   }
741  * The parent side of proxied API context for devtools extension page, such as a
742  * devtools pages and panels running in ExtensionChild.jsm.
743  */
744 class DevToolsExtensionPageContextParent extends ExtensionPageContextParent {
745   constructor(...params) {
746     super(...params);
748     // Set all attributes that are lazily defined to `null` here.
749     //
750     // Note that we can't do that for `this._devToolsToolbox` because it will
751     // be defined when calling our parent constructor and so would override it back to `null`.
752     this._devToolsCommands = null;
753     this._onNavigatedListeners = null;
755     this._onResourceAvailable = this._onResourceAvailable.bind(this);
756   }
758   set devToolsToolbox(toolbox) {
759     if (this._devToolsToolbox) {
760       throw new Error("Cannot set the context DevTools toolbox twice");
761     }
763     this._devToolsToolbox = toolbox;
764   }
766   get devToolsToolbox() {
767     return this._devToolsToolbox;
768   }
770   async addOnNavigatedListener(listener) {
771     if (!this._onNavigatedListeners) {
772       this._onNavigatedListeners = new Set();
774       await this.devToolsToolbox.resourceCommand.watchResources(
775         [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
776         {
777           onAvailable: this._onResourceAvailable,
778           ignoreExistingResources: true,
779         }
780       );
781     }
783     this._onNavigatedListeners.add(listener);
784   }
786   removeOnNavigatedListener(listener) {
787     if (this._onNavigatedListeners) {
788       this._onNavigatedListeners.delete(listener);
789     }
790   }
792   /**
793    * The returned "commands" object, exposing modules implemented from devtools/shared/commands.
794    * Each attribute being a static interface to communicate with the server backend.
795    *
796    * @returns {Promise<object>}
797    */
798   async getDevToolsCommands() {
799     // Ensure that we try to instantiate a commands only once,
800     // even if createCommandsForTabForWebExtension is async.
801     if (this._devToolsCommandsPromise) {
802       return this._devToolsCommandsPromise;
803     }
804     if (this._devToolsCommands) {
805       return this._devToolsCommands;
806     }
808     this._devToolsCommandsPromise = (async () => {
809       const commands =
810         await lazy.DevToolsShim.createCommandsForTabForWebExtension(
811           this.devToolsToolbox.commands.descriptorFront.localTab
812         );
813       await commands.targetCommand.startListening();
814       this._devToolsCommands = commands;
815       this._devToolsCommandsPromise = null;
816       return commands;
817     })();
818     return this._devToolsCommandsPromise;
819   }
821   unload() {
822     // Bail if the toolbox reference was already cleared.
823     if (!this.devToolsToolbox) {
824       return;
825     }
827     if (this._onNavigatedListeners) {
828       this.devToolsToolbox.resourceCommand.unwatchResources(
829         [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
830         { onAvailable: this._onResourceAvailable }
831       );
832     }
834     if (this._devToolsCommands) {
835       this._devToolsCommands.destroy();
836       this._devToolsCommands = null;
837     }
839     if (this._onNavigatedListeners) {
840       this._onNavigatedListeners.clear();
841       this._onNavigatedListeners = null;
842     }
844     this._devToolsToolbox = null;
846     super.unload();
847   }
849   async _onResourceAvailable(resources) {
850     for (const resource of resources) {
851       const { targetFront } = resource;
852       if (targetFront.isTopLevel && resource.name === "dom-complete") {
853         for (const listener of this._onNavigatedListeners) {
854           listener(targetFront.url);
855         }
856       }
857     }
858   }
862  * The parent side of proxied API context for extension background service
863  * worker script.
864  */
865 class BackgroundWorkerContextParent extends ProxyContextParent {
866   constructor(envType, extension, params) {
867     // TODO: split out from ProxyContextParent a base class that
868     // doesn't expect a browsingContext and one for contexts that are
869     // expected to have a browsingContext associated.
870     super(envType, extension, params, null, extension.principal);
872     this.viewType = params.viewType;
873     this.workerDescriptorId = params.workerDescriptorId;
875     this.extension.views.add(this);
877     extension.emit("extension-proxy-context-load", this);
878   }
881 ParentAPIManager = {
882   proxyContexts: new Map(),
884   init() {
885     // TODO: Bug 1595186 - remove/replace all usage of MessageManager below.
886     Services.obs.addObserver(this, "message-manager-close");
888     this.conduit = new lazy.BroadcastConduit(this, {
889       id: "ParentAPIManager",
890       reportOnClosed: "childId",
891       recv: [
892         "CreateProxyContext",
893         "ContextLoaded",
894         "APICall",
895         "AddListener",
896         "RemoveListener",
897       ],
898       send: ["CallResult"],
899       query: ["RunListener", "StreamFilterSuspendCancel"],
900     });
901   },
903   attachMessageManager(extension, processMessageManager) {
904     extension.parentMessageManager = processMessageManager;
905   },
907   async observe(subject, topic, data) {
908     if (topic === "message-manager-close") {
909       let mm = subject;
910       for (let [childId, context] of this.proxyContexts) {
911         if (context.parentMessageManager === mm) {
912           this.closeProxyContext(childId);
913         }
914       }
916       // Reset extension message managers when their child processes shut down.
917       for (let extension of GlobalManager.extensionMap.values()) {
918         if (extension.parentMessageManager === mm) {
919           extension.parentMessageManager = null;
920         }
921       }
922     }
923   },
925   shutdownExtension(extensionId, reason) {
926     if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(reason)) {
927       apiManager._callHandlers([extensionId], "disable", "onDisable");
928     }
930     for (let [childId, context] of this.proxyContexts) {
931       if (context.extension.id == extensionId) {
932         context.shutdown();
933         this.proxyContexts.delete(childId);
934       }
935     }
936   },
938   queryStreamFilterSuspendCancel(childId) {
939     return this.conduit.queryStreamFilterSuspendCancel(childId);
940   },
942   recvCreateProxyContext(data, { actor, sender }) {
943     let { envType, extensionId, childId, principal } = data;
945     if (this.proxyContexts.has(childId)) {
946       throw new Error(
947         "A WebExtension context with the given ID already exists!"
948       );
949     }
951     let extension = GlobalManager.getExtension(extensionId);
952     if (!extension) {
953       throw new Error(`No WebExtension found with ID ${extensionId}`);
954     }
956     let context;
957     if (envType == "addon_parent" || envType == "devtools_parent") {
958       if (!sender.verified) {
959         throw new Error(`Bad sender context envType: ${sender.envType}`);
960       }
962       let isBackgroundWorker = false;
963       if (JSWindowActorParent.isInstance(actor)) {
964         const target = actor.browsingContext.top.embedderElement;
965         let processMessageManager =
966           target.messageManager.processMessageManager ||
967           Services.ppmm.getChildAt(0);
969         if (!extension.parentMessageManager) {
970           if (target.remoteType === extension.remoteType) {
971             this.attachMessageManager(extension, processMessageManager);
972           }
973         }
975         if (processMessageManager !== extension.parentMessageManager) {
976           throw new Error(
977             "Attempt to create privileged extension parent from incorrect child process"
978           );
979         }
980       } else if (JSProcessActorParent.isInstance(actor)) {
981         if (actor.manager.remoteType !== extension.remoteType) {
982           throw new Error(
983             "Attempt to create privileged extension parent from incorrect child process"
984           );
985         }
987         if (envType !== "addon_parent") {
988           throw new Error(
989             `Unexpected envType ${envType} on an extension process actor`
990           );
991         }
992         if (data.viewType !== "background_worker") {
993           throw new Error(
994             `Unexpected viewType ${data.viewType} on an extension process actor`
995           );
996         }
997         isBackgroundWorker = true;
998       } else {
999         // Unreacheable: JSWindowActorParent and JSProcessActorParent are the
1000         // only actors.
1001         throw new Error(
1002           "Attempt to create privileged extension parent via incorrect actor"
1003         );
1004       }
1006       if (isBackgroundWorker) {
1007         context = new BackgroundWorkerContextParent(envType, extension, data);
1008       } else if (envType == "addon_parent") {
1009         context = new ExtensionPageContextParent(
1010           envType,
1011           extension,
1012           data,
1013           actor.browsingContext
1014         );
1015       } else if (envType == "devtools_parent") {
1016         context = new DevToolsExtensionPageContextParent(
1017           envType,
1018           extension,
1019           data,
1020           actor.browsingContext
1021         );
1022       }
1023     } else if (envType == "content_parent") {
1024       // Note: actor is always a JSWindowActorParent, with a browsingContext.
1025       context = new ContentScriptContextParent(
1026         envType,
1027         extension,
1028         data,
1029         actor.browsingContext,
1030         principal
1031       );
1032     } else {
1033       throw new Error(`Invalid WebExtension context envType: ${envType}`);
1034     }
1035     this.proxyContexts.set(childId, context);
1036   },
1038   recvContextLoaded(data, { actor, sender }) {
1039     let context = this.getContextById(data.childId);
1040     verifyActorForContext(actor, context);
1041     const { extension } = context;
1042     extension.emit("extension-proxy-context-load:completed", context);
1043   },
1045   recvConduitClosed(sender) {
1046     this.closeProxyContext(sender.id);
1047   },
1049   closeProxyContext(childId) {
1050     let context = this.proxyContexts.get(childId);
1051     if (context) {
1052       context.unload();
1053       this.proxyContexts.delete(childId);
1054     }
1055   },
1057   /**
1058    * Call the given function and also log the call as appropriate
1059    * (i.e., with activity logging and/or profiler markers)
1060    *
1061    * @param {BaseContext} context The context making this call.
1062    * @param {object} data Additional data about the call.
1063    * @param {Function} callable The actual implementation to invoke.
1064    */
1065   async callAndLog(context, data, callable) {
1066     let { id } = context.extension;
1067     // If we were called via callParentAsyncFunction we don't want
1068     // to log again, check for the flag.
1069     const { alreadyLogged } = data.options || {};
1070     if (!alreadyLogged) {
1071       lazy.ExtensionActivityLog.log(
1072         id,
1073         context.viewType,
1074         "api_call",
1075         data.path,
1076         {
1077           args: data.args,
1078         }
1079       );
1080     }
1082     let start = Cu.now();
1083     try {
1084       return callable();
1085     } finally {
1086       ChromeUtils.addProfilerMarker(
1087         "ExtensionParent",
1088         { startTime: start },
1089         `${id}, api_call: ${data.path}`
1090       );
1091     }
1092   },
1094   async recvAPICall(data, { actor }) {
1095     let context = this.getContextById(data.childId);
1096     let target = actor.browsingContext?.top.embedderElement;
1098     verifyActorForContext(actor, context);
1100     let reply = result => {
1101       if (target && !context.parentMessageManager) {
1102         Services.console.logStringMessage(
1103           "Cannot send function call result: other side closed connection " +
1104             `(call data: ${uneval({ path: data.path, args: data.args })})`
1105         );
1106         return;
1107       }
1109       this.conduit.sendCallResult(data.childId, {
1110         childId: data.childId,
1111         callId: data.callId,
1112         path: data.path,
1113         ...result,
1114       });
1115     };
1117     try {
1118       if (
1119         context.isBackgroundContext &&
1120         !context.extension.persistentBackground
1121       ) {
1122         context.extension.emit("background-script-reset-idle", {
1123           reason: "parentApiCall",
1124           path: data.path,
1125         });
1126       }
1128       let args = data.args;
1129       let { isHandlingUserInput = false } = data.options || {};
1130       let pendingBrowser = context.pendingEventBrowser;
1131       let fun = await context.apiCan.asyncFindAPIPath(data.path);
1132       let result = this.callAndLog(context, data, () => {
1133         return context.withPendingBrowser(pendingBrowser, () =>
1134           context.withCallContextData({ isHandlingUserInput }, () =>
1135             fun(...args)
1136           )
1137         );
1138       });
1140       if (data.callId) {
1141         result = result || Promise.resolve();
1143         result.then(
1144           result => {
1145             result = result instanceof SpreadArgs ? [...result] : [result];
1147             let holder = new StructuredCloneHolder(
1148               `ExtensionParent/${context.extension.id}/recvAPICall/${data.path}`,
1149               null,
1150               result
1151             );
1153             reply({ result: holder });
1154           },
1155           error => {
1156             error = context.normalizeError(error);
1157             reply({
1158               error: { message: error.message, fileName: error.fileName },
1159             });
1160           }
1161         );
1162       }
1163     } catch (e) {
1164       if (data.callId) {
1165         let error = context.normalizeError(e);
1166         reply({ error: { message: error.message } });
1167       } else {
1168         Cu.reportError(e);
1169       }
1170     }
1171   },
1173   async recvAddListener(data, { actor }) {
1174     let context = this.getContextById(data.childId);
1176     verifyActorForContext(actor, context);
1178     let { childId, alreadyLogged = false } = data;
1179     let handlingUserInput = false;
1181     let listener = async (...listenerArgs) => {
1182       let startTime = Cu.now();
1183       // Extract urgentSend flag to avoid deserializing args holder later.
1184       let urgentSend = false;
1185       if (listenerArgs[0] && data.path.startsWith("webRequest.")) {
1186         urgentSend = listenerArgs[0].urgentSend;
1187         delete listenerArgs[0].urgentSend;
1188       }
1189       let runListenerPromise = this.conduit.queryRunListener(childId, {
1190         childId,
1191         handlingUserInput,
1192         listenerId: data.listenerId,
1193         path: data.path,
1194         urgentSend,
1195         get args() {
1196           return new StructuredCloneHolder(
1197             `ExtensionParent/${context.extension.id}/recvAddListener/${data.path}`,
1198             null,
1199             listenerArgs
1200           );
1201         },
1202       });
1203       context.trackRunListenerPromise(runListenerPromise);
1205       const result = await runListenerPromise;
1206       let rv = result && result.deserialize(globalThis);
1207       ChromeUtils.addProfilerMarker(
1208         "ExtensionParent",
1209         { startTime },
1210         `${context.extension.id}, api_event: ${data.path}`
1211       );
1212       lazy.ExtensionActivityLog.log(
1213         context.extension.id,
1214         context.viewType,
1215         "api_event",
1216         data.path,
1217         { args: listenerArgs, result: rv }
1218       );
1219       return rv;
1220     };
1222     context.listenerProxies.set(data.listenerId, listener);
1224     let args = data.args;
1225     let promise = context.apiCan.asyncFindAPIPath(data.path);
1227     // Store pending listener additions so we can be sure they're all
1228     // fully initialize before we consider extension startup complete.
1229     if (context.isBackgroundContext && context.listenerPromises) {
1230       const { listenerPromises } = context;
1231       listenerPromises.add(promise);
1232       let remove = () => {
1233         listenerPromises.delete(promise);
1234       };
1235       promise.then(remove, remove);
1236     }
1238     let handler = await promise;
1239     if (handler.setUserInput) {
1240       handlingUserInput = true;
1241     }
1242     handler.addListener(listener, ...args);
1243     if (!alreadyLogged) {
1244       lazy.ExtensionActivityLog.log(
1245         context.extension.id,
1246         context.viewType,
1247         "api_call",
1248         `${data.path}.addListener`,
1249         { args }
1250       );
1251     }
1252   },
1254   async recvRemoveListener(data) {
1255     let context = this.getContextById(data.childId);
1256     let listener = context.listenerProxies.get(data.listenerId);
1258     let handler = await context.apiCan.asyncFindAPIPath(data.path);
1259     handler.removeListener(listener);
1261     let { alreadyLogged = false } = data;
1262     if (!alreadyLogged) {
1263       lazy.ExtensionActivityLog.log(
1264         context.extension.id,
1265         context.viewType,
1266         "api_call",
1267         `${data.path}.removeListener`,
1268         { args: [] }
1269       );
1270     }
1271   },
1273   getContextById(childId) {
1274     let context = this.proxyContexts.get(childId);
1275     if (!context) {
1276       throw new Error("WebExtension context not found!");
1277     }
1278     return context;
1279   },
1282 ParentAPIManager.init();
1285  * A hidden window which contains the extension pages that are not visible
1286  * (i.e., background pages and devtools pages), and is also used by
1287  * ExtensionDebuggingUtils to contain the browser elements used by the
1288  * addon debugger to connect to the devtools actors running in the same
1289  * process of the target extension (and be able to stay connected across
1290  *  the addon reloads).
1291  */
1292 class HiddenXULWindow {
1293   constructor() {
1294     this._windowlessBrowser = null;
1295     this.unloaded = false;
1296     this.waitInitialized = this.initWindowlessBrowser();
1297   }
1299   shutdown() {
1300     if (this.unloaded) {
1301       throw new Error(
1302         "Unable to shutdown an unloaded HiddenXULWindow instance"
1303       );
1304     }
1306     this.unloaded = true;
1308     this.waitInitialized = null;
1310     if (!this._windowlessBrowser) {
1311       Cu.reportError("HiddenXULWindow was shut down while it was loading.");
1312       // initWindowlessBrowser will close windowlessBrowser when possible.
1313       return;
1314     }
1316     this._windowlessBrowser.close();
1317     this._windowlessBrowser = null;
1318   }
1320   get chromeDocument() {
1321     return this._windowlessBrowser.document;
1322   }
1324   /**
1325    * Private helper that create a HTMLDocument in a windowless browser.
1326    *
1327    * @returns {Promise<void>}
1328    *          A promise which resolves when the windowless browser is ready.
1329    */
1330   async initWindowlessBrowser() {
1331     if (this.waitInitialized) {
1332       throw new Error("HiddenXULWindow already initialized");
1333     }
1335     // The invisible page is currently wrapped in a XUL window to fix an issue
1336     // with using the canvas API from a background page (See Bug 1274775).
1337     let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
1339     // The windowless browser is a thin wrapper around a docShell that keeps
1340     // its related resources alive. It implements nsIWebNavigation and
1341     // forwards its methods to the underlying docShell. That .docShell
1342     // needs `QueryInterface(nsIWebNavigation)` to give us access to the
1343     // webNav methods that are already available on the windowless browser.
1344     let chromeShell = windowlessBrowser.docShell;
1345     chromeShell.QueryInterface(Ci.nsIWebNavigation);
1347     if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
1348       let attrs = chromeShell.getOriginAttributes();
1349       attrs.privateBrowsingId = 1;
1350       chromeShell.setOriginAttributes(attrs);
1351     }
1353     windowlessBrowser.browsingContext.useGlobalHistory = false;
1354     chromeShell.loadURI(DUMMY_PAGE_URI, {
1355       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
1356     });
1358     await promiseObserved(
1359       "chrome-document-global-created",
1360       win => win.document == chromeShell.document
1361     );
1362     await promiseDocumentLoaded(windowlessBrowser.document);
1363     if (this.unloaded) {
1364       windowlessBrowser.close();
1365       return;
1366     }
1367     this._windowlessBrowser = windowlessBrowser;
1368   }
1370   /**
1371    * Creates the browser XUL element that will contain the WebExtension Page.
1372    *
1373    * @param {object} xulAttributes
1374    *        An object that contains the xul attributes to set of the newly
1375    *        created browser XUL element.
1376    *
1377    * @returns {Promise<XULElement>}
1378    *          A Promise which resolves to the newly created browser XUL element.
1379    */
1380   async createBrowserElement(xulAttributes) {
1381     if (!xulAttributes || Object.keys(xulAttributes).length === 0) {
1382       throw new Error("missing mandatory xulAttributes parameter");
1383     }
1385     await this.waitInitialized;
1387     const chromeDoc = this.chromeDocument;
1389     const browser = chromeDoc.createXULElement("browser");
1390     browser.setAttribute("type", "content");
1391     browser.setAttribute("disableglobalhistory", "true");
1392     browser.setAttribute("messagemanagergroup", "webext-browsers");
1394     for (const [name, value] of Object.entries(xulAttributes)) {
1395       if (value != null) {
1396         browser.setAttribute(name, value);
1397       }
1398     }
1400     let awaitFrameLoader;
1402     if (browser.getAttribute("remote") === "true") {
1403       awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
1404     }
1406     chromeDoc.documentElement.appendChild(browser);
1408     // Forcibly flush layout so that we get a pres shell soon enough, see
1409     // bug 1274775.
1410     browser.getBoundingClientRect();
1412     await awaitFrameLoader;
1413     return browser;
1414   }
1417 const SharedWindow = {
1418   _window: null,
1419   _count: 0,
1421   acquire() {
1422     if (this._window == null) {
1423       if (this._count != 0) {
1424         throw new Error(
1425           `Shared window already exists with count ${this._count}`
1426         );
1427       }
1429       this._window = new HiddenXULWindow();
1430     }
1432     this._count++;
1433     return this._window;
1434   },
1436   release() {
1437     if (this._count < 1) {
1438       throw new Error(`Releasing shared window with count ${this._count}`);
1439     }
1441     this._count--;
1442     if (this._count == 0) {
1443       this._window.shutdown();
1444       this._window = null;
1445     }
1446   },
1450  * This is a base class used by the ext-backgroundPage and ext-devtools API implementations
1451  * to inherits the shared boilerplate code needed to create a parent document for the hidden
1452  * extension pages (e.g. the background page, the devtools page) in the BackgroundPage and
1453  * DevToolsPage classes.
1455  * @param {Extension} extension
1456  *        The Extension which owns the hidden extension page created (used to decide
1457  *        if the hidden extension page parent doc is going to be a windowlessBrowser or
1458  *        a visible XUL window).
1459  * @param {string} viewType
1460  *        The viewType of the WebExtension page that is going to be loaded
1461  *        in the created browser element (e.g. "background" or "devtools_page").
1462  */
1463 class HiddenExtensionPage {
1464   constructor(extension, viewType) {
1465     if (!extension || !viewType) {
1466       throw new Error("extension and viewType parameters are mandatory");
1467     }
1469     this.extension = extension;
1470     this.viewType = viewType;
1471     this.browser = null;
1472     this.unloaded = false;
1473   }
1475   /**
1476    * Destroy the created parent document.
1477    */
1478   shutdown() {
1479     if (this.unloaded) {
1480       throw new Error(
1481         "Unable to shutdown an unloaded HiddenExtensionPage instance"
1482       );
1483     }
1485     this.unloaded = true;
1487     if (this.browser) {
1488       this._releaseBrowser();
1489     }
1490   }
1492   _releaseBrowser() {
1493     this.browser.remove();
1494     this.browser = null;
1495     SharedWindow.release();
1496   }
1498   /**
1499    * Creates the browser XUL element that will contain the WebExtension Page.
1500    *
1501    * @returns {Promise<XULElement>}
1502    *          A Promise which resolves to the newly created browser XUL element.
1503    */
1504   async createBrowserElement() {
1505     if (this.browser) {
1506       throw new Error("createBrowserElement called twice");
1507     }
1509     let window = SharedWindow.acquire();
1510     try {
1511       this.browser = await window.createBrowserElement({
1512         "webextension-view-type": this.viewType,
1513         remote: this.extension.remote ? "true" : null,
1514         remoteType: this.extension.remoteType,
1515         initialBrowsingContextGroupId: this.extension.browsingContextGroupId,
1516       });
1517     } catch (e) {
1518       SharedWindow.release();
1519       throw e;
1520     }
1522     if (this.unloaded) {
1523       this._releaseBrowser();
1524       throw new Error("Extension shut down before browser element was created");
1525     }
1527     return this.browser;
1528   }
1532  * This object provides utility functions needed by the devtools actors to
1533  * be able to connect and debug an extension (which can run in the main or in
1534  * a child extension process).
1535  */
1536 const DebugUtils = {
1537   // A lazily created hidden XUL window, which contains the browser elements
1538   // which are used to connect the webextension patent actor to the extension process.
1539   hiddenXULWindow: null,
1541   // Map<extensionId, Promise<XULElement>>
1542   debugBrowserPromises: new Map(),
1543   // DefaultWeakMap<Promise<browser XULElement>, Set<WebExtensionParentActor>>
1544   debugActors: new DefaultWeakMap(() => new Set()),
1546   _extensionUpdatedWatcher: null,
1547   watchExtensionUpdated() {
1548     if (!this._extensionUpdatedWatcher) {
1549       // Watch the updated extension objects.
1550       this._extensionUpdatedWatcher = async (evt, extension) => {
1551         const browserPromise = this.debugBrowserPromises.get(extension.id);
1552         if (browserPromise) {
1553           const browser = await browserPromise;
1554           if (
1555             browser.isRemoteBrowser !== extension.remote &&
1556             this.debugBrowserPromises.get(extension.id) === browserPromise
1557           ) {
1558             // If the cached browser element is not anymore of the same
1559             // remote type of the extension, remove it.
1560             this.debugBrowserPromises.delete(extension.id);
1561             browser.remove();
1562           }
1563         }
1564       };
1566       apiManager.on("ready", this._extensionUpdatedWatcher);
1567     }
1568   },
1570   unwatchExtensionUpdated() {
1571     if (this._extensionUpdatedWatcher) {
1572       apiManager.off("ready", this._extensionUpdatedWatcher);
1573       delete this._extensionUpdatedWatcher;
1574     }
1575   },
1577   getExtensionManifestWarnings(id) {
1578     const addon = GlobalManager.extensionMap.get(id);
1579     if (addon) {
1580       return addon.warnings;
1581     }
1582     return [];
1583   },
1585   /**
1586    * Determine if the extension does have a non-persistent background script
1587    * (either an event page or a background service worker):
1588    *
1589    * Based on this the DevTools client will determine if this extension should provide
1590    * to the extension developers a button to forcefully terminate the background
1591    * script.
1592    *
1593    * @param {string} addonId
1594    *   The id of the addon
1595    *
1596    * @returns {void|boolean}
1597    *   - undefined => does not apply (no background script in the manifest)
1598    *   - true => the background script is persistent.
1599    *   - false => the background script is an event page or a service worker.
1600    */
1601   hasPersistentBackgroundScript(addonId) {
1602     const policy = WebExtensionPolicy.getByID(addonId);
1604     // The addon doesn't have any background script or we
1605     // can't be sure yet.
1606     if (
1607       policy?.extension?.type !== "extension" ||
1608       !policy?.extension?.manifest?.background
1609     ) {
1610       return undefined;
1611     }
1613     return policy.extension.persistentBackground;
1614   },
1616   /**
1617    * Determine if the extension background page is running.
1618    *
1619    * Based on this the DevTools client will show the status of the background
1620    * script in about:debugging.
1621    *
1622    * @param {string} addonId
1623    *   The id of the addon
1624    *
1625    * @returns {void|boolean}
1626    *   - undefined => does not apply (no background script in the manifest)
1627    *   - true => the background script is running.
1628    *   - false => the background script is stopped.
1629    */
1630   isBackgroundScriptRunning(addonId) {
1631     const policy = WebExtensionPolicy.getByID(addonId);
1633     // The addon doesn't have any background script or we
1634     // can't be sure yet.
1635     if (!(this.hasPersistentBackgroundScript(addonId) === false)) {
1636       return undefined;
1637     }
1639     const views = policy?.extension?.views || [];
1640     for (const view of views) {
1641       if (
1642         view.viewType === "background" ||
1643         (view.viewType === "background_worker" && !view.unloaded)
1644       ) {
1645         return true;
1646       }
1647     }
1649     return false;
1650   },
1652   async terminateBackgroundScript(addonId) {
1653     // Terminate the background if the extension does have
1654     // a non-persistent background script (event page or background
1655     // service worker).
1656     if (this.hasPersistentBackgroundScript(addonId) === false) {
1657       const policy = WebExtensionPolicy.getByID(addonId);
1658       // When the event page is being terminated through the Devtools
1659       // action, we should terminate it even if there are DevTools
1660       // toolboxes attached to the extension.
1661       return policy.extension.terminateBackground({
1662         ignoreDevToolsAttached: true,
1663       });
1664     }
1665     throw Error(`Unable to terminate background script for ${addonId}`);
1666   },
1668   /**
1669    * Determine whether a devtools toolbox attached to the extension.
1670    *
1671    * This method is called by the background page idle timeout handler,
1672    * to inhibit terminating the event page when idle while the extension
1673    * developer is debugging the extension through the Addon Debugging window
1674    * (similarly to how service workers are kept alive while the devtools are
1675    * attached).
1676    *
1677    * @param {string} id
1678    *        The id of the extension.
1679    *
1680    * @returns {boolean}
1681    *          true when a devtools toolbox is attached to an extension with
1682    *          the given id, false otherwise.
1683    */
1684   hasDevToolsAttached(id) {
1685     return this.debugBrowserPromises.has(id);
1686   },
1688   /**
1689    * Retrieve a XUL browser element which has been configured to be able to connect
1690    * the devtools actor with the process where the extension is running.
1691    *
1692    * @param {WebExtensionParentActor} webExtensionParentActor
1693    *        The devtools actor that is retrieving the browser element.
1694    *
1695    * @returns {Promise<XULElement>}
1696    *          A promise which resolves to the configured browser XUL element.
1697    */
1698   async getExtensionProcessBrowser(webExtensionParentActor) {
1699     const extensionId = webExtensionParentActor.addonId;
1700     const extension = GlobalManager.getExtension(extensionId);
1701     if (!extension) {
1702       throw new Error(`Extension not found: ${extensionId}`);
1703     }
1705     const createBrowser = () => {
1706       if (!this.hiddenXULWindow) {
1707         this.hiddenXULWindow = new HiddenXULWindow();
1708         this.watchExtensionUpdated();
1709       }
1711       return this.hiddenXULWindow.createBrowserElement({
1712         "webextension-addon-debug-target": extensionId,
1713         remote: extension.remote ? "true" : null,
1714         remoteType: extension.remoteType,
1715         initialBrowsingContextGroupId: extension.browsingContextGroupId,
1716       });
1717     };
1719     let browserPromise = this.debugBrowserPromises.get(extensionId);
1721     // Create a new promise if there is no cached one in the map.
1722     if (!browserPromise) {
1723       browserPromise = createBrowser();
1724       this.debugBrowserPromises.set(extensionId, browserPromise);
1725       browserPromise.then(browser => {
1726         browserPromise.browser = browser;
1727       });
1728       browserPromise.catch(e => {
1729         Cu.reportError(e);
1730         this.debugBrowserPromises.delete(extensionId);
1731       });
1732     }
1734     this.debugActors.get(browserPromise).add(webExtensionParentActor);
1736     return browserPromise;
1737   },
1739   getFrameLoader(extensionId) {
1740     let promise = this.debugBrowserPromises.get(extensionId);
1741     return promise && promise.browser && promise.browser.frameLoader;
1742   },
1744   /**
1745    * Given the devtools actor that has retrieved an addon debug browser element,
1746    * it destroys the XUL browser element, and it also destroy the hidden XUL window
1747    * if it is not currently needed.
1748    *
1749    * @param {WebExtensionParentActor} webExtensionParentActor
1750    *        The devtools actor that has retrieved an addon debug browser element.
1751    */
1752   async releaseExtensionProcessBrowser(webExtensionParentActor) {
1753     const extensionId = webExtensionParentActor.addonId;
1754     const browserPromise = this.debugBrowserPromises.get(extensionId);
1756     if (browserPromise) {
1757       const actorsSet = this.debugActors.get(browserPromise);
1758       actorsSet.delete(webExtensionParentActor);
1759       if (actorsSet.size === 0) {
1760         this.debugActors.delete(browserPromise);
1761         this.debugBrowserPromises.delete(extensionId);
1762         await browserPromise.then(browser => browser.remove());
1763       }
1764     }
1766     if (this.debugBrowserPromises.size === 0 && this.hiddenXULWindow) {
1767       this.hiddenXULWindow.shutdown();
1768       this.hiddenXULWindow = null;
1769       this.unwatchExtensionUpdated();
1770     }
1771   },
1775  * Returns a Promise which resolves with the message data when the given message
1776  * was received by the message manager. The promise is rejected if the message
1777  * manager was closed before a message was received.
1779  * @param {nsIMessageListenerManager} messageManager
1780  *        The message manager on which to listen for messages.
1781  * @param {string} messageName
1782  *        The message to listen for.
1783  * @returns {Promise<*>}
1784  */
1785 function promiseMessageFromChild(messageManager, messageName) {
1786   return new Promise((resolve, reject) => {
1787     let unregister;
1788     function listener(message) {
1789       unregister();
1790       resolve(message.data);
1791     }
1792     function observer(subject, topic, data) {
1793       if (subject === messageManager) {
1794         unregister();
1795         reject(
1796           new Error(
1797             `Message manager was disconnected before receiving ${messageName}`
1798           )
1799         );
1800       }
1801     }
1802     unregister = () => {
1803       Services.obs.removeObserver(observer, "message-manager-close");
1804       messageManager.removeMessageListener(messageName, listener);
1805     };
1806     messageManager.addMessageListener(messageName, listener);
1807     Services.obs.addObserver(observer, "message-manager-close");
1808   });
1811 // This should be called before browser.loadURI is invoked.
1812 async function promiseBackgroundViewLoaded(browser) {
1813   let { childId } = await promiseMessageFromChild(
1814     browser.messageManager,
1815     "Extension:BackgroundViewLoaded"
1816   );
1817   if (childId) {
1818     return ParentAPIManager.getContextById(childId);
1819   }
1823  * This helper is used to subscribe a listener (e.g. in the ext-devtools API implementation)
1824  * to be called for every ExtensionProxyContext created for an extension page given
1825  * its related extension, viewType and browser element (both the top level context and any context
1826  * created for the extension urls running into its iframe descendants).
1828  * @param {object} params
1829  * @param {object} params.extension
1830  *        The Extension on which we are going to listen for the newly created ExtensionProxyContext.
1831  * @param {string} params.viewType
1832  *        The viewType of the WebExtension page that we are watching (e.g. "background" or
1833  *        "devtools_page").
1834  * @param {XULElement} params.browser
1835  *        The browser element of the WebExtension page that we are watching.
1836  * @param {Function} onExtensionProxyContextLoaded
1837  *        The callback that is called when a new context has been loaded (as `callback(context)`);
1839  * @returns {Function}
1840  *          Unsubscribe the listener.
1841  */
1842 function watchExtensionProxyContextLoad(
1843   { extension, viewType, browser },
1844   onExtensionProxyContextLoaded
1845 ) {
1846   if (typeof onExtensionProxyContextLoaded !== "function") {
1847     throw new Error("Missing onExtensionProxyContextLoaded handler");
1848   }
1850   const listener = (event, context) => {
1851     if (context.viewType == viewType && context.xulBrowser == browser) {
1852       onExtensionProxyContextLoaded(context);
1853     }
1854   };
1856   extension.on("extension-proxy-context-load", listener);
1858   return () => {
1859     extension.off("extension-proxy-context-load", listener);
1860   };
1864  * This helper is used to subscribe a listener (e.g. in the ext-backgroundPage)
1865  * to be called for every ExtensionProxyContext created for an extension
1866  * background service worker given its related extension.
1868  * @param {object} params
1869  * @param {object} params.extension
1870  *        The Extension on which we are going to listen for the newly created ExtensionProxyContext.
1871  * @param {Function} onExtensionWorkerContextLoaded
1872  *        The callback that is called when the worker script has been fully loaded (as `callback(context)`);
1874  * @returns {Function}
1875  *          Unsubscribe the listener.
1876  */
1877 function watchExtensionWorkerContextLoaded(
1878   { extension },
1879   onExtensionWorkerContextLoaded
1880 ) {
1881   if (typeof onExtensionWorkerContextLoaded !== "function") {
1882     throw new Error("Missing onExtensionWorkerContextLoaded handler");
1883   }
1885   const listener = (event, context) => {
1886     if (context.viewType == "background_worker") {
1887       onExtensionWorkerContextLoaded(context);
1888     }
1889   };
1891   extension.on("extension-proxy-context-load:completed", listener);
1893   return () => {
1894     extension.off("extension-proxy-context-load:completed", listener);
1895   };
1898 // Manages icon details for toolbar buttons in the |pageAction| and
1899 // |browserAction| APIs.
1900 let IconDetails = {
1901   DEFAULT_ICON: "chrome://mozapps/skin/extensions/extensionGeneric.svg",
1903   // WeakMap<Extension -> Map<url-string -> Map<iconType-string -> object>>>
1904   iconCache: new DefaultWeakMap(() => {
1905     return new DefaultMap(() => new DefaultMap(() => new Map()));
1906   }),
1908   // Normalizes the various acceptable input formats into an object
1909   // with icon size as key and icon URL as value.
1910   //
1911   // If a context is specified (function is called from an extension):
1912   // Throws an error if an invalid icon size was provided or the
1913   // extension is not allowed to load the specified resources.
1914   //
1915   // If no context is specified, instead of throwing an error, this
1916   // function simply logs a warning message.
1917   normalize(details, extension, context = null) {
1918     if (!details.imageData && details.path != null) {
1919       // Pick a cache key for the icon paths. If the path is a string,
1920       // use it directly. Otherwise, stringify the path object.
1921       let key = details.path;
1922       if (typeof key !== "string") {
1923         key = uneval(key);
1924       }
1926       let icons = this.iconCache
1927         .get(extension)
1928         .get(context && context.uri.spec)
1929         .get(details.iconType);
1931       let icon = icons.get(key);
1932       if (!icon) {
1933         icon = this._normalize(details, extension, context);
1934         icons.set(key, icon);
1935       }
1936       return icon;
1937     }
1939     return this._normalize(details, extension, context);
1940   },
1942   _normalize(details, extension, context = null) {
1943     let result = {};
1945     try {
1946       let { imageData, path, themeIcons } = details;
1948       if (imageData) {
1949         if (typeof imageData == "string") {
1950           imageData = { 19: imageData };
1951         }
1953         for (let size of Object.keys(imageData)) {
1954           result[size] = imageData[size];
1955         }
1956       }
1958       let baseURI = context ? context.uri : extension.baseURI;
1960       if (path != null) {
1961         if (typeof path != "object") {
1962           path = { 19: path };
1963         }
1965         for (let size of Object.keys(path)) {
1966           let url = path[size];
1967           if (url) {
1968             url = baseURI.resolve(path[size]);
1970             // The Chrome documentation specifies these parameters as
1971             // relative paths. We currently accept absolute URLs as well,
1972             // which means we need to check that the extension is allowed
1973             // to load them. This will throw an error if it's not allowed.
1974             this._checkURL(url, extension);
1975           }
1976           result[size] = url || this.DEFAULT_ICON;
1977         }
1978       }
1980       if (themeIcons) {
1981         themeIcons.forEach(({ size, light, dark }) => {
1982           let lightURL = baseURI.resolve(light);
1983           let darkURL = baseURI.resolve(dark);
1985           this._checkURL(lightURL, extension);
1986           this._checkURL(darkURL, extension);
1988           let defaultURL = result[size] || result[19]; // always fallback to default first
1989           result[size] = {
1990             default: defaultURL || darkURL, // Fallback to the dark url if no default is specified.
1991             light: lightURL,
1992             dark: darkURL,
1993           };
1994         });
1995       }
1996     } catch (e) {
1997       // Function is called from extension code, delegate error.
1998       if (context) {
1999         throw e;
2000       }
2001       // If there's no context, it's because we're handling this
2002       // as a manifest directive. Log a warning rather than
2003       // raising an error.
2004       extension.manifestError(`Invalid icon data: ${e}`);
2005     }
2007     return result;
2008   },
2010   // Checks if the extension is allowed to load the given URL with the specified principal.
2011   // This will throw an error if the URL is not allowed.
2012   _checkURL(url, extension) {
2013     if (!extension.checkLoadURL(url, { allowInheritsPrincipal: true })) {
2014       throw new ExtensionError(`Illegal URL ${url}`);
2015     }
2016   },
2018   // Returns the appropriate icon URL for the given icons object and the
2019   // screen resolution of the given window.
2020   getPreferredIcon(icons, extension = null, size = 16) {
2021     const DEFAULT = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
2023     let bestSize = null;
2024     if (icons[size]) {
2025       bestSize = size;
2026     } else if (icons[2 * size]) {
2027       bestSize = 2 * size;
2028     } else {
2029       let sizes = Object.keys(icons)
2030         .map(key => parseInt(key, 10))
2031         .sort((a, b) => a - b);
2033       bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
2034     }
2036     if (bestSize) {
2037       return { size: bestSize, icon: icons[bestSize] || DEFAULT };
2038     }
2040     return { size, icon: DEFAULT };
2041   },
2043   // These URLs should already be properly escaped, but make doubly sure CSS
2044   // string escape characters are escaped here, since they could lead to a
2045   // sandbox break.
2046   escapeUrl(url) {
2047     return url.replace(/[\\\s"]/g, encodeURIComponent);
2048   },
2051 class CacheStore {
2052   constructor(storeName) {
2053     this.storeName = storeName;
2054   }
2056   async getStore(path = null) {
2057     let data = await StartupCache.dataPromise;
2059     let store = data.get(this.storeName);
2060     if (!store) {
2061       store = new Map();
2062       data.set(this.storeName, store);
2063     }
2065     let key = path;
2066     if (Array.isArray(path)) {
2067       for (let elem of path.slice(0, -1)) {
2068         let next = store.get(elem);
2069         if (!next) {
2070           next = new Map();
2071           store.set(elem, next);
2072         }
2073         store = next;
2074       }
2075       key = path[path.length - 1];
2076     }
2078     return [store, key];
2079   }
2081   async get(path, createFunc) {
2082     let [store, key] = await this.getStore(path);
2084     let result = store.get(key);
2086     if (result === undefined) {
2087       result = await createFunc(path);
2088       store.set(key, result);
2089       StartupCache.save();
2090     }
2092     return result;
2093   }
2095   async set(path, value) {
2096     let [store, key] = await this.getStore(path);
2098     store.set(key, value);
2099     StartupCache.save();
2100   }
2102   async getAll() {
2103     let [store] = await this.getStore();
2105     return new Map(store);
2106   }
2108   async delete(path) {
2109     let [store, key] = await this.getStore(path);
2111     if (store.delete(key)) {
2112       StartupCache.save();
2113     }
2114   }
2117 // A cache to support faster initialization of extensions at browser startup.
2118 // All cached data is removed when the browser is updated.
2119 // Extension-specific data is removed when the add-on is updated.
2120 var StartupCache = {
2121   _ensureDirectoryPromise: null,
2122   _saveTask: null,
2124   _ensureDirectory() {
2125     if (this._ensureDirectoryPromise === null) {
2126       this._ensureDirectoryPromise = IOUtils.makeDirectory(
2127         PathUtils.parent(this.file),
2128         {
2129           ignoreExisting: true,
2130           createAncestors: true,
2131         }
2132       );
2133     }
2135     return this._ensureDirectoryPromise;
2136   },
2138   // When the application version changes, this file is removed by
2139   // RemoveComponentRegistries in nsAppRunner.cpp.
2140   file: PathUtils.join(
2141     Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
2142     "startupCache",
2143     "webext.sc.lz4"
2144   ),
2146   async _saveNow() {
2147     let data = new Uint8Array(lazy.aomStartup.encodeBlob(this._data));
2148     await this._ensureDirectoryPromise;
2149     await IOUtils.write(this.file, data, { tmpPath: `${this.file}.tmp` });
2151     Glean.extensions.startupCacheWriteBytelength.set(data.byteLength);
2152   },
2154   save() {
2155     this._ensureDirectory();
2157     if (!this._saveTask) {
2158       this._saveTask = new lazy.DeferredTask(() => this._saveNow(), 5000);
2160       IOUtils.profileBeforeChange.addBlocker(
2161         "Flush WebExtension StartupCache",
2162         async () => {
2163           await this._saveTask.finalize();
2164           this._saveTask = null;
2165         }
2166       );
2167     }
2169     return this._saveTask.arm();
2170   },
2172   _data: null,
2173   async _readData() {
2174     let result = new Map();
2175     try {
2176       Glean.extensions.startupCacheLoadTime.start();
2177       let { buffer } = await IOUtils.read(this.file);
2179       result = lazy.aomStartup.decodeBlob(buffer);
2180       Glean.extensions.startupCacheLoadTime.stop();
2181     } catch (e) {
2182       Glean.extensions.startupCacheLoadTime.cancel();
2183       if (!DOMException.isInstance(e) || e.name !== "NotFoundError") {
2184         Cu.reportError(e);
2185       }
2186       let error = lazy.getErrorNameForTelemetry(e);
2187       Glean.extensions.startupCacheReadErrors[error].add(1);
2188     }
2190     this._data = result;
2191     return result;
2192   },
2194   get dataPromise() {
2195     if (!this._dataPromise) {
2196       this._dataPromise = this._readData();
2197     }
2198     return this._dataPromise;
2199   },
2201   clearAddonData(id) {
2202     return Promise.all([
2203       this.general.delete(id),
2204       this.locales.delete(id),
2205       this.manifests.delete(id),
2206       this.permissions.delete(id),
2207       this.menus.delete(id),
2208     ]).catch(e => {
2209       // Ignore the error. It happens when we try to flush the add-on
2210       // data after the AddonManager has flushed the entire startup cache.
2211     });
2212   },
2214   observe(subject, topic, data) {
2215     if (topic === "startupcache-invalidate") {
2216       this._data = new Map();
2217       this._dataPromise = Promise.resolve(this._data);
2218     }
2219   },
2221   get(extension, path, createFunc) {
2222     return this.general.get(
2223       [extension.id, extension.version, ...path],
2224       createFunc
2225     );
2226   },
2228   delete(extension, path) {
2229     return this.general.delete([extension.id, extension.version, ...path]);
2230   },
2232   general: new CacheStore("general"),
2233   locales: new CacheStore("locales"),
2234   manifests: new CacheStore("manifests"),
2235   other: new CacheStore("other"),
2236   permissions: new CacheStore("permissions"),
2237   schemas: new CacheStore("schemas"),
2238   menus: new CacheStore("menus"),
2241 Services.obs.addObserver(StartupCache, "startupcache-invalidate");
2243 export var ExtensionParent = {
2244   GlobalManager,
2245   HiddenExtensionPage,
2246   IconDetails,
2247   ParentAPIManager,
2248   StartupCache,
2249   WebExtensionPolicy,
2250   apiManager,
2251   promiseBackgroundViewLoaded,
2252   watchExtensionProxyContextLoad,
2253   watchExtensionWorkerContextLoaded,
2254   DebugUtils,
2257 // browserPaintedPromise and browserStartupPromise are promises that
2258 // resolve after the first browser window is painted and after browser
2259 // windows have been restored, respectively. Alternatively,
2260 // browserStartupPromise also resolves from the extensions-late-startup
2261 // notification sent by Firefox Reality on desktop platforms, because it
2262 // doesn't support SessionStore.
2263 // _resetStartupPromises should only be called from outside this file in tests.
2264 ExtensionParent._resetStartupPromises = () => {
2265   ExtensionParent.browserPaintedPromise = promiseObserved(
2266     "browser-delayed-startup-finished"
2267   ).then(() => {});
2268   ExtensionParent.browserStartupPromise = Promise.race([
2269     promiseObserved("sessionstore-windows-restored"),
2270     promiseObserved("extensions-late-startup"),
2271   ]).then(() => {});
2273 ExtensionParent._resetStartupPromises();
2275 ChromeUtils.defineLazyGetter(ExtensionParent, "PlatformInfo", () => {
2276   return Object.freeze({
2277     os: (function () {
2278       let os = AppConstants.platform;
2279       if (os == "macosx") {
2280         os = "mac";
2281       }
2282       return os;
2283     })(),
2284     arch: (function () {
2285       let abi = Services.appinfo.XPCOMABI;
2286       let [arch] = abi.split("-");
2287       if (arch == "x86") {
2288         arch = "x86-32";
2289       } else if (arch == "x86_64") {
2290         arch = "x86-64";
2291       }
2292       return arch;
2293     })(),
2294   });
2298  * Retreives the browser_style stylesheets needed for extension popups and sidebars.
2300  * @returns {Array<string>} an array of stylesheets needed for the current platform.
2301  */
2302 ChromeUtils.defineLazyGetter(ExtensionParent, "extensionStylesheets", () => {
2303   let stylesheets = ["chrome://browser/content/extension.css"];
2305   if (AppConstants.platform === "macosx") {
2306     stylesheets.push("chrome://browser/content/extension-mac.css");
2307   }
2309   return stylesheets;