Bug 1729395 - Handle message sender going away during message processing r=robwu
[gecko.git] / toolkit / components / extensions / ExtensionParent.jsm
blob37d4e7a709dca7d1727fc940d55b2f1a9b973b58
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 "use strict";
8 /**
9  * This module contains code for managing APIs that need to run in the
10  * parent process, and handles the parent side of operations that need
11  * to be proxied from ExtensionChild.jsm.
12  */
14 /* exported ExtensionParent */
16 var EXPORTED_SYMBOLS = ["ExtensionParent"];
18 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
19 const { XPCOMUtils } = ChromeUtils.import(
20   "resource://gre/modules/XPCOMUtils.jsm"
23 XPCOMUtils.defineLazyModuleGetters(this, {
24   AddonManager: "resource://gre/modules/AddonManager.jsm",
25   AppConstants: "resource://gre/modules/AppConstants.jsm",
26   AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
27   BroadcastConduit: "resource://gre/modules/ConduitsParent.jsm",
28   DeferredTask: "resource://gre/modules/DeferredTask.jsm",
29   DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",
30   ExtensionData: "resource://gre/modules/Extension.jsm",
31   ExtensionActivityLog: "resource://gre/modules/ExtensionActivityLog.jsm",
32   GeckoViewConnection: "resource://gre/modules/GeckoViewWebExtension.jsm",
33   MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.jsm",
34   NativeApp: "resource://gre/modules/NativeMessaging.jsm",
35   PerformanceCounters: "resource://gre/modules/PerformanceCounters.jsm",
36   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
37   Schemas: "resource://gre/modules/Schemas.jsm",
38 });
40 XPCOMUtils.defineLazyServiceGetters(this, {
41   aomStartup: [
42     "@mozilla.org/addons/addon-manager-startup;1",
43     "amIAddonManagerStartup",
44   ],
45 });
47 // We're using the pref to avoid loading PerformanceCounters.jsm for nothing.
48 XPCOMUtils.defineLazyPreferenceGetter(
49   this,
50   "gTimingEnabled",
51   "extensions.webextensions.enablePerformanceCounters",
52   false
54 const { ExtensionCommon } = ChromeUtils.import(
55   "resource://gre/modules/ExtensionCommon.jsm"
57 const { ExtensionUtils } = ChromeUtils.import(
58   "resource://gre/modules/ExtensionUtils.jsm"
61 var {
62   BaseContext,
63   CanOfAPIs,
64   SchemaAPIManager,
65   SpreadArgs,
66   defineLazyGetter,
67 } = ExtensionCommon;
69 var {
70   DefaultMap,
71   DefaultWeakMap,
72   ExtensionError,
73   promiseDocumentLoaded,
74   promiseEvent,
75   promiseObserved,
76 } = ExtensionUtils;
78 const ERROR_NO_RECEIVERS =
79   "Could not establish connection. Receiving end does not exist.";
81 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
82 const CATEGORY_EXTENSION_MODULES = "webextension-modules";
83 const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
84 const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
86 let schemaURLs = new Set();
88 schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
90 let GlobalManager;
91 let ParentAPIManager;
92 let StartupCache;
94 const global = this;
96 // This object loads the ext-*.js scripts that define the extension API.
97 let apiManager = new (class extends SchemaAPIManager {
98   constructor() {
99     super("main", Schemas);
100     this.initialized = null;
102     /* eslint-disable mozilla/balanced-listeners */
103     this.on("startup", (e, extension) => {
104       return extension.apiManager.onStartup(extension);
105     });
107     this.on("update", async (e, { id, resourceURI }) => {
108       let modules = this.eventModules.get("update");
109       if (modules.size == 0) {
110         return;
111       }
113       let extension = new ExtensionData(resourceURI);
114       await extension.loadManifest();
116       return Promise.all(
117         Array.from(modules).map(async apiName => {
118           let module = await this.asyncLoadModule(apiName);
119           module.onUpdate(id, extension.manifest);
120         })
121       );
122     });
124     this.on("uninstall", (e, { id }) => {
125       let modules = this.eventModules.get("uninstall");
126       return Promise.all(
127         Array.from(modules).map(async apiName => {
128           let module = await this.asyncLoadModule(apiName);
129           return module.onUninstall(id);
130         })
131       );
132     });
133     /* eslint-enable mozilla/balanced-listeners */
135     // Handle any changes that happened during startup
136     let disabledIds = AddonManager.getStartupChanges(
137       AddonManager.STARTUP_CHANGE_DISABLED
138     );
139     if (disabledIds.length) {
140       this._callHandlers(disabledIds, "disable", "onDisable");
141     }
143     let uninstalledIds = AddonManager.getStartupChanges(
144       AddonManager.STARTUP_CHANGE_UNINSTALLED
145     );
146     if (uninstalledIds.length) {
147       this._callHandlers(uninstalledIds, "uninstall", "onUninstall");
148     }
149   }
151   getModuleJSONURLs() {
152     return Array.from(
153       Services.catMan.enumerateCategory(CATEGORY_EXTENSION_MODULES),
154       ({ value }) => value
155     );
156   }
158   // Loads all the ext-*.js scripts currently registered.
159   lazyInit() {
160     if (this.initialized) {
161       return this.initialized;
162     }
164     let modulesPromise = StartupCache.other.get(["parentModules"], () =>
165       this.loadModuleJSON(this.getModuleJSONURLs())
166     );
168     let scriptURLs = [];
169     for (let { value } of Services.catMan.enumerateCategory(
170       CATEGORY_EXTENSION_SCRIPTS
171     )) {
172       scriptURLs.push(value);
173     }
175     let promise = (async () => {
176       let scripts = await Promise.all(
177         scriptURLs.map(url => ChromeUtils.compileScript(url))
178       );
180       this.initModuleData(await modulesPromise);
182       this.initGlobal();
183       for (let script of scripts) {
184         script.executeInGlobal(this.global);
185       }
187       // Load order matters here. The base manifest defines types which are
188       // extended by other schemas, so needs to be loaded first.
189       return Schemas.load(BASE_SCHEMA).then(() => {
190         let promises = [];
191         for (let { value } of Services.catMan.enumerateCategory(
192           CATEGORY_EXTENSION_SCHEMAS
193         )) {
194           promises.push(Schemas.load(value));
195         }
196         for (let [url, { content }] of this.schemaURLs) {
197           promises.push(Schemas.load(url, content));
198         }
199         for (let url of schemaURLs) {
200           promises.push(Schemas.load(url));
201         }
202         return Promise.all(promises).then(() => {
203           Schemas.updateSharedSchemas();
204         });
205       });
206     })();
208     /* eslint-disable mozilla/balanced-listeners */
209     Services.mm.addMessageListener("Extension:GetTabAndWindowId", this);
210     /* eslint-enable mozilla/balanced-listeners */
212     this.initialized = promise;
213     return this.initialized;
214   }
216   receiveMessage({ name, target, sync }) {
217     if (name === "Extension:GetTabAndWindowId") {
218       let result = this.global.tabTracker.getBrowserData(target);
220       if (result.tabId) {
221         if (sync) {
222           return result;
223         }
224         target.messageManager.sendAsyncMessage(
225           "Extension:SetFrameData",
226           result
227         );
228       }
229     }
230   }
232   // Call static handlers for the given event on the given extension ids,
233   // and set up a shutdown blocker to ensure they all complete.
234   _callHandlers(ids, event, method) {
235     let promises = Array.from(this.eventModules.get(event))
236       .map(async modName => {
237         let module = await this.asyncLoadModule(modName);
238         return ids.map(id => module[method](id));
239       })
240       .flat();
241     if (event === "disable") {
242       promises.push(...ids.map(id => this.emit("disable", id)));
243     }
244     if (event === "enabling") {
245       promises.push(...ids.map(id => this.emit("enabling", id)));
246     }
248     AsyncShutdown.profileBeforeChange.addBlocker(
249       `Extension API ${event} handlers for ${ids.join(",")}`,
250       Promise.all(promises)
251     );
252   }
253 })();
255 // Receives messages related to the extension messaging API and forwards them
256 // to relevant child messengers.  Also handles Native messaging and GeckoView.
257 const ProxyMessenger = {
258   /**
259    * @typedef {object} ParentPort
260    * @prop {function(StructuredCloneHolder)} onPortMessage
261    * @prop {function()} onPortDisconnect
262    */
263   /** @type Map<number, ParentPort> */
264   ports: new Map(),
266   init() {
267     this.conduit = new BroadcastConduit(ProxyMessenger, {
268       id: "ProxyMessenger",
269       reportOnClosed: "portId",
270       recv: ["PortConnect", "PortMessage", "NativeMessage", "RuntimeMessage"],
271       cast: ["PortConnect", "PortMessage", "PortDisconnect", "RuntimeMessage"],
272     });
273   },
275   openNative(nativeApp, sender) {
276     let context = ParentAPIManager.getContextById(sender.childId);
277     if (context.extension.hasPermission("geckoViewAddons")) {
278       return new GeckoViewConnection(
279         this.getSender(context.extension, sender),
280         sender.actor.browsingContext.top.embedderElement,
281         nativeApp,
282         context.extension.hasPermission("nativeMessagingFromContent")
283       );
284     } else if (sender.verified) {
285       return new NativeApp(context, nativeApp);
286     }
287     throw new Error(`Native messaging not allowed: ${JSON.stringify(sender)}`);
288   },
290   recvNativeMessage({ nativeApp, holder }, { sender }) {
291     return this.openNative(nativeApp, sender).sendMessage(holder);
292   },
294   getSender(extension, source) {
295     let sender = {
296       contextId: source.id,
297       id: source.extensionId,
298       envType: source.envType,
299       frameId: source.frameId,
300       url: source.url,
301     };
303     let browser = source.actor.browsingContext.top.embedderElement;
304     let data = browser && apiManager.global.tabTracker.getBrowserData(browser);
305     if (data?.tabId > 0) {
306       sender.tab = extension.tabManager.get(data.tabId, null)?.convert();
307     }
309     return sender;
310   },
312   getTopBrowsingContextId(tabId) {
313     // If a tab alredy has content scripts, no need to check private browsing.
314     let tab = apiManager.global.tabTracker.getTab(tabId, null);
315     if (!tab || (tab.browser || tab).getAttribute("pending") === "true") {
316       // No receivers in discarded tabs, so bail early to keep the browser lazy.
317       throw new ExtensionError(ERROR_NO_RECEIVERS);
318     }
319     let browser = tab.linkedBrowser || tab.browser;
320     return browser.browsingContext.id;
321   },
323   // TODO: Rework/simplify this and getSender/getTopBC after bug 1580766.
324   async normalizeArgs(arg, sender) {
325     arg.extensionId = arg.extensionId || sender.extensionId;
326     let extension = GlobalManager.extensionMap.get(arg.extensionId);
327     if (!extension) {
328       return Promise.reject({ message: ERROR_NO_RECEIVERS });
329     }
330     await extension.wakeupBackground?.();
332     arg.sender = this.getSender(extension, sender);
333     arg.topBC = arg.tabId && this.getTopBrowsingContextId(arg.tabId);
334     return arg.tabId ? "tab" : "messenger";
335   },
337   async recvRuntimeMessage(arg, { sender }) {
338     arg.firstResponse = true;
339     let kind = await this.normalizeArgs(arg, sender);
340     let result = await this.conduit.castRuntimeMessage(kind, arg);
341     if (!result) {
342       // "throw new ExtensionError" cannot be used because then the stack of the
343       // sendMessage call would not be added to the error object generated by
344       // context.normalizeError. Test coverage by test_ext_error_location.js.
345       return Promise.reject({ message: ERROR_NO_RECEIVERS });
346     }
347     return result.value;
348   },
350   async recvPortConnect(arg, { sender }) {
351     if (arg.native) {
352       let port = this.openNative(arg.name, sender).onConnect(arg.portId, this);
353       this.ports.set(arg.portId, port);
354       return;
355     }
357     // PortMessages that follow will need to wait for the port to be opened.
358     let resolvePort;
359     this.ports.set(arg.portId, new Promise(res => (resolvePort = res)));
361     let kind = await this.normalizeArgs(arg, sender);
362     let all = await this.conduit.castPortConnect(kind, arg);
363     resolvePort();
365     // If there are no active onConnect listeners.
366     if (!all.some(x => x.value)) {
367       throw new ExtensionError(ERROR_NO_RECEIVERS);
368     }
369   },
371   async recvPortMessage({ holder }, { sender }) {
372     if (sender.native) {
373       return this.ports.get(sender.portId).onPortMessage(holder);
374     }
375     await this.ports.get(sender.portId);
376     this.sendPortMessage(sender.portId, holder, !sender.source);
377   },
379   recvConduitClosed(sender) {
380     let app = this.ports.get(sender.portId);
381     if (this.ports.delete(sender.portId) && sender.native) {
382       return app.onPortDisconnect();
383     }
384     this.sendPortDisconnect(sender.portId, null, !sender.source);
385   },
387   sendPortMessage(portId, holder, source = true) {
388     this.conduit.castPortMessage("port", { portId, source, holder });
389   },
391   sendPortDisconnect(portId, error, source = true) {
392     this.conduit.castPortDisconnect("port", { portId, source, error });
393     this.ports.delete(portId);
394   },
396 ProxyMessenger.init();
398 // Responsible for loading extension APIs into the right globals.
399 GlobalManager = {
400   // Map[extension ID -> Extension]. Determines which extension is
401   // responsible for content under a particular extension ID.
402   extensionMap: new Map(),
403   initialized: false,
405   init(extension) {
406     if (this.extensionMap.size == 0) {
407       apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
408       this.initialized = true;
409       Services.ppmm.addMessageListener(
410         "Extension:SendPerformanceCounter",
411         this
412       );
413     }
414     this.extensionMap.set(extension.id, extension);
415   },
417   uninit(extension) {
418     this.extensionMap.delete(extension.id);
420     if (this.extensionMap.size == 0 && this.initialized) {
421       apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
422       this.initialized = false;
423       Services.ppmm.removeMessageListener(
424         "Extension:SendPerformanceCounter",
425         this
426       );
427     }
428   },
430   async receiveMessage({ name, data }) {
431     switch (name) {
432       case "Extension:SendPerformanceCounter":
433         PerformanceCounters.merge(data.counters);
434         break;
435     }
436   },
438   _onExtensionBrowser(type, browser, additionalData = {}) {
439     browser.messageManager.loadFrameScript(
440       "resource://gre/modules/onExtensionBrowser.js",
441       false,
442       true
443     );
445     let viewType = browser.getAttribute("webextension-view-type");
446     if (viewType) {
447       let data = { viewType };
449       let { tabTracker } = apiManager.global;
450       Object.assign(data, tabTracker.getBrowserData(browser), additionalData);
452       browser.messageManager.sendAsyncMessage("Extension:SetFrameData", data);
453     }
454   },
456   getExtension(extensionId) {
457     return this.extensionMap.get(extensionId);
458   },
462  * The proxied parent side of a context in ExtensionChild.jsm, for the
463  * parent side of a proxied API.
464  */
465 class ProxyContextParent extends BaseContext {
466   constructor(envType, extension, params, xulBrowser, principal) {
467     super(envType, extension);
469     this.uri = Services.io.newURI(params.url);
471     this.incognito = params.incognito;
473     this.listenerPromises = new Set();
475     // This message manager is used by ParentAPIManager to send messages and to
476     // close the ProxyContext if the underlying message manager closes. This
477     // message manager object may change when `xulBrowser` swaps docshells, e.g.
478     // when a tab is moved to a different window.
479     this.messageManagerProxy = new MessageManagerProxy(xulBrowser);
481     Object.defineProperty(this, "principal", {
482       value: principal,
483       enumerable: true,
484       configurable: true,
485     });
487     this.listenerProxies = new Map();
489     this.pendingEventBrowser = null;
491     apiManager.emit("proxy-context-load", this);
492   }
494   async withPendingBrowser(browser, callable) {
495     let savedBrowser = this.pendingEventBrowser;
496     this.pendingEventBrowser = browser;
497     try {
498       let result = await callable();
499       return result;
500     } finally {
501       this.pendingEventBrowser = savedBrowser;
502     }
503   }
505   logActivity(type, name, data) {
506     // The base class will throw so we catch any subclasses that do not implement.
507     // We do not want to throw here, but we also do not log here.
508   }
510   get cloneScope() {
511     return this.sandbox;
512   }
514   applySafe(callback, args) {
515     // There's no need to clone when calling listeners for a proxied
516     // context.
517     return this.applySafeWithoutClone(callback, args);
518   }
520   get xulBrowser() {
521     return this.messageManagerProxy.eventTarget;
522   }
524   get parentMessageManager() {
525     return this.messageManagerProxy.messageManager;
526   }
528   shutdown() {
529     this.unload();
530   }
532   unload() {
533     if (this.unloaded) {
534       return;
535     }
536     this.messageManagerProxy.dispose();
537     super.unload();
538     apiManager.emit("proxy-context-unload", this);
539   }
542 defineLazyGetter(ProxyContextParent.prototype, "apiCan", function() {
543   let obj = {};
544   let can = new CanOfAPIs(this, this.extension.apiManager, obj);
545   return can;
548 defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() {
549   return this.apiCan.root;
552 defineLazyGetter(ProxyContextParent.prototype, "sandbox", function() {
553   // NOTE: the required Blob and URL globals are used in the ext-registerContentScript.js
554   // API module to convert JS and CSS data into blob URLs.
555   return Cu.Sandbox(this.principal, {
556     sandboxName: this.uri.spec,
557     wantGlobalProperties: ["Blob", "URL"],
558   });
562  * The parent side of proxied API context for extension content script
563  * running in ExtensionContent.jsm.
564  */
565 class ContentScriptContextParent extends ProxyContextParent {}
568  * The parent side of proxied API context for extension page, such as a
569  * background script, a tab page, or a popup, running in
570  * ExtensionChild.jsm.
571  */
572 class ExtensionPageContextParent extends ProxyContextParent {
573   constructor(envType, extension, params, xulBrowser) {
574     super(envType, extension, params, xulBrowser, extension.principal);
576     this.viewType = params.viewType;
578     this.extension.views.add(this);
580     extension.emit("extension-proxy-context-load", this);
581   }
583   // The window that contains this context. This may change due to moving tabs.
584   get appWindow() {
585     let win = this.xulBrowser.ownerGlobal;
586     return win.browsingContext.topChromeWindow;
587   }
589   get currentWindow() {
590     if (this.viewType !== "background") {
591       return this.appWindow;
592     }
593   }
595   get tabId() {
596     let { tabTracker } = apiManager.global;
597     let data = tabTracker.getBrowserData(this.xulBrowser);
598     if (data.tabId >= 0) {
599       return data.tabId;
600     }
601   }
603   onBrowserChange(browser) {
604     super.onBrowserChange(browser);
605     this.xulBrowser = browser;
606   }
608   unload() {
609     super.unload();
610     this.extension.views.delete(this);
611   }
613   shutdown() {
614     apiManager.emit("page-shutdown", this);
615     super.shutdown();
616   }
620  * The parent side of proxied API context for devtools extension page, such as a
621  * devtools pages and panels running in ExtensionChild.jsm.
622  */
623 class DevToolsExtensionPageContextParent extends ExtensionPageContextParent {
624   constructor(...params) {
625     super(...params);
627     // Set all attributes that are lazily defined to `null` here.
628     //
629     // Note that we can't do that for `this._devToolsToolbox` because it will
630     // be defined when calling our parent constructor and so would override it back to `null`.
631     this._devToolsCommands = null;
632     this._onNavigatedListeners = null;
634     this._onResourceAvailable = this._onResourceAvailable.bind(this);
635   }
637   set devToolsToolbox(toolbox) {
638     if (this._devToolsToolbox) {
639       throw new Error("Cannot set the context DevTools toolbox twice");
640     }
642     this._devToolsToolbox = toolbox;
643   }
645   get devToolsToolbox() {
646     return this._devToolsToolbox;
647   }
649   async addOnNavigatedListener(listener) {
650     if (!this._onNavigatedListeners) {
651       this._onNavigatedListeners = new Set();
653       await this.devToolsToolbox.resourceCommand.watchResources(
654         [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
655         {
656           onAvailable: this._onResourceAvailable,
657           ignoreExistingResources: true,
658         }
659       );
660     }
662     this._onNavigatedListeners.add(listener);
663   }
665   removeOnNavigatedListener(listener) {
666     if (this._onNavigatedListeners) {
667       this._onNavigatedListeners.delete(listener);
668     }
669   }
671   /**
672    * The returned "commands" object, exposing modules implemented from devtools/shared/commands.
673    * Each attribute being a static interface to communicate with the server backend.
674    *
675    * @returns {Promise<Object>}
676    */
677   async getDevToolsCommands() {
678     // Ensure that we try to instantiate a commands only once,
679     // even if createCommandsForTabForWebExtension is async.
680     if (this._devToolsCommandsPromise) {
681       return this._devToolsCommandsPromise;
682     }
683     if (this._devToolsCommands) {
684       return this._devToolsCommands;
685     }
687     this._devToolsCommandsPromise = (async () => {
688       const commands = await DevToolsShim.createCommandsForTabForWebExtension(
689         this.devToolsToolbox.descriptorFront.localTab
690       );
691       await commands.targetCommand.startListening();
692       this._devToolsCommands = commands;
693       this._devToolsCommandsPromise = null;
694       return commands;
695     })();
696     return this._devToolsCommandsPromise;
697   }
699   unload() {
700     // Bail if the toolbox reference was already cleared.
701     if (!this.devToolsToolbox) {
702       return;
703     }
705     if (this._onNavigatedListeners) {
706       this.devToolsToolbox.resourceCommand.unwatchResources(
707         [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
708         { onAvailable: this._onResourceAvailable }
709       );
710     }
712     if (this._devToolsCommands) {
713       this._devToolsCommands.destroy();
714       this._devToolsCommands = null;
715     }
717     if (this._onNavigatedListeners) {
718       this._onNavigatedListeners.clear();
719       this._onNavigatedListeners = null;
720     }
722     this._devToolsToolbox = null;
724     super.unload();
725   }
727   async _onResourceAvailable(resources) {
728     for (const resource of resources) {
729       const { targetFront } = resource;
730       if (targetFront.isTopLevel && resource.name === "dom-complete") {
731         const url = targetFront.localTab.linkedBrowser.currentURI.spec;
732         for (const listener of this._onNavigatedListeners) {
733           listener(url);
734         }
735       }
736     }
737   }
740 ParentAPIManager = {
741   proxyContexts: new Map(),
743   init() {
744     // TODO: Bug 1595186 - remove/replace all usage of MessageManager below.
745     Services.obs.addObserver(this, "message-manager-close");
747     this.conduit = new BroadcastConduit(this, {
748       id: "ParentAPIManager",
749       reportOnClosed: "childId",
750       recv: ["CreateProxyContext", "APICall", "AddListener", "RemoveListener"],
751       send: ["CallResult"],
752       query: ["RunListener"],
753     });
754   },
756   attachMessageManager(extension, processMessageManager) {
757     extension.parentMessageManager = processMessageManager;
758   },
760   async observe(subject, topic, data) {
761     if (topic === "message-manager-close") {
762       let mm = subject;
763       for (let [childId, context] of this.proxyContexts) {
764         if (context.parentMessageManager === mm) {
765           this.closeProxyContext(childId);
766         }
767       }
769       // Reset extension message managers when their child processes shut down.
770       for (let extension of GlobalManager.extensionMap.values()) {
771         if (extension.parentMessageManager === mm) {
772           extension.parentMessageManager = null;
773         }
774       }
775     }
776   },
778   shutdownExtension(extensionId, reason) {
779     if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(reason)) {
780       apiManager._callHandlers([extensionId], "disable", "onDisable");
781     }
783     for (let [childId, context] of this.proxyContexts) {
784       if (context.extension.id == extensionId) {
785         context.shutdown();
786         this.proxyContexts.delete(childId);
787       }
788     }
789   },
791   recvCreateProxyContext(data, { actor, sender }) {
792     let { envType, extensionId, childId, principal } = data;
793     let target = actor.browsingContext.top.embedderElement;
795     if (this.proxyContexts.has(childId)) {
796       throw new Error(
797         "A WebExtension context with the given ID already exists!"
798       );
799     }
801     let extension = GlobalManager.getExtension(extensionId);
802     if (!extension) {
803       throw new Error(`No WebExtension found with ID ${extensionId}`);
804     }
806     let context;
807     if (envType == "addon_parent" || envType == "devtools_parent") {
808       if (!sender.verified) {
809         throw new Error(`Bad sender context envType: ${sender.envType}`);
810       }
812       let processMessageManager =
813         target.messageManager.processMessageManager ||
814         Services.ppmm.getChildAt(0);
816       if (!extension.parentMessageManager) {
817         if (target.remoteType === extension.remoteType) {
818           this.attachMessageManager(extension, processMessageManager);
819         }
820       }
822       if (processMessageManager !== extension.parentMessageManager) {
823         throw new Error(
824           "Attempt to create privileged extension parent from incorrect child process"
825         );
826       }
828       if (envType == "addon_parent") {
829         context = new ExtensionPageContextParent(
830           envType,
831           extension,
832           data,
833           target
834         );
835       } else if (envType == "devtools_parent") {
836         context = new DevToolsExtensionPageContextParent(
837           envType,
838           extension,
839           data,
840           target
841         );
842       }
843     } else if (envType == "content_parent") {
844       context = new ContentScriptContextParent(
845         envType,
846         extension,
847         data,
848         target,
849         principal
850       );
851     } else {
852       throw new Error(`Invalid WebExtension context envType: ${envType}`);
853     }
854     this.proxyContexts.set(childId, context);
855   },
857   recvConduitClosed(sender) {
858     this.closeProxyContext(sender.id);
859   },
861   closeProxyContext(childId) {
862     let context = this.proxyContexts.get(childId);
863     if (context) {
864       context.unload();
865       this.proxyContexts.delete(childId);
866     }
867   },
869   async retrievePerformanceCounters() {
870     // getting the parent counters
871     return PerformanceCounters.getData();
872   },
874   /**
875    * Call the given function and also log the call as appropriate
876    * (i.e., with PerformanceCounters and/or activity logging)
877    *
878    * @param {BaseContext} context The context making this call.
879    * @param {object} data Additional data about the call.
880    * @param {function} callable The actual implementation to invoke.
881    */
882   async callAndLog(context, data, callable) {
883     let { id } = context.extension;
884     // If we were called via callParentAsyncFunction we don't want
885     // to log again, check for the flag.
886     const { alreadyLogged } = data.options || {};
887     if (!alreadyLogged) {
888       ExtensionActivityLog.log(id, context.viewType, "api_call", data.path, {
889         args: data.args,
890       });
891     }
893     let start = Cu.now();
894     try {
895       return callable();
896     } finally {
897       ChromeUtils.addProfilerMarker(
898         "ExtensionParent",
899         { startTime: start },
900         `${id}, api_call: ${data.path}`
901       );
902       if (gTimingEnabled) {
903         let end = Cu.now() * 1000;
904         PerformanceCounters.storeExecutionTime(
905           id,
906           data.path,
907           end - start * 1000
908         );
909       }
910     }
911   },
913   async recvAPICall(data, { actor }) {
914     let context = this.getContextById(data.childId);
915     let target = actor.browsingContext.top.embedderElement;
916     if (context.parentMessageManager !== target.messageManager) {
917       throw new Error("Got message on unexpected message manager");
918     }
920     let reply = result => {
921       if (!context.parentMessageManager) {
922         Services.console.logStringMessage(
923           "Cannot send function call result: other side closed connection " +
924             `(call data: ${uneval({ path: data.path, args: data.args })})`
925         );
926         return;
927       }
929       this.conduit.sendCallResult(data.childId, {
930         childId: data.childId,
931         callId: data.callId,
932         path: data.path,
933         ...result,
934       });
935     };
937     try {
938       let args = data.args;
939       let pendingBrowser = context.pendingEventBrowser;
940       let fun = await context.apiCan.asyncFindAPIPath(data.path);
941       let result = this.callAndLog(context, data, () => {
942         return context.withPendingBrowser(pendingBrowser, () => fun(...args));
943       });
945       if (data.callId) {
946         result = result || Promise.resolve();
948         result.then(
949           result => {
950             result = result instanceof SpreadArgs ? [...result] : [result];
952             let holder = new StructuredCloneHolder(result);
954             reply({ result: holder });
955           },
956           error => {
957             error = context.normalizeError(error);
958             reply({
959               error: { message: error.message, fileName: error.fileName },
960             });
961           }
962         );
963       }
964     } catch (e) {
965       if (data.callId) {
966         let error = context.normalizeError(e);
967         reply({ error: { message: error.message } });
968       } else {
969         Cu.reportError(e);
970       }
971     }
972   },
974   async recvAddListener(data, { actor }) {
975     let context = this.getContextById(data.childId);
976     let target = actor.browsingContext.top.embedderElement;
977     if (context.parentMessageManager !== target.messageManager) {
978       throw new Error("Got message on unexpected message manager");
979     }
981     let { childId, alreadyLogged = false } = data;
982     let handlingUserInput = false;
984     let listener = async (...listenerArgs) => {
985       let startTime = Cu.now();
986       // Extract urgentSend flag to avoid deserializing args holder later.
987       let urgentSend = false;
988       if (listenerArgs[0] && data.path.startsWith("webRequest.")) {
989         urgentSend = listenerArgs[0].urgentSend;
990         delete listenerArgs[0].urgentSend;
991       }
992       let result = await this.conduit.queryRunListener(childId, {
993         childId,
994         handlingUserInput,
995         listenerId: data.listenerId,
996         path: data.path,
997         urgentSend,
998         get args() {
999           return new StructuredCloneHolder(listenerArgs);
1000         },
1001       });
1002       let rv = result && result.deserialize(global);
1003       ChromeUtils.addProfilerMarker(
1004         "ExtensionParent",
1005         { startTime },
1006         `${context.extension.id}, api_event: ${data.path}`
1007       );
1008       ExtensionActivityLog.log(
1009         context.extension.id,
1010         context.viewType,
1011         "api_event",
1012         data.path,
1013         { args: listenerArgs, result: rv }
1014       );
1015       return rv;
1016     };
1018     context.listenerProxies.set(data.listenerId, listener);
1020     let args = data.args;
1021     let promise = context.apiCan.asyncFindAPIPath(data.path);
1023     // Store pending listener additions so we can be sure they're all
1024     // fully initialize before we consider extension startup complete.
1025     if (context.viewType === "background" && context.listenerPromises) {
1026       const { listenerPromises } = context;
1027       listenerPromises.add(promise);
1028       let remove = () => {
1029         listenerPromises.delete(promise);
1030       };
1031       promise.then(remove, remove);
1032     }
1034     let handler = await promise;
1035     if (handler.setUserInput) {
1036       handlingUserInput = true;
1037     }
1038     handler.addListener(listener, ...args);
1039     if (!alreadyLogged) {
1040       ExtensionActivityLog.log(
1041         context.extension.id,
1042         context.viewType,
1043         "api_call",
1044         `${data.path}.addListener`,
1045         { args }
1046       );
1047     }
1048   },
1050   async recvRemoveListener(data) {
1051     let context = this.getContextById(data.childId);
1052     let listener = context.listenerProxies.get(data.listenerId);
1054     let handler = await context.apiCan.asyncFindAPIPath(data.path);
1055     handler.removeListener(listener);
1057     let { alreadyLogged = false } = data;
1058     if (!alreadyLogged) {
1059       ExtensionActivityLog.log(
1060         context.extension.id,
1061         context.viewType,
1062         "api_call",
1063         `${data.path}.removeListener`,
1064         { args: [] }
1065       );
1066     }
1067   },
1069   getContextById(childId) {
1070     let context = this.proxyContexts.get(childId);
1071     if (!context) {
1072       throw new Error("WebExtension context not found!");
1073     }
1074     return context;
1075   },
1078 ParentAPIManager.init();
1081  * A hidden window which contains the extension pages that are not visible
1082  * (i.e., background pages and devtools pages), and is also used by
1083  * ExtensionDebuggingUtils to contain the browser elements used by the
1084  * addon debugger to connect to the devtools actors running in the same
1085  * process of the target extension (and be able to stay connected across
1086  *  the addon reloads).
1087  */
1088 class HiddenXULWindow {
1089   constructor() {
1090     this._windowlessBrowser = null;
1091     this.unloaded = false;
1092     this.waitInitialized = this.initWindowlessBrowser();
1093   }
1095   shutdown() {
1096     if (this.unloaded) {
1097       throw new Error(
1098         "Unable to shutdown an unloaded HiddenXULWindow instance"
1099       );
1100     }
1102     this.unloaded = true;
1104     this.waitInitialized = null;
1106     if (!this._windowlessBrowser) {
1107       Cu.reportError("HiddenXULWindow was shut down while it was loading.");
1108       // initWindowlessBrowser will close windowlessBrowser when possible.
1109       return;
1110     }
1112     this._windowlessBrowser.close();
1113     this._windowlessBrowser = null;
1114   }
1116   get chromeDocument() {
1117     return this._windowlessBrowser.document;
1118   }
1120   /**
1121    * Private helper that create a HTMLDocument in a windowless browser.
1122    *
1123    * @returns {Promise<void>}
1124    *          A promise which resolves when the windowless browser is ready.
1125    */
1126   async initWindowlessBrowser() {
1127     if (this.waitInitialized) {
1128       throw new Error("HiddenXULWindow already initialized");
1129     }
1131     // The invisible page is currently wrapped in a XUL window to fix an issue
1132     // with using the canvas API from a background page (See Bug 1274775).
1133     let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
1135     // The windowless browser is a thin wrapper around a docShell that keeps
1136     // its related resources alive. It implements nsIWebNavigation and
1137     // forwards its methods to the underlying docShell. That .docShell
1138     // needs `QueryInterface(nsIWebNavigation)` to give us access to the
1139     // webNav methods that are already available on the windowless browser.
1140     let chromeShell = windowlessBrowser.docShell;
1141     chromeShell.QueryInterface(Ci.nsIWebNavigation);
1143     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
1144       let attrs = chromeShell.getOriginAttributes();
1145       attrs.privateBrowsingId = 1;
1146       chromeShell.setOriginAttributes(attrs);
1147     }
1149     windowlessBrowser.browsingContext.useGlobalHistory = false;
1150     chromeShell.loadURI("chrome://extensions/content/dummy.xhtml", {
1151       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
1152     });
1154     await promiseObserved(
1155       "chrome-document-global-created",
1156       win => win.document == chromeShell.document
1157     );
1158     await promiseDocumentLoaded(windowlessBrowser.document);
1159     if (this.unloaded) {
1160       windowlessBrowser.close();
1161       return;
1162     }
1163     this._windowlessBrowser = windowlessBrowser;
1164   }
1166   /**
1167    * Creates the browser XUL element that will contain the WebExtension Page.
1168    *
1169    * @param {Object} xulAttributes
1170    *        An object that contains the xul attributes to set of the newly
1171    *        created browser XUL element.
1172    *
1173    * @returns {Promise<XULElement>}
1174    *          A Promise which resolves to the newly created browser XUL element.
1175    */
1176   async createBrowserElement(xulAttributes) {
1177     if (!xulAttributes || Object.keys(xulAttributes).length === 0) {
1178       throw new Error("missing mandatory xulAttributes parameter");
1179     }
1181     await this.waitInitialized;
1183     const chromeDoc = this.chromeDocument;
1185     const browser = chromeDoc.createXULElement("browser");
1186     browser.setAttribute("type", "content");
1187     browser.setAttribute("disableglobalhistory", "true");
1188     browser.setAttribute("messagemanagergroup", "webext-browsers");
1190     for (const [name, value] of Object.entries(xulAttributes)) {
1191       if (value != null) {
1192         browser.setAttribute(name, value);
1193       }
1194     }
1196     let awaitFrameLoader = Promise.resolve();
1198     if (browser.getAttribute("remote") === "true") {
1199       awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
1200     }
1202     chromeDoc.documentElement.appendChild(browser);
1204     // Forcibly flush layout so that we get a pres shell soon enough, see
1205     // bug 1274775.
1206     browser.getBoundingClientRect();
1208     await awaitFrameLoader;
1209     return browser;
1210   }
1213 const SharedWindow = {
1214   _window: null,
1215   _count: 0,
1217   acquire() {
1218     if (this._window == null) {
1219       if (this._count != 0) {
1220         throw new Error(
1221           `Shared window already exists with count ${this._count}`
1222         );
1223       }
1225       this._window = new HiddenXULWindow();
1226     }
1228     this._count++;
1229     return this._window;
1230   },
1232   release() {
1233     if (this._count < 1) {
1234       throw new Error(`Releasing shared window with count ${this._count}`);
1235     }
1237     this._count--;
1238     if (this._count == 0) {
1239       this._window.shutdown();
1240       this._window = null;
1241     }
1242   },
1246  * This is a base class used by the ext-backgroundPage and ext-devtools API implementations
1247  * to inherits the shared boilerplate code needed to create a parent document for the hidden
1248  * extension pages (e.g. the background page, the devtools page) in the BackgroundPage and
1249  * DevToolsPage classes.
1251  * @param {Extension} extension
1252  *        The Extension which owns the hidden extension page created (used to decide
1253  *        if the hidden extension page parent doc is going to be a windowlessBrowser or
1254  *        a visible XUL window).
1255  * @param {string} viewType
1256  *        The viewType of the WebExtension page that is going to be loaded
1257  *        in the created browser element (e.g. "background" or "devtools_page").
1258  */
1259 class HiddenExtensionPage {
1260   constructor(extension, viewType) {
1261     if (!extension || !viewType) {
1262       throw new Error("extension and viewType parameters are mandatory");
1263     }
1265     this.extension = extension;
1266     this.viewType = viewType;
1267     this.browser = null;
1268     this.unloaded = false;
1269   }
1271   /**
1272    * Destroy the created parent document.
1273    */
1274   shutdown() {
1275     if (this.unloaded) {
1276       throw new Error(
1277         "Unable to shutdown an unloaded HiddenExtensionPage instance"
1278       );
1279     }
1281     this.unloaded = true;
1283     if (this.browser) {
1284       this._releaseBrowser();
1285     }
1286   }
1288   _releaseBrowser() {
1289     this.browser.remove();
1290     this.browser = null;
1291     SharedWindow.release();
1292   }
1294   /**
1295    * Creates the browser XUL element that will contain the WebExtension Page.
1296    *
1297    * @returns {Promise<XULElement>}
1298    *          A Promise which resolves to the newly created browser XUL element.
1299    */
1300   async createBrowserElement() {
1301     if (this.browser) {
1302       throw new Error("createBrowserElement called twice");
1303     }
1305     let window = SharedWindow.acquire();
1306     try {
1307       this.browser = await window.createBrowserElement({
1308         "webextension-view-type": this.viewType,
1309         remote: this.extension.remote ? "true" : null,
1310         remoteType: this.extension.remoteType,
1311         initialBrowsingContextGroupId: this.extension.browsingContextGroupId,
1312       });
1313     } catch (e) {
1314       SharedWindow.release();
1315       throw e;
1316     }
1318     if (this.unloaded) {
1319       this._releaseBrowser();
1320       throw new Error("Extension shut down before browser element was created");
1321     }
1323     return this.browser;
1324   }
1328  * This object provides utility functions needed by the devtools actors to
1329  * be able to connect and debug an extension (which can run in the main or in
1330  * a child extension process).
1331  */
1332 const DebugUtils = {
1333   // A lazily created hidden XUL window, which contains the browser elements
1334   // which are used to connect the webextension patent actor to the extension process.
1335   hiddenXULWindow: null,
1337   // Map<extensionId, Promise<XULElement>>
1338   debugBrowserPromises: new Map(),
1339   // DefaultWeakMap<Promise<browser XULElement>, Set<WebExtensionParentActor>>
1340   debugActors: new DefaultWeakMap(() => new Set()),
1342   _extensionUpdatedWatcher: null,
1343   watchExtensionUpdated() {
1344     if (!this._extensionUpdatedWatcher) {
1345       // Watch the updated extension objects.
1346       this._extensionUpdatedWatcher = async (evt, extension) => {
1347         const browserPromise = this.debugBrowserPromises.get(extension.id);
1348         if (browserPromise) {
1349           const browser = await browserPromise;
1350           if (
1351             browser.isRemoteBrowser !== extension.remote &&
1352             this.debugBrowserPromises.get(extension.id) === browserPromise
1353           ) {
1354             // If the cached browser element is not anymore of the same
1355             // remote type of the extension, remove it.
1356             this.debugBrowserPromises.delete(extension.id);
1357             browser.remove();
1358           }
1359         }
1360       };
1362       apiManager.on("ready", this._extensionUpdatedWatcher);
1363     }
1364   },
1366   unwatchExtensionUpdated() {
1367     if (this._extensionUpdatedWatcher) {
1368       apiManager.off("ready", this._extensionUpdatedWatcher);
1369       delete this._extensionUpdatedWatcher;
1370     }
1371   },
1373   getExtensionManifestWarnings(id) {
1374     const addon = GlobalManager.extensionMap.get(id);
1375     if (addon) {
1376       return addon.warnings;
1377     }
1378     return [];
1379   },
1381   /**
1382    * Retrieve a XUL browser element which has been configured to be able to connect
1383    * the devtools actor with the process where the extension is running.
1384    *
1385    * @param {WebExtensionParentActor} webExtensionParentActor
1386    *        The devtools actor that is retrieving the browser element.
1387    *
1388    * @returns {Promise<XULElement>}
1389    *          A promise which resolves to the configured browser XUL element.
1390    */
1391   async getExtensionProcessBrowser(webExtensionParentActor) {
1392     const extensionId = webExtensionParentActor.addonId;
1393     const extension = GlobalManager.getExtension(extensionId);
1394     if (!extension) {
1395       throw new Error(`Extension not found: ${extensionId}`);
1396     }
1398     const createBrowser = () => {
1399       if (!this.hiddenXULWindow) {
1400         this.hiddenXULWindow = new HiddenXULWindow();
1401         this.watchExtensionUpdated();
1402       }
1404       return this.hiddenXULWindow.createBrowserElement({
1405         "webextension-addon-debug-target": extensionId,
1406         remote: extension.remote ? "true" : null,
1407         remoteType: extension.remoteType,
1408         initialBrowsingContextGroupId: extension.browsingContextGroupId,
1409       });
1410     };
1412     let browserPromise = this.debugBrowserPromises.get(extensionId);
1414     // Create a new promise if there is no cached one in the map.
1415     if (!browserPromise) {
1416       browserPromise = createBrowser();
1417       this.debugBrowserPromises.set(extensionId, browserPromise);
1418       browserPromise.then(browser => {
1419         browserPromise.browser = browser;
1420       });
1421       browserPromise.catch(e => {
1422         Cu.reportError(e);
1423         this.debugBrowserPromises.delete(extensionId);
1424       });
1425     }
1427     this.debugActors.get(browserPromise).add(webExtensionParentActor);
1429     return browserPromise;
1430   },
1432   getFrameLoader(extensionId) {
1433     let promise = this.debugBrowserPromises.get(extensionId);
1434     return promise && promise.browser && promise.browser.frameLoader;
1435   },
1437   /**
1438    * Given the devtools actor that has retrieved an addon debug browser element,
1439    * it destroys the XUL browser element, and it also destroy the hidden XUL window
1440    * if it is not currently needed.
1441    *
1442    * @param {WebExtensionParentActor} webExtensionParentActor
1443    *        The devtools actor that has retrieved an addon debug browser element.
1444    */
1445   async releaseExtensionProcessBrowser(webExtensionParentActor) {
1446     const extensionId = webExtensionParentActor.addonId;
1447     const browserPromise = this.debugBrowserPromises.get(extensionId);
1449     if (browserPromise) {
1450       const actorsSet = this.debugActors.get(browserPromise);
1451       actorsSet.delete(webExtensionParentActor);
1452       if (actorsSet.size === 0) {
1453         this.debugActors.delete(browserPromise);
1454         this.debugBrowserPromises.delete(extensionId);
1455         await browserPromise.then(browser => browser.remove());
1456       }
1457     }
1459     if (this.debugBrowserPromises.size === 0 && this.hiddenXULWindow) {
1460       this.hiddenXULWindow.shutdown();
1461       this.hiddenXULWindow = null;
1462       this.unwatchExtensionUpdated();
1463     }
1464   },
1468  * Returns a Promise which resolves with the message data when the given message
1469  * was received by the message manager. The promise is rejected if the message
1470  * manager was closed before a message was received.
1472  * @param {MessageListenerManager} messageManager
1473  *        The message manager on which to listen for messages.
1474  * @param {string} messageName
1475  *        The message to listen for.
1476  * @returns {Promise<*>}
1477  */
1478 function promiseMessageFromChild(messageManager, messageName) {
1479   return new Promise((resolve, reject) => {
1480     let unregister;
1481     function listener(message) {
1482       unregister();
1483       resolve(message.data);
1484     }
1485     function observer(subject, topic, data) {
1486       if (subject === messageManager) {
1487         unregister();
1488         reject(
1489           new Error(
1490             `Message manager was disconnected before receiving ${messageName}`
1491           )
1492         );
1493       }
1494     }
1495     unregister = () => {
1496       Services.obs.removeObserver(observer, "message-manager-close");
1497       messageManager.removeMessageListener(messageName, listener);
1498     };
1499     messageManager.addMessageListener(messageName, listener);
1500     Services.obs.addObserver(observer, "message-manager-close");
1501   });
1504 // This should be called before browser.loadURI is invoked.
1505 async function promiseExtensionViewLoaded(browser) {
1506   let { childId } = await promiseMessageFromChild(
1507     browser.messageManager,
1508     "Extension:ExtensionViewLoaded"
1509   );
1510   if (childId) {
1511     return ParentAPIManager.getContextById(childId);
1512   }
1516  * This helper is used to subscribe a listener (e.g. in the ext-devtools API implementation)
1517  * to be called for every ExtensionProxyContext created for an extension page given
1518  * its related extension, viewType and browser element (both the top level context and any context
1519  * created for the extension urls running into its iframe descendants).
1521  * @param {object} params.extension
1522  *        The Extension on which we are going to listen for the newly created ExtensionProxyContext.
1523  * @param {string} params.viewType
1524  *        The viewType of the WebExtension page that we are watching (e.g. "background" or
1525  *        "devtools_page").
1526  * @param {XULElement} params.browser
1527  *        The browser element of the WebExtension page that we are watching.
1528  * @param {function} onExtensionProxyContextLoaded
1529  *        The callback that is called when a new context has been loaded (as `callback(context)`);
1531  * @returns {function}
1532  *          Unsubscribe the listener.
1533  */
1534 function watchExtensionProxyContextLoad(
1535   { extension, viewType, browser },
1536   onExtensionProxyContextLoaded
1537 ) {
1538   if (typeof onExtensionProxyContextLoaded !== "function") {
1539     throw new Error("Missing onExtensionProxyContextLoaded handler");
1540   }
1542   const listener = (event, context) => {
1543     if (context.viewType == viewType && context.xulBrowser == browser) {
1544       onExtensionProxyContextLoaded(context);
1545     }
1546   };
1548   extension.on("extension-proxy-context-load", listener);
1550   return () => {
1551     extension.off("extension-proxy-context-load", listener);
1552   };
1555 // Manages icon details for toolbar buttons in the |pageAction| and
1556 // |browserAction| APIs.
1557 let IconDetails = {
1558   DEFAULT_ICON: "chrome://mozapps/skin/extensions/extensionGeneric.svg",
1560   // WeakMap<Extension -> Map<url-string -> Map<iconType-string -> object>>>
1561   iconCache: new DefaultWeakMap(() => {
1562     return new DefaultMap(() => new DefaultMap(() => new Map()));
1563   }),
1565   // Normalizes the various acceptable input formats into an object
1566   // with icon size as key and icon URL as value.
1567   //
1568   // If a context is specified (function is called from an extension):
1569   // Throws an error if an invalid icon size was provided or the
1570   // extension is not allowed to load the specified resources.
1571   //
1572   // If no context is specified, instead of throwing an error, this
1573   // function simply logs a warning message.
1574   normalize(details, extension, context = null) {
1575     if (!details.imageData && details.path != null) {
1576       // Pick a cache key for the icon paths. If the path is a string,
1577       // use it directly. Otherwise, stringify the path object.
1578       let key = details.path;
1579       if (typeof key !== "string") {
1580         key = uneval(key);
1581       }
1583       let icons = this.iconCache
1584         .get(extension)
1585         .get(context && context.uri.spec)
1586         .get(details.iconType);
1588       let icon = icons.get(key);
1589       if (!icon) {
1590         icon = this._normalize(details, extension, context);
1591         icons.set(key, icon);
1592       }
1593       return icon;
1594     }
1596     return this._normalize(details, extension, context);
1597   },
1599   _normalize(details, extension, context = null) {
1600     let result = {};
1602     try {
1603       let { imageData, path, themeIcons } = details;
1605       if (imageData) {
1606         if (typeof imageData == "string") {
1607           imageData = { "19": imageData };
1608         }
1610         for (let size of Object.keys(imageData)) {
1611           result[size] = imageData[size];
1612         }
1613       }
1615       let baseURI = context ? context.uri : extension.baseURI;
1617       if (path != null) {
1618         if (typeof path != "object") {
1619           path = { "19": path };
1620         }
1622         for (let size of Object.keys(path)) {
1623           let url = path[size];
1624           if (url) {
1625             url = baseURI.resolve(path[size]);
1627             // The Chrome documentation specifies these parameters as
1628             // relative paths. We currently accept absolute URLs as well,
1629             // which means we need to check that the extension is allowed
1630             // to load them. This will throw an error if it's not allowed.
1631             this._checkURL(url, extension);
1632           }
1633           result[size] = url || this.DEFAULT_ICON;
1634         }
1635       }
1637       if (themeIcons) {
1638         themeIcons.forEach(({ size, light, dark }) => {
1639           let lightURL = baseURI.resolve(light);
1640           let darkURL = baseURI.resolve(dark);
1642           this._checkURL(lightURL, extension);
1643           this._checkURL(darkURL, extension);
1645           let defaultURL = result[size] || result[19]; // always fallback to default first
1646           result[size] = {
1647             default: defaultURL || darkURL, // Fallback to the dark url if no default is specified.
1648             light: lightURL,
1649             dark: darkURL,
1650           };
1651         });
1652       }
1653     } catch (e) {
1654       // Function is called from extension code, delegate error.
1655       if (context) {
1656         throw e;
1657       }
1658       // If there's no context, it's because we're handling this
1659       // as a manifest directive. Log a warning rather than
1660       // raising an error.
1661       extension.manifestError(`Invalid icon data: ${e}`);
1662     }
1664     return result;
1665   },
1667   // Checks if the extension is allowed to load the given URL with the specified principal.
1668   // This will throw an error if the URL is not allowed.
1669   _checkURL(url, extension) {
1670     if (!extension.checkLoadURL(url, { allowInheritsPrincipal: true })) {
1671       throw new ExtensionError(`Illegal URL ${url}`);
1672     }
1673   },
1675   // Returns the appropriate icon URL for the given icons object and the
1676   // screen resolution of the given window.
1677   getPreferredIcon(icons, extension = null, size = 16) {
1678     const DEFAULT = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
1680     let bestSize = null;
1681     if (icons[size]) {
1682       bestSize = size;
1683     } else if (icons[2 * size]) {
1684       bestSize = 2 * size;
1685     } else {
1686       let sizes = Object.keys(icons)
1687         .map(key => parseInt(key, 10))
1688         .sort((a, b) => a - b);
1690       bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
1691     }
1693     if (bestSize) {
1694       return { size: bestSize, icon: icons[bestSize] || DEFAULT };
1695     }
1697     return { size, icon: DEFAULT };
1698   },
1700   // These URLs should already be properly escaped, but make doubly sure CSS
1701   // string escape characters are escaped here, since they could lead to a
1702   // sandbox break.
1703   escapeUrl(url) {
1704     return url.replace(/[\\\s"]/g, encodeURIComponent);
1705   },
1708 // A cache to support faster initialization of extensions at browser startup.
1709 // All cached data is removed when the browser is updated.
1710 // Extension-specific data is removed when the add-on is updated.
1711 StartupCache = {
1712   STORE_NAMES: Object.freeze([
1713     "general",
1714     "locales",
1715     "manifests",
1716     "other",
1717     "permissions",
1718     "schemas",
1719   ]),
1721   _ensureDirectoryPromise: null,
1722   _saveTask: null,
1724   _ensureDirectory() {
1725     if (this._ensureDirectoryPromise === null) {
1726       this._ensureDirectoryPromise = IOUtils.makeDirectory(
1727         PathUtils.parent(this.file),
1728         {
1729           ignoreExisting: true,
1730           createAncestors: true,
1731         }
1732       );
1733     }
1735     return this._ensureDirectoryPromise;
1736   },
1738   // When the application version changes, this file is removed by
1739   // RemoveComponentRegistries in nsAppRunner.cpp.
1740   file: PathUtils.join(
1741     Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
1742     "startupCache",
1743     "webext.sc.lz4"
1744   ),
1746   async _saveNow() {
1747     let data = new Uint8Array(aomStartup.encodeBlob(this._data));
1748     await this._ensureDirectoryPromise;
1749     await IOUtils.write(this.file, data, { tmpPath: `${this.file}.tmp` });
1750   },
1752   save() {
1753     this._ensureDirectory();
1755     if (!this._saveTask) {
1756       this._saveTask = new DeferredTask(() => this._saveNow(), 5000);
1758       IOUtils.profileBeforeChange.addBlocker(
1759         "Flush WebExtension StartupCache",
1760         async () => {
1761           await this._saveTask.finalize();
1762           this._saveTask = null;
1763         }
1764       );
1765     }
1767     return this._saveTask.arm();
1768   },
1770   _data: null,
1771   async _readData() {
1772     let result = new Map();
1773     try {
1774       let { buffer } = await IOUtils.read(this.file);
1776       result = aomStartup.decodeBlob(buffer);
1777     } catch (e) {
1778       if (!(e instanceof DOMException) || e.name !== "NotFoundError") {
1779         Cu.reportError(e);
1780       }
1781     }
1783     this._data = result;
1784     return result;
1785   },
1787   get dataPromise() {
1788     if (!this._dataPromise) {
1789       this._dataPromise = this._readData();
1790     }
1791     return this._dataPromise;
1792   },
1794   clearAddonData(id) {
1795     return Promise.all([
1796       this.general.delete(id),
1797       this.locales.delete(id),
1798       this.manifests.delete(id),
1799       this.permissions.delete(id),
1800     ]).catch(e => {
1801       // Ignore the error. It happens when we try to flush the add-on
1802       // data after the AddonManager has flushed the entire startup cache.
1803     });
1804   },
1806   observe(subject, topic, data) {
1807     if (topic === "startupcache-invalidate") {
1808       this._data = new Map();
1809       this._dataPromise = Promise.resolve(this._data);
1810     }
1811   },
1813   get(extension, path, createFunc) {
1814     return this.general.get(
1815       [extension.id, extension.version, ...path],
1816       createFunc
1817     );
1818   },
1820   delete(extension, path) {
1821     return this.general.delete([extension.id, extension.version, ...path]);
1822   },
1825 Services.obs.addObserver(StartupCache, "startupcache-invalidate");
1827 class CacheStore {
1828   constructor(storeName) {
1829     this.storeName = storeName;
1830   }
1832   async getStore(path = null) {
1833     let data = await StartupCache.dataPromise;
1835     let store = data.get(this.storeName);
1836     if (!store) {
1837       store = new Map();
1838       data.set(this.storeName, store);
1839     }
1841     let key = path;
1842     if (Array.isArray(path)) {
1843       for (let elem of path.slice(0, -1)) {
1844         let next = store.get(elem);
1845         if (!next) {
1846           next = new Map();
1847           store.set(elem, next);
1848         }
1849         store = next;
1850       }
1851       key = path[path.length - 1];
1852     }
1854     return [store, key];
1855   }
1857   async get(path, createFunc) {
1858     let [store, key] = await this.getStore(path);
1860     let result = store.get(key);
1862     if (result === undefined) {
1863       result = await createFunc(path);
1864       store.set(key, result);
1865       StartupCache.save();
1866     }
1868     return result;
1869   }
1871   async set(path, value) {
1872     let [store, key] = await this.getStore(path);
1874     store.set(key, value);
1875     StartupCache.save();
1876   }
1878   async getAll() {
1879     let [store] = await this.getStore();
1881     return new Map(store);
1882   }
1884   async delete(path) {
1885     let [store, key] = await this.getStore(path);
1887     if (store.delete(key)) {
1888       StartupCache.save();
1889     }
1890   }
1893 for (let name of StartupCache.STORE_NAMES) {
1894   StartupCache[name] = new CacheStore(name);
1897 var ExtensionParent = {
1898   GlobalManager,
1899   HiddenExtensionPage,
1900   IconDetails,
1901   ParentAPIManager,
1902   StartupCache,
1903   WebExtensionPolicy,
1904   apiManager,
1905   promiseExtensionViewLoaded,
1906   watchExtensionProxyContextLoad,
1907   DebugUtils,
1910 // browserPaintedPromise and browserStartupPromise are promises that
1911 // resolve after the first browser window is painted and after browser
1912 // windows have been restored, respectively. Alternatively,
1913 // browserStartupPromise also resolves from the extensions-late-startup
1914 // notification sent by Firefox Reality on desktop platforms, because it
1915 // doesn't support SessionStore.
1916 // _resetStartupPromises should only be called from outside this file in tests.
1917 ExtensionParent._resetStartupPromises = () => {
1918   ExtensionParent.browserPaintedPromise = promiseObserved(
1919     "browser-delayed-startup-finished"
1920   ).then(() => {});
1921   ExtensionParent.browserStartupPromise = Promise.race([
1922     promiseObserved("sessionstore-windows-restored"),
1923     promiseObserved("extensions-late-startup"),
1924   ]).then(() => {});
1926 ExtensionParent._resetStartupPromises();
1928 XPCOMUtils.defineLazyGetter(ExtensionParent, "PlatformInfo", () => {
1929   return Object.freeze({
1930     os: (function() {
1931       let os = AppConstants.platform;
1932       if (os == "macosx") {
1933         os = "mac";
1934       }
1935       return os;
1936     })(),
1937     arch: (function() {
1938       let abi = Services.appinfo.XPCOMABI;
1939       let [arch] = abi.split("-");
1940       if (arch == "x86") {
1941         arch = "x86-32";
1942       } else if (arch == "x86_64") {
1943         arch = "x86-64";
1944       }
1945       return arch;
1946     })(),
1947   });
1951  * Retreives the browser_style stylesheets needed for extension popups and sidebars.
1952  * @returns {Array<string>} an array of stylesheets needed for the current platform.
1953  */
1954 XPCOMUtils.defineLazyGetter(ExtensionParent, "extensionStylesheets", () => {
1955   let stylesheets = ["chrome://browser/content/extension.css"];
1957   if (AppConstants.platform === "macosx") {
1958     stylesheets.push("chrome://browser/content/extension-mac.css");
1959   }
1961   return stylesheets;