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/. */
8 * This module contains code for managing APIs that need to run in the
9 * parent process, and handles the parent side of operations that need
10 * to be proxied from ExtensionChild.sys.mjs.
13 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
15 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
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",
35 XPCOMUtils.defineLazyServiceGetters(lazy, {
37 "@mozilla.org/addons/addon-manager-startup;1",
38 "amIAddonManagerStartup",
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 } =
56 promiseDocumentLoaded,
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");
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");
82 } else if (JSProcessActorParent.isInstance(actor)) {
83 if (actor.manager.remoteType !== context.extension.remoteType) {
84 throw new Error("Got message from unexpected process");
89 // This object loads the ext-*.js scripts that define the extension API.
90 let apiManager = new (class extends SchemaAPIManager {
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);
100 this.on("update", async (e, { id, resourceURI, isPrivileged }) => {
101 let modules = this.eventModules.get("update");
102 if (modules.size == 0) {
106 let extension = new lazy.ExtensionData(resourceURI, isPrivileged);
107 await extension.loadManifest();
110 Array.from(modules).map(async apiName => {
111 let module = await this.asyncLoadModule(apiName);
112 module.onUpdate(id, extension.manifest);
117 this.on("uninstall", (e, { id }) => {
118 let modules = this.eventModules.get("uninstall");
120 Array.from(modules).map(async apiName => {
121 let module = await this.asyncLoadModule(apiName);
122 return module.onUninstall(id);
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
132 if (disabledIds.length) {
133 this._callHandlers(disabledIds, "disable", "onDisable");
136 let uninstalledIds = lazy.AddonManager.getStartupChanges(
137 lazy.AddonManager.STARTUP_CHANGE_UNINSTALLED
139 if (uninstalledIds.length) {
140 this._callHandlers(uninstalledIds, "uninstall", "onUninstall");
144 getModuleJSONURLs() {
146 Services.catMan.enumerateCategory(CATEGORY_EXTENSION_MODULES),
151 // Loads all the ext-*.js scripts currently registered.
153 if (this.initialized) {
154 return this.initialized;
157 let modulesPromise = StartupCache.other.get(["parentModules"], () =>
158 this.loadModuleJSON(this.getModuleJSONURLs())
162 for (let { value } of Services.catMan.enumerateCategory(
163 CATEGORY_EXTENSION_SCRIPTS
165 scriptURLs.push(value);
168 let promise = (async () => {
169 let scripts = await Promise.all(
170 scriptURLs.map(url => ChromeUtils.compileScript(url))
173 this.initModuleData(await modulesPromise);
176 for (let script of scripts) {
177 script.executeInGlobal(this.global);
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(() => {
184 for (let { value } of Services.catMan.enumerateCategory(
185 CATEGORY_EXTENSION_SCHEMAS
187 promises.push(lazy.Schemas.load(value));
189 for (let [url, { content }] of this.schemaURLs) {
190 promises.push(lazy.Schemas.load(url, content));
192 for (let url of schemaURLs) {
193 promises.push(lazy.Schemas.load(url));
195 return Promise.all(promises).then(() => {
196 lazy.Schemas.updateSharedSchemas();
201 Services.mm.addMessageListener("Extension:GetFrameData", this);
203 this.initialized = promise;
204 return this.initialized;
207 receiveMessage({ target }) {
208 let data = GlobalManager.frameData.get(target) || {};
209 Object.assign(data, this.global.tabTracker.getBrowserData(target));
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));
222 if (event === "disable") {
223 promises.push(...ids.map(id => this.emit("disable", id)));
225 if (event === "enabling") {
226 promises.push(...ids.map(id => this.emit("enabling", id)));
229 lazy.AsyncShutdown.profileBeforeChange.addBlocker(
230 `Extension API ${event} handlers for ${ids.join(",")}`,
231 Promise.all(promises)
237 * @typedef {object} ParentPort
238 * @property {boolean} [native]
239 * @property {string} [senderChildId]
240 * @property {function(StructuredCloneHolder): any} onPortMessage
241 * @property {Function} onPortDisconnect
244 // Receives messages related to the extension messaging API and forwards them
245 // to relevant child messengers. Also handles Native messaging and GeckoView.
246 /** @typedef {typeof ProxyMessenger} NativeMessenger */
247 const ProxyMessenger = {
248 /** @type {Map<number, Partial<ParentPort>&Promise<ParentPort>>} */
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"],
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,
267 context.extension.hasPermission("nativeMessagingFromContent")
269 } else if (sender.verified) {
270 return new lazy.NativeApp(context, nativeApp);
272 sender = this.getSender(context.extension, sender);
273 throw new Error(`Native messaging not allowed: ${JSON.stringify(sender)}`);
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
282 const promiseSendMessage = app.sendMessage(holder);
283 const sendMessagePort = {
285 senderChildId: sender.childId,
287 this.trackNativeAppPort(sendMessagePort);
288 const untrackSendMessage = () => this.untrackNativeAppPort(sendMessagePort);
289 promiseSendMessage.then(untrackSendMessage, untrackSendMessage);
291 return promiseSendMessage;
294 getSender(extension, source) {
296 contextId: source.id,
297 id: source.extensionId,
298 envType: source.envType,
302 if (JSWindowActorParent.isInstance(source.actor)) {
303 let browser = source.actor.browsingContext.top.embedderElement;
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;
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);
323 let browser = tab.linkedBrowser || tab.browser;
324 return browser.browsingContext.id;
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);
332 return Promise.reject({ message: ERROR_NO_RECEIVERS });
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";
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);
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 });
355 async recvPortConnect(arg, { sender }) {
357 let port = this.openNative(arg.name, sender).onConnect(arg.portId, this);
358 port.senderChildId = sender.childId;
360 this.ports.set(arg.portId, port);
361 this.trackNativeAppPort(port);
365 // PortMessages that follow will need to wait for the port to be opened.
366 /** @type {callback} */
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);
374 // If there are no active onConnect listeners.
375 if (!all.some(x => x.value)) {
376 throw new ExtensionError(ERROR_NO_RECEIVERS);
380 async recvPortMessage({ holder }, { sender }) {
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);
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);
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();
400 this.sendPortDisconnect(sender.portId, null, !sender.source);
403 sendPortMessage(portId, holder, source = true) {
404 this.conduit.castPortMessage("port", { portId, source, holder });
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);
414 trackNativeAppPort(port) {
420 let context = ParentAPIManager.getContextById(port.senderChildId);
421 context?.trackNativeAppPort(port);
423 // getContextById will throw if the context has been destroyed
428 untrackNativeAppPort(port) {
434 let context = ParentAPIManager.getContextById(port.senderChildId);
435 context?.untrackNativeAppPort(port);
437 // getContextById will throw if the context has been destroyed
442 ProxyMessenger.init();
444 // Responsible for loading extension APIs into the right globals.
446 // Map[extension ID -> Extension]. Determines which extension is
447 // responsible for content under a particular extension ID.
448 extensionMap: new Map(),
451 /** @type {WeakMap<Browser, object>} Extension Context init data. */
452 frameData: new WeakMap(),
455 if (this.extensionMap.size == 0) {
456 apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
457 this.initialized = true;
459 this.extensionMap.set(extension.id, 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;
471 _onExtensionBrowser(type, browser, data = {}) {
472 data.viewType = browser.getAttribute("webextension-view-type");
474 GlobalManager.frameData.set(browser, data);
478 getExtension(extensionId) {
479 return this.extensionMap.get(extensionId);
484 * The proxied parent side of a context in ExtensionChild.sys.mjs, for the
485 * parent side of a proxied API.
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", {
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);
529 get isProxyContextParent() {
533 trackRunListenerPromise(runListenerPromise) {
535 // The extension was already shutdown.
537 // Not a non persistent background script context.
538 !this.isBackgroundContext ||
539 this.extension.persistentBackground
543 const clearFromSet = () =>
544 this.runListenerPromises.delete(runListenerPromise);
545 runListenerPromise.then(clearFromSet, clearFromSet);
546 this.runListenerPromises.add(runListenerPromise);
549 clearPendingRunListenerPromises() {
550 this.runListenerPromises.clear();
553 get pendingRunListenerPromisesCount() {
554 return this.runListenerPromises.size;
557 trackNativeAppPort(port) {
559 // Not a native port.
561 // Not a non persistent background script context.
562 !this.isBackgroundContext ||
563 this.extension?.persistentBackground ||
564 // The extension was already shutdown.
569 this.activeNativePorts.add(port);
572 untrackNativeAppPort(port) {
573 this.activeNativePorts.delete(port);
576 get hasActiveNativeAppPorts() {
577 return !!ChromeUtils.nondeterministicGetWeakSetKeys(this.activeNativePorts)
582 * Call the `callable` parameter with `context.callContextData` set to the value passed
583 * as the first parameter of this method.
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
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).
594 * @param {object} callContextData
595 * @param {boolean} callContextData.isHandlingUserInput
596 * @param {Function} callable
598 * @returns {any} Returns the value returned by calling the `callable` method.
600 withCallContextData({ isHandlingUserInput }, callable) {
601 if (this.callContextData) {
603 `Unexpected pre-existing callContextData on "${this.extension?.policy.debugName}" contextId ${this.contextId}`
608 this.callContextData = {
613 this.callContextData = null;
617 async withPendingBrowser(browser, callable) {
618 let savedBrowser = this.pendingEventBrowser;
619 this.pendingEventBrowser = browser;
621 let result = await callable();
624 this.pendingEventBrowser = savedBrowser;
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.
637 applySafe(callback, args) {
638 // There's no need to clone when calling listeners for a proxied
640 return this.applySafeWithoutClone(callback, args);
644 return this.messageManagerProxy?.eventTarget;
647 get parentMessageManager() {
648 // TODO bug 1595186: Replace use of parentMessageManager.
649 return this.messageManagerProxy?.messageManager;
661 this.messageManagerProxy?.dispose();
664 apiManager.emit("proxy-context-unload", this);
668 const apiCan = new CanOfAPIs(this, this.extension.apiManager, {});
669 return redefineGetter(this, "apiCan", apiCan);
673 return redefineGetter(this, "apiObj", this.apiCan.root);
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"],
682 return redefineGetter(this, "sandbox", sandbox);
687 * The parent side of proxied API context for extension content script
688 * running in ExtensionContent.sys.mjs.
690 class ContentScriptContextParent extends ProxyContextParent {}
693 * The parent side of proxied API context for extension page, such as a
694 * background script, a tab page, or a popup, running in
695 * ExtensionChild.sys.mjs.
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);
709 // The window that contains this context. This may change due to moving tabs.
711 let win = this.xulBrowser.ownerGlobal;
712 return win.browsingContext.topChromeWindow;
715 get currentWindow() {
716 if (this.viewType !== "background") {
717 return this.appWindow;
723 let { tabTracker } = apiManager.global;
724 let data = tabTracker.getBrowserData(this.xulBrowser);
725 if (data.tabId >= 0) {
733 this.extension.views.delete(this);
737 apiManager.emit("page-shutdown", this);
743 * The parent side of proxied API context for devtools extension page, such as a
744 * devtools pages and panels running in ExtensionChild.sys.mjs.
746 class DevToolsExtensionPageContextParent extends ExtensionPageContextParent {
747 constructor(...params) {
750 // Set all attributes that are lazily defined to `null` here.
752 // Note that we can't do that for `this._devToolsToolbox` because it will
753 // be defined when calling our parent constructor and so would override it back to `null`.
754 this._devToolsCommands = null;
755 this._onNavigatedListeners = null;
757 this._onResourceAvailable = this._onResourceAvailable.bind(this);
760 set devToolsToolbox(toolbox) {
761 if (this._devToolsToolbox) {
762 throw new Error("Cannot set the context DevTools toolbox twice");
765 this._devToolsToolbox = toolbox;
768 get devToolsToolbox() {
769 return this._devToolsToolbox;
772 async addOnNavigatedListener(listener) {
773 if (!this._onNavigatedListeners) {
774 this._onNavigatedListeners = new Set();
776 await this.devToolsToolbox.resourceCommand.watchResources(
777 [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
779 onAvailable: this._onResourceAvailable,
780 ignoreExistingResources: true,
785 this._onNavigatedListeners.add(listener);
788 removeOnNavigatedListener(listener) {
789 if (this._onNavigatedListeners) {
790 this._onNavigatedListeners.delete(listener);
795 * The returned "commands" object, exposing modules implemented from devtools/shared/commands.
796 * Each attribute being a static interface to communicate with the server backend.
798 * @returns {Promise<object>}
800 async getDevToolsCommands() {
801 // Ensure that we try to instantiate a commands only once,
802 // even if createCommandsForTabForWebExtension is async.
803 if (this._devToolsCommandsPromise) {
804 return this._devToolsCommandsPromise;
806 if (this._devToolsCommands) {
807 return this._devToolsCommands;
810 this._devToolsCommandsPromise = (async () => {
812 await lazy.DevToolsShim.createCommandsForTabForWebExtension(
813 this.devToolsToolbox.commands.descriptorFront.localTab
815 await commands.targetCommand.startListening();
816 this._devToolsCommands = commands;
817 this._devToolsCommandsPromise = null;
820 return this._devToolsCommandsPromise;
824 // Bail if the toolbox reference was already cleared.
825 if (!this.devToolsToolbox) {
829 if (this._onNavigatedListeners) {
830 this.devToolsToolbox.resourceCommand.unwatchResources(
831 [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
832 { onAvailable: this._onResourceAvailable }
836 if (this._devToolsCommands) {
837 this._devToolsCommands.destroy();
838 this._devToolsCommands = null;
841 if (this._onNavigatedListeners) {
842 this._onNavigatedListeners.clear();
843 this._onNavigatedListeners = null;
846 this._devToolsToolbox = null;
851 async _onResourceAvailable(resources) {
852 for (const resource of resources) {
853 const { targetFront } = resource;
854 if (targetFront.isTopLevel && resource.name === "dom-complete") {
855 for (const listener of this._onNavigatedListeners) {
856 listener(targetFront.url);
864 * The parent side of proxied API context for extension background service
867 class BackgroundWorkerContextParent extends ProxyContextParent {
868 constructor(envType, extension, params) {
869 // TODO: split out from ProxyContextParent a base class that
870 // doesn't expect a browsingContext and one for contexts that are
871 // expected to have a browsingContext associated.
872 super(envType, extension, params, null, extension.principal);
874 this.viewType = params.viewType;
875 this.workerDescriptorId = params.workerDescriptorId;
877 this.extension.views.add(this);
879 extension.emit("extension-proxy-context-load", this);
884 proxyContexts: new Map(),
887 // TODO: Bug 1595186 - remove/replace all usage of MessageManager below.
888 Services.obs.addObserver(this, "message-manager-close");
890 this.conduit = new lazy.BroadcastConduit(this, {
891 id: "ParentAPIManager",
892 reportOnClosed: "childId",
894 "CreateProxyContext",
900 send: ["CallResult"],
901 query: ["RunListener", "StreamFilterSuspendCancel"],
905 attachMessageManager(extension, processMessageManager) {
906 extension.parentMessageManager = processMessageManager;
909 async observe(subject, topic) {
910 if (topic === "message-manager-close") {
912 for (let [childId, context] of this.proxyContexts) {
913 if (context.parentMessageManager === mm) {
914 this.closeProxyContext(childId);
918 // Reset extension message managers when their child processes shut down.
919 for (let extension of GlobalManager.extensionMap.values()) {
920 if (extension.parentMessageManager === mm) {
921 extension.parentMessageManager = null;
927 shutdownExtension(extensionId, reason) {
928 if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(reason)) {
929 apiManager._callHandlers([extensionId], "disable", "onDisable");
932 for (let [childId, context] of this.proxyContexts) {
933 if (context.extension.id == extensionId) {
935 this.proxyContexts.delete(childId);
940 queryStreamFilterSuspendCancel(childId) {
941 return this.conduit.queryStreamFilterSuspendCancel(childId);
944 recvCreateProxyContext(data, { actor, sender }) {
945 let { envType, extensionId, childId, principal } = data;
947 if (this.proxyContexts.has(childId)) {
949 "A WebExtension context with the given ID already exists!"
953 let extension = GlobalManager.getExtension(extensionId);
955 throw new Error(`No WebExtension found with ID ${extensionId}`);
959 if (envType == "addon_parent" || envType == "devtools_parent") {
960 if (!sender.verified) {
961 throw new Error(`Bad sender context envType: ${sender.envType}`);
964 let isBackgroundWorker = false;
965 if (JSWindowActorParent.isInstance(actor)) {
966 const target = actor.browsingContext.top.embedderElement;
967 let processMessageManager =
968 target.messageManager.processMessageManager ||
969 Services.ppmm.getChildAt(0);
971 if (!extension.parentMessageManager) {
972 if (target.remoteType === extension.remoteType) {
973 this.attachMessageManager(extension, processMessageManager);
977 if (processMessageManager !== extension.parentMessageManager) {
979 "Attempt to create privileged extension parent from incorrect child process"
982 } else if (JSProcessActorParent.isInstance(actor)) {
983 if (actor.manager.remoteType !== extension.remoteType) {
985 "Attempt to create privileged extension parent from incorrect child process"
989 if (envType !== "addon_parent") {
991 `Unexpected envType ${envType} on an extension process actor`
994 if (data.viewType !== "background_worker") {
996 `Unexpected viewType ${data.viewType} on an extension process actor`
999 isBackgroundWorker = true;
1001 // Unreacheable: JSWindowActorParent and JSProcessActorParent are the
1004 "Attempt to create privileged extension parent via incorrect actor"
1008 if (isBackgroundWorker) {
1009 context = new BackgroundWorkerContextParent(envType, extension, data);
1010 } else if (envType == "addon_parent") {
1011 context = new ExtensionPageContextParent(
1015 actor.browsingContext
1017 } else if (envType == "devtools_parent") {
1018 context = new DevToolsExtensionPageContextParent(
1022 actor.browsingContext
1025 } else if (envType == "content_parent") {
1026 // Note: actor is always a JSWindowActorParent, with a browsingContext.
1027 context = new ContentScriptContextParent(
1031 actor.browsingContext,
1035 throw new Error(`Invalid WebExtension context envType: ${envType}`);
1037 this.proxyContexts.set(childId, context);
1040 recvContextLoaded(data, { actor }) {
1041 let context = this.getContextById(data.childId);
1042 verifyActorForContext(actor, context);
1043 const { extension } = context;
1044 extension.emit("extension-proxy-context-load:completed", context);
1047 recvConduitClosed(sender) {
1048 this.closeProxyContext(sender.id);
1051 closeProxyContext(childId) {
1052 let context = this.proxyContexts.get(childId);
1055 this.proxyContexts.delete(childId);
1060 * Call the given function and also log the call as appropriate
1061 * (i.e., with activity logging and/or profiler markers)
1063 * @param {BaseContext} context The context making this call.
1064 * @param {object} data Additional data about the call.
1065 * @param {Function} callable The actual implementation to invoke.
1067 async callAndLog(context, data, callable) {
1068 let { id } = context.extension;
1069 // If we were called via callParentAsyncFunction we don't want
1070 // to log again, check for the flag.
1071 const { alreadyLogged } = data.options || {};
1072 if (!alreadyLogged) {
1073 lazy.ExtensionActivityLog.log(
1084 let start = Cu.now();
1088 ChromeUtils.addProfilerMarker(
1090 { startTime: start },
1091 `${id}, api_call: ${data.path}`
1096 async recvAPICall(data, { actor }) {
1097 let context = this.getContextById(data.childId);
1098 let target = actor.browsingContext?.top.embedderElement;
1100 verifyActorForContext(actor, context);
1102 let reply = result => {
1103 if (target && !context.parentMessageManager) {
1104 Services.console.logStringMessage(
1105 "Cannot send function call result: other side closed connection " +
1106 `(call data: ${uneval({ path: data.path, args: data.args })})`
1111 this.conduit.sendCallResult(data.childId, {
1112 childId: data.childId,
1113 callId: data.callId,
1121 context.isBackgroundContext &&
1122 !context.extension.persistentBackground
1124 context.extension.emit("background-script-reset-idle", {
1125 reason: "parentApiCall",
1130 let args = data.args;
1131 let { isHandlingUserInput = false } = data.options || {};
1132 let pendingBrowser = context.pendingEventBrowser;
1133 let fun = await context.apiCan.asyncFindAPIPath(data.path);
1134 let result = this.callAndLog(context, data, () => {
1135 return context.withPendingBrowser(pendingBrowser, () =>
1136 context.withCallContextData({ isHandlingUserInput }, () =>
1143 result = result || Promise.resolve();
1147 result = result instanceof SpreadArgs ? [...result] : [result];
1149 let holder = new StructuredCloneHolder(
1150 `ExtensionParent/${context.extension.id}/recvAPICall/${data.path}`,
1155 reply({ result: holder });
1158 error = context.normalizeError(error);
1160 error: { message: error.message, fileName: error.fileName },
1167 let error = context.normalizeError(e);
1168 reply({ error: { message: error.message } });
1175 async recvAddListener(data, { actor }) {
1176 let context = this.getContextById(data.childId);
1178 verifyActorForContext(actor, context);
1180 let { childId, alreadyLogged = false } = data;
1181 let handlingUserInput = false;
1183 let listener = async (...listenerArgs) => {
1184 let startTime = Cu.now();
1185 // Extract urgentSend flag to avoid deserializing args holder later.
1186 let urgentSend = false;
1187 if (listenerArgs[0] && data.path.startsWith("webRequest.")) {
1188 urgentSend = listenerArgs[0].urgentSend;
1189 delete listenerArgs[0].urgentSend;
1191 let runListenerPromise = this.conduit.queryRunListener(childId, {
1194 listenerId: data.listenerId,
1198 return new StructuredCloneHolder(
1199 `ExtensionParent/${context.extension.id}/recvAddListener/${data.path}`,
1205 context.trackRunListenerPromise(runListenerPromise);
1207 const result = await runListenerPromise;
1208 let rv = result && result.deserialize(globalThis);
1209 ChromeUtils.addProfilerMarker(
1212 `${context.extension.id}, api_event: ${data.path}`
1214 lazy.ExtensionActivityLog.log(
1215 context.extension.id,
1219 { args: listenerArgs, result: rv }
1224 context.listenerProxies.set(data.listenerId, listener);
1226 let args = data.args;
1227 let promise = context.apiCan.asyncFindAPIPath(data.path);
1229 // Store pending listener additions so we can be sure they're all
1230 // fully initialize before we consider extension startup complete.
1231 if (context.isBackgroundContext && context.listenerPromises) {
1232 const { listenerPromises } = context;
1233 listenerPromises.add(promise);
1234 let remove = () => {
1235 listenerPromises.delete(promise);
1237 promise.then(remove, remove);
1240 let handler = await promise;
1241 if (handler.setUserInput) {
1242 handlingUserInput = true;
1244 handler.addListener(listener, ...args);
1245 if (!alreadyLogged) {
1246 lazy.ExtensionActivityLog.log(
1247 context.extension.id,
1250 `${data.path}.addListener`,
1256 async recvRemoveListener(data) {
1257 let context = this.getContextById(data.childId);
1258 let listener = context.listenerProxies.get(data.listenerId);
1260 let handler = await context.apiCan.asyncFindAPIPath(data.path);
1261 handler.removeListener(listener);
1263 let { alreadyLogged = false } = data;
1264 if (!alreadyLogged) {
1265 lazy.ExtensionActivityLog.log(
1266 context.extension.id,
1269 `${data.path}.removeListener`,
1275 getContextById(childId) {
1276 let context = this.proxyContexts.get(childId);
1278 throw new Error("WebExtension context not found!");
1284 ParentAPIManager.init();
1287 * A hidden window which contains the extension pages that are not visible
1288 * (i.e., background pages and devtools pages), and is also used by
1289 * ExtensionDebuggingUtils to contain the browser elements used by the
1290 * addon debugger to connect to the devtools actors running in the same
1291 * process of the target extension (and be able to stay connected across
1292 * the addon reloads).
1294 class HiddenXULWindow {
1296 this._windowlessBrowser = null;
1297 this.unloaded = false;
1298 this.waitInitialized = this.initWindowlessBrowser();
1302 if (this.unloaded) {
1304 "Unable to shutdown an unloaded HiddenXULWindow instance"
1308 this.unloaded = true;
1310 this.waitInitialized = null;
1312 if (!this._windowlessBrowser) {
1313 Cu.reportError("HiddenXULWindow was shut down while it was loading.");
1314 // initWindowlessBrowser will close windowlessBrowser when possible.
1318 this._windowlessBrowser.close();
1319 this._windowlessBrowser = null;
1322 get chromeDocument() {
1323 return this._windowlessBrowser.document;
1327 * Private helper that create a HTMLDocument in a windowless browser.
1329 * @returns {Promise<void>}
1330 * A promise which resolves when the windowless browser is ready.
1332 async initWindowlessBrowser() {
1333 if (this.waitInitialized) {
1334 throw new Error("HiddenXULWindow already initialized");
1337 // The invisible page is currently wrapped in a XUL window to fix an issue
1338 // with using the canvas API from a background page (See Bug 1274775).
1339 let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
1341 // The windowless browser is a thin wrapper around a docShell that keeps
1342 // its related resources alive. It implements nsIWebNavigation and
1343 // forwards its methods to the underlying docShell. That .docShell
1344 // needs `QueryInterface(nsIWebNavigation)` to give us access to the
1345 // webNav methods that are already available on the windowless browser.
1346 let chromeShell = windowlessBrowser.docShell;
1347 chromeShell.QueryInterface(Ci.nsIWebNavigation);
1349 if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
1350 let attrs = chromeShell.getOriginAttributes();
1351 attrs.privateBrowsingId = 1;
1352 chromeShell.setOriginAttributes(attrs);
1355 windowlessBrowser.browsingContext.useGlobalHistory = false;
1356 chromeShell.loadURI(DUMMY_PAGE_URI, {
1357 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
1360 await promiseObserved(
1361 "chrome-document-global-created",
1362 win => win.document == chromeShell.document
1364 await promiseDocumentLoaded(windowlessBrowser.document);
1365 if (this.unloaded) {
1366 windowlessBrowser.close();
1369 this._windowlessBrowser = windowlessBrowser;
1373 * Creates the browser XUL element that will contain the WebExtension Page.
1375 * @param {object} xulAttributes
1376 * An object that contains the xul attributes to set of the newly
1377 * created browser XUL element.
1379 * @returns {Promise<XULElement>}
1380 * A Promise which resolves to the newly created browser XUL element.
1382 async createBrowserElement(xulAttributes) {
1383 if (!xulAttributes || Object.keys(xulAttributes).length === 0) {
1384 throw new Error("missing mandatory xulAttributes parameter");
1387 await this.waitInitialized;
1389 const chromeDoc = this.chromeDocument;
1391 const browser = chromeDoc.createXULElement("browser");
1392 browser.setAttribute("type", "content");
1393 browser.setAttribute("disableglobalhistory", "true");
1394 browser.setAttribute("messagemanagergroup", "webext-browsers");
1395 browser.setAttribute("manualactiveness", "true");
1397 for (const [name, value] of Object.entries(xulAttributes)) {
1398 if (value != null) {
1399 browser.setAttribute(name, value);
1403 let awaitFrameLoader;
1405 if (browser.getAttribute("remote") === "true") {
1406 awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
1409 chromeDoc.documentElement.appendChild(browser);
1411 // Forcibly flush layout so that we get a pres shell soon enough, see
1413 browser.getBoundingClientRect();
1414 await awaitFrameLoader;
1416 // FIXME(emilio): This unconditionally active frame seems rather
1417 // unfortunate, but matches previous behavior.
1418 browser.docShellIsActive = true;
1424 const SharedWindow = {
1429 if (this._window == null) {
1430 if (this._count != 0) {
1432 `Shared window already exists with count ${this._count}`
1436 this._window = new HiddenXULWindow();
1440 return this._window;
1444 if (this._count < 1) {
1445 throw new Error(`Releasing shared window with count ${this._count}`);
1449 if (this._count == 0) {
1450 this._window.shutdown();
1451 this._window = null;
1457 * This is a base class used by the ext-backgroundPage and ext-devtools API implementations
1458 * to inherits the shared boilerplate code needed to create a parent document for the hidden
1459 * extension pages (e.g. the background page, the devtools page) in the BackgroundPage and
1460 * DevToolsPage classes.
1462 * @param {Extension} extension
1463 * The Extension which owns the hidden extension page created (used to decide
1464 * if the hidden extension page parent doc is going to be a windowlessBrowser or
1465 * a visible XUL window).
1466 * @param {string} viewType
1467 * The viewType of the WebExtension page that is going to be loaded
1468 * in the created browser element (e.g. "background" or "devtools_page").
1470 class HiddenExtensionPage {
1471 constructor(extension, viewType) {
1472 if (!extension || !viewType) {
1473 throw new Error("extension and viewType parameters are mandatory");
1476 this.extension = extension;
1477 this.viewType = viewType;
1478 this.browser = null;
1479 this.unloaded = false;
1483 * Destroy the created parent document.
1486 if (this.unloaded) {
1488 "Unable to shutdown an unloaded HiddenExtensionPage instance"
1492 this.unloaded = true;
1495 this._releaseBrowser();
1500 this.browser.remove();
1501 this.browser = null;
1502 SharedWindow.release();
1506 * Creates the browser XUL element that will contain the WebExtension Page.
1508 * @returns {Promise<XULElement>}
1509 * A Promise which resolves to the newly created browser XUL element.
1511 async createBrowserElement() {
1513 throw new Error("createBrowserElement called twice");
1516 let window = SharedWindow.acquire();
1518 this.browser = await window.createBrowserElement({
1519 "webextension-view-type": this.viewType,
1520 remote: this.extension.remote ? "true" : null,
1521 remoteType: this.extension.remoteType,
1522 initialBrowsingContextGroupId: this.extension.browsingContextGroupId,
1525 SharedWindow.release();
1529 if (this.unloaded) {
1530 this._releaseBrowser();
1531 throw new Error("Extension shut down before browser element was created");
1534 return this.browser;
1539 * This object provides utility functions needed by the devtools actors to
1540 * be able to connect and debug an extension (which can run in the main or in
1541 * a child extension process).
1543 const DebugUtils = {
1544 // A lazily created hidden XUL window, which contains the browser elements
1545 // which are used to connect the webextension patent actor to the extension process.
1546 hiddenXULWindow: null,
1548 // Map<extensionId, Promise<XULElement>>
1549 debugBrowserPromises: new Map(),
1550 // DefaultWeakMap<Promise<browser XULElement>, Set<WebExtensionParentActor>>
1551 debugActors: new DefaultWeakMap(() => new Set()),
1553 _extensionUpdatedWatcher: null,
1554 watchExtensionUpdated() {
1555 if (!this._extensionUpdatedWatcher) {
1556 // Watch the updated extension objects.
1557 this._extensionUpdatedWatcher = async (evt, extension) => {
1558 const browserPromise = this.debugBrowserPromises.get(extension.id);
1559 if (browserPromise) {
1560 const browser = await browserPromise;
1562 browser.isRemoteBrowser !== extension.remote &&
1563 this.debugBrowserPromises.get(extension.id) === browserPromise
1565 // If the cached browser element is not anymore of the same
1566 // remote type of the extension, remove it.
1567 this.debugBrowserPromises.delete(extension.id);
1573 apiManager.on("ready", this._extensionUpdatedWatcher);
1577 unwatchExtensionUpdated() {
1578 if (this._extensionUpdatedWatcher) {
1579 apiManager.off("ready", this._extensionUpdatedWatcher);
1580 delete this._extensionUpdatedWatcher;
1584 getExtensionManifestWarnings(id) {
1585 const addon = GlobalManager.extensionMap.get(id);
1587 return addon.warnings;
1593 * Determine if the extension does have a non-persistent background script
1594 * (either an event page or a background service worker):
1596 * Based on this the DevTools client will determine if this extension should provide
1597 * to the extension developers a button to forcefully terminate the background
1600 * @param {string} addonId
1601 * The id of the addon
1603 * @returns {void|boolean}
1604 * - undefined => does not apply (no background script in the manifest)
1605 * - true => the background script is persistent.
1606 * - false => the background script is an event page or a service worker.
1608 hasPersistentBackgroundScript(addonId) {
1609 const policy = WebExtensionPolicy.getByID(addonId);
1611 // The addon doesn't have any background script or we
1612 // can't be sure yet.
1614 policy?.extension?.type !== "extension" ||
1615 !policy?.extension?.manifest?.background
1620 return policy.extension.persistentBackground;
1624 * Determine if the extension background page is running.
1626 * Based on this the DevTools client will show the status of the background
1627 * script in about:debugging.
1629 * @param {string} addonId
1630 * The id of the addon
1632 * @returns {void|boolean}
1633 * - undefined => does not apply (no background script in the manifest)
1634 * - true => the background script is running.
1635 * - false => the background script is stopped.
1637 isBackgroundScriptRunning(addonId) {
1638 const policy = WebExtensionPolicy.getByID(addonId);
1640 // The addon doesn't have any background script or we
1641 // can't be sure yet.
1642 if (!(this.hasPersistentBackgroundScript(addonId) === false)) {
1646 const views = policy?.extension?.views || [];
1647 for (const view of views) {
1649 view.viewType === "background" ||
1650 (view.viewType === "background_worker" && !view.unloaded)
1659 async terminateBackgroundScript(addonId) {
1660 // Terminate the background if the extension does have
1661 // a non-persistent background script (event page or background
1663 if (this.hasPersistentBackgroundScript(addonId) === false) {
1664 const policy = WebExtensionPolicy.getByID(addonId);
1665 // When the event page is being terminated through the Devtools
1666 // action, we should terminate it even if there are DevTools
1667 // toolboxes attached to the extension.
1668 return policy.extension.terminateBackground({
1669 ignoreDevToolsAttached: true,
1672 throw Error(`Unable to terminate background script for ${addonId}`);
1676 * Determine whether a devtools toolbox attached to the extension.
1678 * This method is called by the background page idle timeout handler,
1679 * to inhibit terminating the event page when idle while the extension
1680 * developer is debugging the extension through the Addon Debugging window
1681 * (similarly to how service workers are kept alive while the devtools are
1684 * @param {string} id
1685 * The id of the extension.
1687 * @returns {boolean}
1688 * true when a devtools toolbox is attached to an extension with
1689 * the given id, false otherwise.
1691 hasDevToolsAttached(id) {
1692 return this.debugBrowserPromises.has(id);
1696 * Retrieve a XUL browser element which has been configured to be able to connect
1697 * the devtools actor with the process where the extension is running.
1699 * @param {WebExtensionParentActor} webExtensionParentActor
1700 * The devtools actor that is retrieving the browser element.
1702 * @returns {Promise<XULElement>}
1703 * A promise which resolves to the configured browser XUL element.
1705 async getExtensionProcessBrowser(webExtensionParentActor) {
1706 const extensionId = webExtensionParentActor.addonId;
1707 const extension = GlobalManager.getExtension(extensionId);
1709 throw new Error(`Extension not found: ${extensionId}`);
1712 const createBrowser = () => {
1713 if (!this.hiddenXULWindow) {
1714 this.hiddenXULWindow = new HiddenXULWindow();
1715 this.watchExtensionUpdated();
1718 return this.hiddenXULWindow.createBrowserElement({
1719 "webextension-addon-debug-target": extensionId,
1720 remote: extension.remote ? "true" : null,
1721 remoteType: extension.remoteType,
1722 initialBrowsingContextGroupId: extension.browsingContextGroupId,
1726 let browserPromise = this.debugBrowserPromises.get(extensionId);
1728 // Create a new promise if there is no cached one in the map.
1729 if (!browserPromise) {
1730 browserPromise = createBrowser();
1731 this.debugBrowserPromises.set(extensionId, browserPromise);
1732 browserPromise.then(browser => {
1733 browserPromise.browser = browser;
1735 browserPromise.catch(e => {
1737 this.debugBrowserPromises.delete(extensionId);
1741 this.debugActors.get(browserPromise).add(webExtensionParentActor);
1743 return browserPromise;
1746 getFrameLoader(extensionId) {
1747 let promise = this.debugBrowserPromises.get(extensionId);
1748 return promise && promise.browser && promise.browser.frameLoader;
1752 * Given the devtools actor that has retrieved an addon debug browser element,
1753 * it destroys the XUL browser element, and it also destroy the hidden XUL window
1754 * if it is not currently needed.
1756 * @param {WebExtensionParentActor} webExtensionParentActor
1757 * The devtools actor that has retrieved an addon debug browser element.
1759 async releaseExtensionProcessBrowser(webExtensionParentActor) {
1760 const extensionId = webExtensionParentActor.addonId;
1761 const browserPromise = this.debugBrowserPromises.get(extensionId);
1763 if (browserPromise) {
1764 const actorsSet = this.debugActors.get(browserPromise);
1765 actorsSet.delete(webExtensionParentActor);
1766 if (actorsSet.size === 0) {
1767 this.debugActors.delete(browserPromise);
1768 this.debugBrowserPromises.delete(extensionId);
1769 await browserPromise.then(browser => browser.remove());
1773 if (this.debugBrowserPromises.size === 0 && this.hiddenXULWindow) {
1774 this.hiddenXULWindow.shutdown();
1775 this.hiddenXULWindow = null;
1776 this.unwatchExtensionUpdated();
1782 * Returns a Promise which resolves with the message data when the given message
1783 * was received by the message manager. The promise is rejected if the message
1784 * manager was closed before a message was received.
1786 * @param {nsIMessageListenerManager} messageManager
1787 * The message manager on which to listen for messages.
1788 * @param {string} messageName
1789 * The message to listen for.
1790 * @returns {Promise<*>}
1792 function promiseMessageFromChild(messageManager, messageName) {
1793 return new Promise((resolve, reject) => {
1795 function listener(message) {
1797 resolve(message.data);
1799 function observer(subject) {
1800 if (subject === messageManager) {
1804 `Message manager was disconnected before receiving ${messageName}`
1809 unregister = () => {
1810 Services.obs.removeObserver(observer, "message-manager-close");
1811 messageManager.removeMessageListener(messageName, listener);
1813 messageManager.addMessageListener(messageName, listener);
1814 Services.obs.addObserver(observer, "message-manager-close");
1818 // This should be called before browser.loadURI is invoked.
1819 async function promiseBackgroundViewLoaded(browser) {
1820 let { childId } = await promiseMessageFromChild(
1821 browser.messageManager,
1822 "Extension:BackgroundViewLoaded"
1825 return ParentAPIManager.getContextById(childId);
1830 * This helper is used to subscribe a listener (e.g. in the ext-devtools API implementation)
1831 * to be called for every ExtensionProxyContext created for an extension page given
1832 * its related extension, viewType and browser element (both the top level context and any context
1833 * created for the extension urls running into its iframe descendants).
1835 * @param {object} params
1836 * @param {object} params.extension
1837 * The Extension on which we are going to listen for the newly created ExtensionProxyContext.
1838 * @param {string} params.viewType
1839 * The viewType of the WebExtension page that we are watching (e.g. "background" or
1841 * @param {XULElement} params.browser
1842 * The browser element of the WebExtension page that we are watching.
1843 * @param {Function} onExtensionProxyContextLoaded
1844 * The callback that is called when a new context has been loaded (as `callback(context)`);
1846 * @returns {Function}
1847 * Unsubscribe the listener.
1849 function watchExtensionProxyContextLoad(
1850 { extension, viewType, browser },
1851 onExtensionProxyContextLoaded
1853 if (typeof onExtensionProxyContextLoaded !== "function") {
1854 throw new Error("Missing onExtensionProxyContextLoaded handler");
1857 const listener = (event, context) => {
1858 if (context.viewType == viewType && context.xulBrowser == browser) {
1859 onExtensionProxyContextLoaded(context);
1863 extension.on("extension-proxy-context-load", listener);
1866 extension.off("extension-proxy-context-load", listener);
1871 * This helper is used to subscribe a listener (e.g. in the ext-backgroundPage)
1872 * to be called for every ExtensionProxyContext created for an extension
1873 * background service worker given its related extension.
1875 * @param {object} params
1876 * @param {object} params.extension
1877 * The Extension on which we are going to listen for the newly created ExtensionProxyContext.
1878 * @param {Function} onExtensionWorkerContextLoaded
1879 * The callback that is called when the worker script has been fully loaded (as `callback(context)`);
1881 * @returns {Function}
1882 * Unsubscribe the listener.
1884 function watchExtensionWorkerContextLoaded(
1886 onExtensionWorkerContextLoaded
1888 if (typeof onExtensionWorkerContextLoaded !== "function") {
1889 throw new Error("Missing onExtensionWorkerContextLoaded handler");
1892 const listener = (event, context) => {
1893 if (context.viewType == "background_worker") {
1894 onExtensionWorkerContextLoaded(context);
1898 extension.on("extension-proxy-context-load:completed", listener);
1901 extension.off("extension-proxy-context-load:completed", listener);
1905 // Manages icon details for toolbar buttons in the |pageAction| and
1906 // |browserAction| APIs.
1908 DEFAULT_ICON: "chrome://mozapps/skin/extensions/extensionGeneric.svg",
1910 // WeakMap<Extension -> Map<url-string -> Map<iconType-string -> object>>>
1911 iconCache: new DefaultWeakMap(() => {
1912 return new DefaultMap(() => new DefaultMap(() => new Map()));
1915 // Normalizes the various acceptable input formats into an object
1916 // with icon size as key and icon URL as value.
1918 // If a context is specified (function is called from an extension):
1919 // Throws an error if an invalid icon size was provided or the
1920 // extension is not allowed to load the specified resources.
1922 // If no context is specified, instead of throwing an error, this
1923 // function simply logs a warning message.
1924 normalize(details, extension, context = null) {
1925 if (!details.imageData && details.path != null) {
1926 // Pick a cache key for the icon paths. If the path is a string,
1927 // use it directly. Otherwise, stringify the path object.
1928 let key = details.path;
1929 if (typeof key !== "string") {
1933 let icons = this.iconCache
1935 .get(context && context.uri.spec)
1936 .get(details.iconType);
1938 let icon = icons.get(key);
1940 icon = this._normalize(details, extension, context);
1941 icons.set(key, icon);
1946 return this._normalize(details, extension, context);
1949 _normalize(details, extension, context = null) {
1953 let { imageData, path, themeIcons } = details;
1956 if (typeof imageData == "string") {
1957 imageData = { 19: imageData };
1960 for (let size of Object.keys(imageData)) {
1961 result[size] = imageData[size];
1965 let baseURI = context ? context.uri : extension.baseURI;
1968 if (typeof path != "object") {
1969 path = { 19: path };
1972 for (let size of Object.keys(path)) {
1973 let url = path[size];
1975 url = baseURI.resolve(path[size]);
1977 // The Chrome documentation specifies these parameters as
1978 // relative paths. We currently accept absolute URLs as well,
1979 // which means we need to check that the extension is allowed
1980 // to load them. This will throw an error if it's not allowed.
1981 this._checkURL(url, extension);
1983 result[size] = url || this.DEFAULT_ICON;
1988 themeIcons.forEach(({ size, light, dark }) => {
1989 let lightURL = baseURI.resolve(light);
1990 let darkURL = baseURI.resolve(dark);
1992 this._checkURL(lightURL, extension);
1993 this._checkURL(darkURL, extension);
1995 let defaultURL = result[size] || result[19]; // always fallback to default first
1997 default: defaultURL || darkURL, // Fallback to the dark url if no default is specified.
2004 // Function is called from extension code, delegate error.
2008 // If there's no context, it's because we're handling this
2009 // as a manifest directive. Log a warning rather than
2010 // raising an error.
2011 extension.manifestError(`Invalid icon data: ${e}`);
2017 // Checks if the extension is allowed to load the given URL with the specified principal.
2018 // This will throw an error if the URL is not allowed.
2019 _checkURL(url, extension) {
2020 if (!extension.checkLoadURL(url, { allowInheritsPrincipal: true })) {
2021 throw new ExtensionError(`Illegal URL ${url}`);
2025 // Returns the appropriate icon URL for the given icons object and the
2026 // screen resolution of the given window.
2027 getPreferredIcon(icons, extension, size = 16) {
2028 const DEFAULT = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
2030 let bestSize = null;
2033 } else if (icons[2 * size]) {
2034 bestSize = 2 * size;
2036 let sizes = Object.keys(icons)
2037 .map(key => parseInt(key, 10))
2038 .sort((a, b) => a - b);
2040 bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
2044 return { size: bestSize, icon: icons[bestSize] || DEFAULT };
2047 return { size, icon: DEFAULT };
2050 // These URLs should already be properly escaped, but make doubly sure CSS
2051 // string escape characters are escaped here, since they could lead to a
2054 return url.replace(/[\\\s"]/g, encodeURIComponent);
2059 constructor(storeName) {
2060 this.storeName = storeName;
2063 async getStore(path = null) {
2064 let data = await StartupCache.dataPromise;
2066 let store = data.get(this.storeName);
2069 data.set(this.storeName, store);
2073 if (Array.isArray(path)) {
2074 for (let elem of path.slice(0, -1)) {
2075 let next = store.get(elem);
2078 store.set(elem, next);
2082 key = path[path.length - 1];
2085 return [store, key];
2088 async get(path, createFunc) {
2089 let [store, key] = await this.getStore(path);
2091 let result = store.get(key);
2093 if (result === undefined) {
2094 result = await createFunc(path);
2095 store.set(key, result);
2096 StartupCache.save();
2102 async set(path, value) {
2103 let [store, key] = await this.getStore(path);
2105 store.set(key, value);
2106 StartupCache.save();
2110 let [store] = await this.getStore();
2112 return new Map(store);
2115 async delete(path) {
2116 let [store, key] = await this.getStore(path);
2118 if (store.delete(key)) {
2119 StartupCache.save();
2124 // A cache to support faster initialization of extensions at browser startup.
2125 // All cached data is removed when the browser is updated.
2126 // Extension-specific data is removed when the add-on is updated.
2127 var StartupCache = {
2128 _ensureDirectoryPromise: null,
2131 _ensureDirectory() {
2132 if (this._ensureDirectoryPromise === null) {
2133 this._ensureDirectoryPromise = IOUtils.makeDirectory(
2134 PathUtils.parent(this.file),
2136 ignoreExisting: true,
2137 createAncestors: true,
2142 return this._ensureDirectoryPromise;
2145 // When the application version changes, this file is removed by
2146 // RemoveComponentRegistries in nsAppRunner.cpp.
2147 file: PathUtils.join(
2148 Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
2154 let data = new Uint8Array(lazy.aomStartup.encodeBlob(this._data));
2155 await this._ensureDirectoryPromise;
2156 await IOUtils.write(this.file, data, { tmpPath: `${this.file}.tmp` });
2158 Glean.extensions.startupCacheWriteBytelength.set(data.byteLength);
2162 this._ensureDirectory();
2164 if (!this._saveTask) {
2165 this._saveTask = new lazy.DeferredTask(() => this._saveNow(), 5000);
2167 IOUtils.profileBeforeChange.addBlocker(
2168 "Flush WebExtension StartupCache",
2170 await this._saveTask.finalize();
2171 this._saveTask = null;
2176 return this._saveTask.arm();
2181 let result = new Map();
2183 Glean.extensions.startupCacheLoadTime.start();
2184 let { buffer } = await IOUtils.read(this.file);
2186 result = lazy.aomStartup.decodeBlob(buffer);
2187 Glean.extensions.startupCacheLoadTime.stop();
2189 Glean.extensions.startupCacheLoadTime.cancel();
2190 if (!DOMException.isInstance(e) || e.name !== "NotFoundError") {
2193 let error = lazy.getErrorNameForTelemetry(e);
2194 Glean.extensions.startupCacheReadErrors[error].add(1);
2197 this._data = result;
2202 if (!this._dataPromise) {
2203 this._dataPromise = this._readData();
2205 return this._dataPromise;
2208 clearAddonData(id) {
2209 return Promise.all([
2210 this.general.delete(id),
2211 this.locales.delete(id),
2212 this.manifests.delete(id),
2213 this.permissions.delete(id),
2214 this.menus.delete(id),
2216 // Ignore the error. It happens when we try to flush the add-on
2217 // data after the AddonManager has flushed the entire startup cache.
2221 observe(subject, topic) {
2222 if (topic === "startupcache-invalidate") {
2223 this._data = new Map();
2224 this._dataPromise = Promise.resolve(this._data);
2228 get(extension, path, createFunc) {
2229 return this.general.get(
2230 [extension.id, extension.version, ...path],
2235 delete(extension, path) {
2236 return this.general.delete([extension.id, extension.version, ...path]);
2239 general: new CacheStore("general"),
2240 locales: new CacheStore("locales"),
2241 manifests: new CacheStore("manifests"),
2242 other: new CacheStore("other"),
2243 permissions: new CacheStore("permissions"),
2244 schemas: new CacheStore("schemas"),
2245 menus: new CacheStore("menus"),
2248 Services.obs.addObserver(StartupCache, "startupcache-invalidate");
2250 export var ExtensionParent = {
2252 HiddenExtensionPage,
2258 promiseBackgroundViewLoaded,
2259 watchExtensionProxyContextLoad,
2260 watchExtensionWorkerContextLoaded,
2264 // browserPaintedPromise and browserStartupPromise are promises that
2265 // resolve after the first browser window is painted and after browser
2266 // windows have been restored, respectively. Alternatively,
2267 // browserStartupPromise also resolves from the extensions-late-startup
2268 // notification sent by Firefox Reality on desktop platforms, because it
2269 // doesn't support SessionStore.
2270 // _resetStartupPromises should only be called from outside this file in tests.
2271 ExtensionParent._resetStartupPromises = () => {
2272 ExtensionParent.browserPaintedPromise = promiseObserved(
2273 "browser-delayed-startup-finished"
2275 ExtensionParent.browserStartupPromise = Promise.race([
2276 promiseObserved("sessionstore-windows-restored"),
2277 promiseObserved("extensions-late-startup"),
2280 ExtensionParent._resetStartupPromises();
2282 ChromeUtils.defineLazyGetter(ExtensionParent, "PlatformInfo", () => {
2283 return Object.freeze({
2285 let os = AppConstants.platform;
2286 if (os == "macosx") {
2291 arch: (function () {
2292 let abi = Services.appinfo.XPCOMABI;
2293 let [arch] = abi.split("-");
2294 if (arch == "x86") {
2296 } else if (arch == "x86_64") {