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.jsm.
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, 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.jsm, 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;
628 logActivity(type, name, data) {
629 // The base class will throw so we catch any subclasses that do not implement.
630 // We do not want to throw here, but we also do not log here.
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.jsm.
690 class ContentScriptContextParent extends ProxyContextParent {}
693 * The parent side of proxied API context for extension page, such as a
694 * background script, a tab page, or a popup, running in
695 * ExtensionChild.jsm.
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;
722 let { tabTracker } = apiManager.global;
723 let data = tabTracker.getBrowserData(this.xulBrowser);
724 if (data.tabId >= 0) {
731 this.extension.views.delete(this);
735 apiManager.emit("page-shutdown", this);
741 * The parent side of proxied API context for devtools extension page, such as a
742 * devtools pages and panels running in ExtensionChild.jsm.
744 class DevToolsExtensionPageContextParent extends ExtensionPageContextParent {
745 constructor(...params) {
748 // Set all attributes that are lazily defined to `null` here.
750 // Note that we can't do that for `this._devToolsToolbox` because it will
751 // be defined when calling our parent constructor and so would override it back to `null`.
752 this._devToolsCommands = null;
753 this._onNavigatedListeners = null;
755 this._onResourceAvailable = this._onResourceAvailable.bind(this);
758 set devToolsToolbox(toolbox) {
759 if (this._devToolsToolbox) {
760 throw new Error("Cannot set the context DevTools toolbox twice");
763 this._devToolsToolbox = toolbox;
766 get devToolsToolbox() {
767 return this._devToolsToolbox;
770 async addOnNavigatedListener(listener) {
771 if (!this._onNavigatedListeners) {
772 this._onNavigatedListeners = new Set();
774 await this.devToolsToolbox.resourceCommand.watchResources(
775 [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
777 onAvailable: this._onResourceAvailable,
778 ignoreExistingResources: true,
783 this._onNavigatedListeners.add(listener);
786 removeOnNavigatedListener(listener) {
787 if (this._onNavigatedListeners) {
788 this._onNavigatedListeners.delete(listener);
793 * The returned "commands" object, exposing modules implemented from devtools/shared/commands.
794 * Each attribute being a static interface to communicate with the server backend.
796 * @returns {Promise<object>}
798 async getDevToolsCommands() {
799 // Ensure that we try to instantiate a commands only once,
800 // even if createCommandsForTabForWebExtension is async.
801 if (this._devToolsCommandsPromise) {
802 return this._devToolsCommandsPromise;
804 if (this._devToolsCommands) {
805 return this._devToolsCommands;
808 this._devToolsCommandsPromise = (async () => {
810 await lazy.DevToolsShim.createCommandsForTabForWebExtension(
811 this.devToolsToolbox.commands.descriptorFront.localTab
813 await commands.targetCommand.startListening();
814 this._devToolsCommands = commands;
815 this._devToolsCommandsPromise = null;
818 return this._devToolsCommandsPromise;
822 // Bail if the toolbox reference was already cleared.
823 if (!this.devToolsToolbox) {
827 if (this._onNavigatedListeners) {
828 this.devToolsToolbox.resourceCommand.unwatchResources(
829 [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
830 { onAvailable: this._onResourceAvailable }
834 if (this._devToolsCommands) {
835 this._devToolsCommands.destroy();
836 this._devToolsCommands = null;
839 if (this._onNavigatedListeners) {
840 this._onNavigatedListeners.clear();
841 this._onNavigatedListeners = null;
844 this._devToolsToolbox = null;
849 async _onResourceAvailable(resources) {
850 for (const resource of resources) {
851 const { targetFront } = resource;
852 if (targetFront.isTopLevel && resource.name === "dom-complete") {
853 for (const listener of this._onNavigatedListeners) {
854 listener(targetFront.url);
862 * The parent side of proxied API context for extension background service
865 class BackgroundWorkerContextParent extends ProxyContextParent {
866 constructor(envType, extension, params) {
867 // TODO: split out from ProxyContextParent a base class that
868 // doesn't expect a browsingContext and one for contexts that are
869 // expected to have a browsingContext associated.
870 super(envType, extension, params, null, extension.principal);
872 this.viewType = params.viewType;
873 this.workerDescriptorId = params.workerDescriptorId;
875 this.extension.views.add(this);
877 extension.emit("extension-proxy-context-load", this);
882 proxyContexts: new Map(),
885 // TODO: Bug 1595186 - remove/replace all usage of MessageManager below.
886 Services.obs.addObserver(this, "message-manager-close");
888 this.conduit = new lazy.BroadcastConduit(this, {
889 id: "ParentAPIManager",
890 reportOnClosed: "childId",
892 "CreateProxyContext",
898 send: ["CallResult"],
899 query: ["RunListener", "StreamFilterSuspendCancel"],
903 attachMessageManager(extension, processMessageManager) {
904 extension.parentMessageManager = processMessageManager;
907 async observe(subject, topic, data) {
908 if (topic === "message-manager-close") {
910 for (let [childId, context] of this.proxyContexts) {
911 if (context.parentMessageManager === mm) {
912 this.closeProxyContext(childId);
916 // Reset extension message managers when their child processes shut down.
917 for (let extension of GlobalManager.extensionMap.values()) {
918 if (extension.parentMessageManager === mm) {
919 extension.parentMessageManager = null;
925 shutdownExtension(extensionId, reason) {
926 if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(reason)) {
927 apiManager._callHandlers([extensionId], "disable", "onDisable");
930 for (let [childId, context] of this.proxyContexts) {
931 if (context.extension.id == extensionId) {
933 this.proxyContexts.delete(childId);
938 queryStreamFilterSuspendCancel(childId) {
939 return this.conduit.queryStreamFilterSuspendCancel(childId);
942 recvCreateProxyContext(data, { actor, sender }) {
943 let { envType, extensionId, childId, principal } = data;
945 if (this.proxyContexts.has(childId)) {
947 "A WebExtension context with the given ID already exists!"
951 let extension = GlobalManager.getExtension(extensionId);
953 throw new Error(`No WebExtension found with ID ${extensionId}`);
957 if (envType == "addon_parent" || envType == "devtools_parent") {
958 if (!sender.verified) {
959 throw new Error(`Bad sender context envType: ${sender.envType}`);
962 let isBackgroundWorker = false;
963 if (JSWindowActorParent.isInstance(actor)) {
964 const target = actor.browsingContext.top.embedderElement;
965 let processMessageManager =
966 target.messageManager.processMessageManager ||
967 Services.ppmm.getChildAt(0);
969 if (!extension.parentMessageManager) {
970 if (target.remoteType === extension.remoteType) {
971 this.attachMessageManager(extension, processMessageManager);
975 if (processMessageManager !== extension.parentMessageManager) {
977 "Attempt to create privileged extension parent from incorrect child process"
980 } else if (JSProcessActorParent.isInstance(actor)) {
981 if (actor.manager.remoteType !== extension.remoteType) {
983 "Attempt to create privileged extension parent from incorrect child process"
987 if (envType !== "addon_parent") {
989 `Unexpected envType ${envType} on an extension process actor`
992 if (data.viewType !== "background_worker") {
994 `Unexpected viewType ${data.viewType} on an extension process actor`
997 isBackgroundWorker = true;
999 // Unreacheable: JSWindowActorParent and JSProcessActorParent are the
1002 "Attempt to create privileged extension parent via incorrect actor"
1006 if (isBackgroundWorker) {
1007 context = new BackgroundWorkerContextParent(envType, extension, data);
1008 } else if (envType == "addon_parent") {
1009 context = new ExtensionPageContextParent(
1013 actor.browsingContext
1015 } else if (envType == "devtools_parent") {
1016 context = new DevToolsExtensionPageContextParent(
1020 actor.browsingContext
1023 } else if (envType == "content_parent") {
1024 // Note: actor is always a JSWindowActorParent, with a browsingContext.
1025 context = new ContentScriptContextParent(
1029 actor.browsingContext,
1033 throw new Error(`Invalid WebExtension context envType: ${envType}`);
1035 this.proxyContexts.set(childId, context);
1038 recvContextLoaded(data, { actor, sender }) {
1039 let context = this.getContextById(data.childId);
1040 verifyActorForContext(actor, context);
1041 const { extension } = context;
1042 extension.emit("extension-proxy-context-load:completed", context);
1045 recvConduitClosed(sender) {
1046 this.closeProxyContext(sender.id);
1049 closeProxyContext(childId) {
1050 let context = this.proxyContexts.get(childId);
1053 this.proxyContexts.delete(childId);
1058 * Call the given function and also log the call as appropriate
1059 * (i.e., with activity logging and/or profiler markers)
1061 * @param {BaseContext} context The context making this call.
1062 * @param {object} data Additional data about the call.
1063 * @param {Function} callable The actual implementation to invoke.
1065 async callAndLog(context, data, callable) {
1066 let { id } = context.extension;
1067 // If we were called via callParentAsyncFunction we don't want
1068 // to log again, check for the flag.
1069 const { alreadyLogged } = data.options || {};
1070 if (!alreadyLogged) {
1071 lazy.ExtensionActivityLog.log(
1082 let start = Cu.now();
1086 ChromeUtils.addProfilerMarker(
1088 { startTime: start },
1089 `${id}, api_call: ${data.path}`
1094 async recvAPICall(data, { actor }) {
1095 let context = this.getContextById(data.childId);
1096 let target = actor.browsingContext?.top.embedderElement;
1098 verifyActorForContext(actor, context);
1100 let reply = result => {
1101 if (target && !context.parentMessageManager) {
1102 Services.console.logStringMessage(
1103 "Cannot send function call result: other side closed connection " +
1104 `(call data: ${uneval({ path: data.path, args: data.args })})`
1109 this.conduit.sendCallResult(data.childId, {
1110 childId: data.childId,
1111 callId: data.callId,
1119 context.isBackgroundContext &&
1120 !context.extension.persistentBackground
1122 context.extension.emit("background-script-reset-idle", {
1123 reason: "parentApiCall",
1128 let args = data.args;
1129 let { isHandlingUserInput = false } = data.options || {};
1130 let pendingBrowser = context.pendingEventBrowser;
1131 let fun = await context.apiCan.asyncFindAPIPath(data.path);
1132 let result = this.callAndLog(context, data, () => {
1133 return context.withPendingBrowser(pendingBrowser, () =>
1134 context.withCallContextData({ isHandlingUserInput }, () =>
1141 result = result || Promise.resolve();
1145 result = result instanceof SpreadArgs ? [...result] : [result];
1147 let holder = new StructuredCloneHolder(
1148 `ExtensionParent/${context.extension.id}/recvAPICall/${data.path}`,
1153 reply({ result: holder });
1156 error = context.normalizeError(error);
1158 error: { message: error.message, fileName: error.fileName },
1165 let error = context.normalizeError(e);
1166 reply({ error: { message: error.message } });
1173 async recvAddListener(data, { actor }) {
1174 let context = this.getContextById(data.childId);
1176 verifyActorForContext(actor, context);
1178 let { childId, alreadyLogged = false } = data;
1179 let handlingUserInput = false;
1181 let listener = async (...listenerArgs) => {
1182 let startTime = Cu.now();
1183 // Extract urgentSend flag to avoid deserializing args holder later.
1184 let urgentSend = false;
1185 if (listenerArgs[0] && data.path.startsWith("webRequest.")) {
1186 urgentSend = listenerArgs[0].urgentSend;
1187 delete listenerArgs[0].urgentSend;
1189 let runListenerPromise = this.conduit.queryRunListener(childId, {
1192 listenerId: data.listenerId,
1196 return new StructuredCloneHolder(
1197 `ExtensionParent/${context.extension.id}/recvAddListener/${data.path}`,
1203 context.trackRunListenerPromise(runListenerPromise);
1205 const result = await runListenerPromise;
1206 let rv = result && result.deserialize(globalThis);
1207 ChromeUtils.addProfilerMarker(
1210 `${context.extension.id}, api_event: ${data.path}`
1212 lazy.ExtensionActivityLog.log(
1213 context.extension.id,
1217 { args: listenerArgs, result: rv }
1222 context.listenerProxies.set(data.listenerId, listener);
1224 let args = data.args;
1225 let promise = context.apiCan.asyncFindAPIPath(data.path);
1227 // Store pending listener additions so we can be sure they're all
1228 // fully initialize before we consider extension startup complete.
1229 if (context.isBackgroundContext && context.listenerPromises) {
1230 const { listenerPromises } = context;
1231 listenerPromises.add(promise);
1232 let remove = () => {
1233 listenerPromises.delete(promise);
1235 promise.then(remove, remove);
1238 let handler = await promise;
1239 if (handler.setUserInput) {
1240 handlingUserInput = true;
1242 handler.addListener(listener, ...args);
1243 if (!alreadyLogged) {
1244 lazy.ExtensionActivityLog.log(
1245 context.extension.id,
1248 `${data.path}.addListener`,
1254 async recvRemoveListener(data) {
1255 let context = this.getContextById(data.childId);
1256 let listener = context.listenerProxies.get(data.listenerId);
1258 let handler = await context.apiCan.asyncFindAPIPath(data.path);
1259 handler.removeListener(listener);
1261 let { alreadyLogged = false } = data;
1262 if (!alreadyLogged) {
1263 lazy.ExtensionActivityLog.log(
1264 context.extension.id,
1267 `${data.path}.removeListener`,
1273 getContextById(childId) {
1274 let context = this.proxyContexts.get(childId);
1276 throw new Error("WebExtension context not found!");
1282 ParentAPIManager.init();
1285 * A hidden window which contains the extension pages that are not visible
1286 * (i.e., background pages and devtools pages), and is also used by
1287 * ExtensionDebuggingUtils to contain the browser elements used by the
1288 * addon debugger to connect to the devtools actors running in the same
1289 * process of the target extension (and be able to stay connected across
1290 * the addon reloads).
1292 class HiddenXULWindow {
1294 this._windowlessBrowser = null;
1295 this.unloaded = false;
1296 this.waitInitialized = this.initWindowlessBrowser();
1300 if (this.unloaded) {
1302 "Unable to shutdown an unloaded HiddenXULWindow instance"
1306 this.unloaded = true;
1308 this.waitInitialized = null;
1310 if (!this._windowlessBrowser) {
1311 Cu.reportError("HiddenXULWindow was shut down while it was loading.");
1312 // initWindowlessBrowser will close windowlessBrowser when possible.
1316 this._windowlessBrowser.close();
1317 this._windowlessBrowser = null;
1320 get chromeDocument() {
1321 return this._windowlessBrowser.document;
1325 * Private helper that create a HTMLDocument in a windowless browser.
1327 * @returns {Promise<void>}
1328 * A promise which resolves when the windowless browser is ready.
1330 async initWindowlessBrowser() {
1331 if (this.waitInitialized) {
1332 throw new Error("HiddenXULWindow already initialized");
1335 // The invisible page is currently wrapped in a XUL window to fix an issue
1336 // with using the canvas API from a background page (See Bug 1274775).
1337 let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
1339 // The windowless browser is a thin wrapper around a docShell that keeps
1340 // its related resources alive. It implements nsIWebNavigation and
1341 // forwards its methods to the underlying docShell. That .docShell
1342 // needs `QueryInterface(nsIWebNavigation)` to give us access to the
1343 // webNav methods that are already available on the windowless browser.
1344 let chromeShell = windowlessBrowser.docShell;
1345 chromeShell.QueryInterface(Ci.nsIWebNavigation);
1347 if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
1348 let attrs = chromeShell.getOriginAttributes();
1349 attrs.privateBrowsingId = 1;
1350 chromeShell.setOriginAttributes(attrs);
1353 windowlessBrowser.browsingContext.useGlobalHistory = false;
1354 chromeShell.loadURI(DUMMY_PAGE_URI, {
1355 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
1358 await promiseObserved(
1359 "chrome-document-global-created",
1360 win => win.document == chromeShell.document
1362 await promiseDocumentLoaded(windowlessBrowser.document);
1363 if (this.unloaded) {
1364 windowlessBrowser.close();
1367 this._windowlessBrowser = windowlessBrowser;
1371 * Creates the browser XUL element that will contain the WebExtension Page.
1373 * @param {object} xulAttributes
1374 * An object that contains the xul attributes to set of the newly
1375 * created browser XUL element.
1377 * @returns {Promise<XULElement>}
1378 * A Promise which resolves to the newly created browser XUL element.
1380 async createBrowserElement(xulAttributes) {
1381 if (!xulAttributes || Object.keys(xulAttributes).length === 0) {
1382 throw new Error("missing mandatory xulAttributes parameter");
1385 await this.waitInitialized;
1387 const chromeDoc = this.chromeDocument;
1389 const browser = chromeDoc.createXULElement("browser");
1390 browser.setAttribute("type", "content");
1391 browser.setAttribute("disableglobalhistory", "true");
1392 browser.setAttribute("messagemanagergroup", "webext-browsers");
1394 for (const [name, value] of Object.entries(xulAttributes)) {
1395 if (value != null) {
1396 browser.setAttribute(name, value);
1400 let awaitFrameLoader;
1402 if (browser.getAttribute("remote") === "true") {
1403 awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
1406 chromeDoc.documentElement.appendChild(browser);
1408 // Forcibly flush layout so that we get a pres shell soon enough, see
1410 browser.getBoundingClientRect();
1412 await awaitFrameLoader;
1417 const SharedWindow = {
1422 if (this._window == null) {
1423 if (this._count != 0) {
1425 `Shared window already exists with count ${this._count}`
1429 this._window = new HiddenXULWindow();
1433 return this._window;
1437 if (this._count < 1) {
1438 throw new Error(`Releasing shared window with count ${this._count}`);
1442 if (this._count == 0) {
1443 this._window.shutdown();
1444 this._window = null;
1450 * This is a base class used by the ext-backgroundPage and ext-devtools API implementations
1451 * to inherits the shared boilerplate code needed to create a parent document for the hidden
1452 * extension pages (e.g. the background page, the devtools page) in the BackgroundPage and
1453 * DevToolsPage classes.
1455 * @param {Extension} extension
1456 * The Extension which owns the hidden extension page created (used to decide
1457 * if the hidden extension page parent doc is going to be a windowlessBrowser or
1458 * a visible XUL window).
1459 * @param {string} viewType
1460 * The viewType of the WebExtension page that is going to be loaded
1461 * in the created browser element (e.g. "background" or "devtools_page").
1463 class HiddenExtensionPage {
1464 constructor(extension, viewType) {
1465 if (!extension || !viewType) {
1466 throw new Error("extension and viewType parameters are mandatory");
1469 this.extension = extension;
1470 this.viewType = viewType;
1471 this.browser = null;
1472 this.unloaded = false;
1476 * Destroy the created parent document.
1479 if (this.unloaded) {
1481 "Unable to shutdown an unloaded HiddenExtensionPage instance"
1485 this.unloaded = true;
1488 this._releaseBrowser();
1493 this.browser.remove();
1494 this.browser = null;
1495 SharedWindow.release();
1499 * Creates the browser XUL element that will contain the WebExtension Page.
1501 * @returns {Promise<XULElement>}
1502 * A Promise which resolves to the newly created browser XUL element.
1504 async createBrowserElement() {
1506 throw new Error("createBrowserElement called twice");
1509 let window = SharedWindow.acquire();
1511 this.browser = await window.createBrowserElement({
1512 "webextension-view-type": this.viewType,
1513 remote: this.extension.remote ? "true" : null,
1514 remoteType: this.extension.remoteType,
1515 initialBrowsingContextGroupId: this.extension.browsingContextGroupId,
1518 SharedWindow.release();
1522 if (this.unloaded) {
1523 this._releaseBrowser();
1524 throw new Error("Extension shut down before browser element was created");
1527 return this.browser;
1532 * This object provides utility functions needed by the devtools actors to
1533 * be able to connect and debug an extension (which can run in the main or in
1534 * a child extension process).
1536 const DebugUtils = {
1537 // A lazily created hidden XUL window, which contains the browser elements
1538 // which are used to connect the webextension patent actor to the extension process.
1539 hiddenXULWindow: null,
1541 // Map<extensionId, Promise<XULElement>>
1542 debugBrowserPromises: new Map(),
1543 // DefaultWeakMap<Promise<browser XULElement>, Set<WebExtensionParentActor>>
1544 debugActors: new DefaultWeakMap(() => new Set()),
1546 _extensionUpdatedWatcher: null,
1547 watchExtensionUpdated() {
1548 if (!this._extensionUpdatedWatcher) {
1549 // Watch the updated extension objects.
1550 this._extensionUpdatedWatcher = async (evt, extension) => {
1551 const browserPromise = this.debugBrowserPromises.get(extension.id);
1552 if (browserPromise) {
1553 const browser = await browserPromise;
1555 browser.isRemoteBrowser !== extension.remote &&
1556 this.debugBrowserPromises.get(extension.id) === browserPromise
1558 // If the cached browser element is not anymore of the same
1559 // remote type of the extension, remove it.
1560 this.debugBrowserPromises.delete(extension.id);
1566 apiManager.on("ready", this._extensionUpdatedWatcher);
1570 unwatchExtensionUpdated() {
1571 if (this._extensionUpdatedWatcher) {
1572 apiManager.off("ready", this._extensionUpdatedWatcher);
1573 delete this._extensionUpdatedWatcher;
1577 getExtensionManifestWarnings(id) {
1578 const addon = GlobalManager.extensionMap.get(id);
1580 return addon.warnings;
1586 * Determine if the extension does have a non-persistent background script
1587 * (either an event page or a background service worker):
1589 * Based on this the DevTools client will determine if this extension should provide
1590 * to the extension developers a button to forcefully terminate the background
1593 * @param {string} addonId
1594 * The id of the addon
1596 * @returns {void|boolean}
1597 * - undefined => does not apply (no background script in the manifest)
1598 * - true => the background script is persistent.
1599 * - false => the background script is an event page or a service worker.
1601 hasPersistentBackgroundScript(addonId) {
1602 const policy = WebExtensionPolicy.getByID(addonId);
1604 // The addon doesn't have any background script or we
1605 // can't be sure yet.
1607 policy?.extension?.type !== "extension" ||
1608 !policy?.extension?.manifest?.background
1613 return policy.extension.persistentBackground;
1617 * Determine if the extension background page is running.
1619 * Based on this the DevTools client will show the status of the background
1620 * script in about:debugging.
1622 * @param {string} addonId
1623 * The id of the addon
1625 * @returns {void|boolean}
1626 * - undefined => does not apply (no background script in the manifest)
1627 * - true => the background script is running.
1628 * - false => the background script is stopped.
1630 isBackgroundScriptRunning(addonId) {
1631 const policy = WebExtensionPolicy.getByID(addonId);
1633 // The addon doesn't have any background script or we
1634 // can't be sure yet.
1635 if (!(this.hasPersistentBackgroundScript(addonId) === false)) {
1639 const views = policy?.extension?.views || [];
1640 for (const view of views) {
1642 view.viewType === "background" ||
1643 (view.viewType === "background_worker" && !view.unloaded)
1652 async terminateBackgroundScript(addonId) {
1653 // Terminate the background if the extension does have
1654 // a non-persistent background script (event page or background
1656 if (this.hasPersistentBackgroundScript(addonId) === false) {
1657 const policy = WebExtensionPolicy.getByID(addonId);
1658 // When the event page is being terminated through the Devtools
1659 // action, we should terminate it even if there are DevTools
1660 // toolboxes attached to the extension.
1661 return policy.extension.terminateBackground({
1662 ignoreDevToolsAttached: true,
1665 throw Error(`Unable to terminate background script for ${addonId}`);
1669 * Determine whether a devtools toolbox attached to the extension.
1671 * This method is called by the background page idle timeout handler,
1672 * to inhibit terminating the event page when idle while the extension
1673 * developer is debugging the extension through the Addon Debugging window
1674 * (similarly to how service workers are kept alive while the devtools are
1677 * @param {string} id
1678 * The id of the extension.
1680 * @returns {boolean}
1681 * true when a devtools toolbox is attached to an extension with
1682 * the given id, false otherwise.
1684 hasDevToolsAttached(id) {
1685 return this.debugBrowserPromises.has(id);
1689 * Retrieve a XUL browser element which has been configured to be able to connect
1690 * the devtools actor with the process where the extension is running.
1692 * @param {WebExtensionParentActor} webExtensionParentActor
1693 * The devtools actor that is retrieving the browser element.
1695 * @returns {Promise<XULElement>}
1696 * A promise which resolves to the configured browser XUL element.
1698 async getExtensionProcessBrowser(webExtensionParentActor) {
1699 const extensionId = webExtensionParentActor.addonId;
1700 const extension = GlobalManager.getExtension(extensionId);
1702 throw new Error(`Extension not found: ${extensionId}`);
1705 const createBrowser = () => {
1706 if (!this.hiddenXULWindow) {
1707 this.hiddenXULWindow = new HiddenXULWindow();
1708 this.watchExtensionUpdated();
1711 return this.hiddenXULWindow.createBrowserElement({
1712 "webextension-addon-debug-target": extensionId,
1713 remote: extension.remote ? "true" : null,
1714 remoteType: extension.remoteType,
1715 initialBrowsingContextGroupId: extension.browsingContextGroupId,
1719 let browserPromise = this.debugBrowserPromises.get(extensionId);
1721 // Create a new promise if there is no cached one in the map.
1722 if (!browserPromise) {
1723 browserPromise = createBrowser();
1724 this.debugBrowserPromises.set(extensionId, browserPromise);
1725 browserPromise.then(browser => {
1726 browserPromise.browser = browser;
1728 browserPromise.catch(e => {
1730 this.debugBrowserPromises.delete(extensionId);
1734 this.debugActors.get(browserPromise).add(webExtensionParentActor);
1736 return browserPromise;
1739 getFrameLoader(extensionId) {
1740 let promise = this.debugBrowserPromises.get(extensionId);
1741 return promise && promise.browser && promise.browser.frameLoader;
1745 * Given the devtools actor that has retrieved an addon debug browser element,
1746 * it destroys the XUL browser element, and it also destroy the hidden XUL window
1747 * if it is not currently needed.
1749 * @param {WebExtensionParentActor} webExtensionParentActor
1750 * The devtools actor that has retrieved an addon debug browser element.
1752 async releaseExtensionProcessBrowser(webExtensionParentActor) {
1753 const extensionId = webExtensionParentActor.addonId;
1754 const browserPromise = this.debugBrowserPromises.get(extensionId);
1756 if (browserPromise) {
1757 const actorsSet = this.debugActors.get(browserPromise);
1758 actorsSet.delete(webExtensionParentActor);
1759 if (actorsSet.size === 0) {
1760 this.debugActors.delete(browserPromise);
1761 this.debugBrowserPromises.delete(extensionId);
1762 await browserPromise.then(browser => browser.remove());
1766 if (this.debugBrowserPromises.size === 0 && this.hiddenXULWindow) {
1767 this.hiddenXULWindow.shutdown();
1768 this.hiddenXULWindow = null;
1769 this.unwatchExtensionUpdated();
1775 * Returns a Promise which resolves with the message data when the given message
1776 * was received by the message manager. The promise is rejected if the message
1777 * manager was closed before a message was received.
1779 * @param {nsIMessageListenerManager} messageManager
1780 * The message manager on which to listen for messages.
1781 * @param {string} messageName
1782 * The message to listen for.
1783 * @returns {Promise<*>}
1785 function promiseMessageFromChild(messageManager, messageName) {
1786 return new Promise((resolve, reject) => {
1788 function listener(message) {
1790 resolve(message.data);
1792 function observer(subject, topic, data) {
1793 if (subject === messageManager) {
1797 `Message manager was disconnected before receiving ${messageName}`
1802 unregister = () => {
1803 Services.obs.removeObserver(observer, "message-manager-close");
1804 messageManager.removeMessageListener(messageName, listener);
1806 messageManager.addMessageListener(messageName, listener);
1807 Services.obs.addObserver(observer, "message-manager-close");
1811 // This should be called before browser.loadURI is invoked.
1812 async function promiseBackgroundViewLoaded(browser) {
1813 let { childId } = await promiseMessageFromChild(
1814 browser.messageManager,
1815 "Extension:BackgroundViewLoaded"
1818 return ParentAPIManager.getContextById(childId);
1823 * This helper is used to subscribe a listener (e.g. in the ext-devtools API implementation)
1824 * to be called for every ExtensionProxyContext created for an extension page given
1825 * its related extension, viewType and browser element (both the top level context and any context
1826 * created for the extension urls running into its iframe descendants).
1828 * @param {object} params
1829 * @param {object} params.extension
1830 * The Extension on which we are going to listen for the newly created ExtensionProxyContext.
1831 * @param {string} params.viewType
1832 * The viewType of the WebExtension page that we are watching (e.g. "background" or
1834 * @param {XULElement} params.browser
1835 * The browser element of the WebExtension page that we are watching.
1836 * @param {Function} onExtensionProxyContextLoaded
1837 * The callback that is called when a new context has been loaded (as `callback(context)`);
1839 * @returns {Function}
1840 * Unsubscribe the listener.
1842 function watchExtensionProxyContextLoad(
1843 { extension, viewType, browser },
1844 onExtensionProxyContextLoaded
1846 if (typeof onExtensionProxyContextLoaded !== "function") {
1847 throw new Error("Missing onExtensionProxyContextLoaded handler");
1850 const listener = (event, context) => {
1851 if (context.viewType == viewType && context.xulBrowser == browser) {
1852 onExtensionProxyContextLoaded(context);
1856 extension.on("extension-proxy-context-load", listener);
1859 extension.off("extension-proxy-context-load", listener);
1864 * This helper is used to subscribe a listener (e.g. in the ext-backgroundPage)
1865 * to be called for every ExtensionProxyContext created for an extension
1866 * background service worker given its related extension.
1868 * @param {object} params
1869 * @param {object} params.extension
1870 * The Extension on which we are going to listen for the newly created ExtensionProxyContext.
1871 * @param {Function} onExtensionWorkerContextLoaded
1872 * The callback that is called when the worker script has been fully loaded (as `callback(context)`);
1874 * @returns {Function}
1875 * Unsubscribe the listener.
1877 function watchExtensionWorkerContextLoaded(
1879 onExtensionWorkerContextLoaded
1881 if (typeof onExtensionWorkerContextLoaded !== "function") {
1882 throw new Error("Missing onExtensionWorkerContextLoaded handler");
1885 const listener = (event, context) => {
1886 if (context.viewType == "background_worker") {
1887 onExtensionWorkerContextLoaded(context);
1891 extension.on("extension-proxy-context-load:completed", listener);
1894 extension.off("extension-proxy-context-load:completed", listener);
1898 // Manages icon details for toolbar buttons in the |pageAction| and
1899 // |browserAction| APIs.
1901 DEFAULT_ICON: "chrome://mozapps/skin/extensions/extensionGeneric.svg",
1903 // WeakMap<Extension -> Map<url-string -> Map<iconType-string -> object>>>
1904 iconCache: new DefaultWeakMap(() => {
1905 return new DefaultMap(() => new DefaultMap(() => new Map()));
1908 // Normalizes the various acceptable input formats into an object
1909 // with icon size as key and icon URL as value.
1911 // If a context is specified (function is called from an extension):
1912 // Throws an error if an invalid icon size was provided or the
1913 // extension is not allowed to load the specified resources.
1915 // If no context is specified, instead of throwing an error, this
1916 // function simply logs a warning message.
1917 normalize(details, extension, context = null) {
1918 if (!details.imageData && details.path != null) {
1919 // Pick a cache key for the icon paths. If the path is a string,
1920 // use it directly. Otherwise, stringify the path object.
1921 let key = details.path;
1922 if (typeof key !== "string") {
1926 let icons = this.iconCache
1928 .get(context && context.uri.spec)
1929 .get(details.iconType);
1931 let icon = icons.get(key);
1933 icon = this._normalize(details, extension, context);
1934 icons.set(key, icon);
1939 return this._normalize(details, extension, context);
1942 _normalize(details, extension, context = null) {
1946 let { imageData, path, themeIcons } = details;
1949 if (typeof imageData == "string") {
1950 imageData = { 19: imageData };
1953 for (let size of Object.keys(imageData)) {
1954 result[size] = imageData[size];
1958 let baseURI = context ? context.uri : extension.baseURI;
1961 if (typeof path != "object") {
1962 path = { 19: path };
1965 for (let size of Object.keys(path)) {
1966 let url = path[size];
1968 url = baseURI.resolve(path[size]);
1970 // The Chrome documentation specifies these parameters as
1971 // relative paths. We currently accept absolute URLs as well,
1972 // which means we need to check that the extension is allowed
1973 // to load them. This will throw an error if it's not allowed.
1974 this._checkURL(url, extension);
1976 result[size] = url || this.DEFAULT_ICON;
1981 themeIcons.forEach(({ size, light, dark }) => {
1982 let lightURL = baseURI.resolve(light);
1983 let darkURL = baseURI.resolve(dark);
1985 this._checkURL(lightURL, extension);
1986 this._checkURL(darkURL, extension);
1988 let defaultURL = result[size] || result[19]; // always fallback to default first
1990 default: defaultURL || darkURL, // Fallback to the dark url if no default is specified.
1997 // Function is called from extension code, delegate error.
2001 // If there's no context, it's because we're handling this
2002 // as a manifest directive. Log a warning rather than
2003 // raising an error.
2004 extension.manifestError(`Invalid icon data: ${e}`);
2010 // Checks if the extension is allowed to load the given URL with the specified principal.
2011 // This will throw an error if the URL is not allowed.
2012 _checkURL(url, extension) {
2013 if (!extension.checkLoadURL(url, { allowInheritsPrincipal: true })) {
2014 throw new ExtensionError(`Illegal URL ${url}`);
2018 // Returns the appropriate icon URL for the given icons object and the
2019 // screen resolution of the given window.
2020 getPreferredIcon(icons, extension = null, size = 16) {
2021 const DEFAULT = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
2023 let bestSize = null;
2026 } else if (icons[2 * size]) {
2027 bestSize = 2 * size;
2029 let sizes = Object.keys(icons)
2030 .map(key => parseInt(key, 10))
2031 .sort((a, b) => a - b);
2033 bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
2037 return { size: bestSize, icon: icons[bestSize] || DEFAULT };
2040 return { size, icon: DEFAULT };
2043 // These URLs should already be properly escaped, but make doubly sure CSS
2044 // string escape characters are escaped here, since they could lead to a
2047 return url.replace(/[\\\s"]/g, encodeURIComponent);
2052 constructor(storeName) {
2053 this.storeName = storeName;
2056 async getStore(path = null) {
2057 let data = await StartupCache.dataPromise;
2059 let store = data.get(this.storeName);
2062 data.set(this.storeName, store);
2066 if (Array.isArray(path)) {
2067 for (let elem of path.slice(0, -1)) {
2068 let next = store.get(elem);
2071 store.set(elem, next);
2075 key = path[path.length - 1];
2078 return [store, key];
2081 async get(path, createFunc) {
2082 let [store, key] = await this.getStore(path);
2084 let result = store.get(key);
2086 if (result === undefined) {
2087 result = await createFunc(path);
2088 store.set(key, result);
2089 StartupCache.save();
2095 async set(path, value) {
2096 let [store, key] = await this.getStore(path);
2098 store.set(key, value);
2099 StartupCache.save();
2103 let [store] = await this.getStore();
2105 return new Map(store);
2108 async delete(path) {
2109 let [store, key] = await this.getStore(path);
2111 if (store.delete(key)) {
2112 StartupCache.save();
2117 // A cache to support faster initialization of extensions at browser startup.
2118 // All cached data is removed when the browser is updated.
2119 // Extension-specific data is removed when the add-on is updated.
2120 var StartupCache = {
2121 _ensureDirectoryPromise: null,
2124 _ensureDirectory() {
2125 if (this._ensureDirectoryPromise === null) {
2126 this._ensureDirectoryPromise = IOUtils.makeDirectory(
2127 PathUtils.parent(this.file),
2129 ignoreExisting: true,
2130 createAncestors: true,
2135 return this._ensureDirectoryPromise;
2138 // When the application version changes, this file is removed by
2139 // RemoveComponentRegistries in nsAppRunner.cpp.
2140 file: PathUtils.join(
2141 Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
2147 let data = new Uint8Array(lazy.aomStartup.encodeBlob(this._data));
2148 await this._ensureDirectoryPromise;
2149 await IOUtils.write(this.file, data, { tmpPath: `${this.file}.tmp` });
2151 Glean.extensions.startupCacheWriteBytelength.set(data.byteLength);
2155 this._ensureDirectory();
2157 if (!this._saveTask) {
2158 this._saveTask = new lazy.DeferredTask(() => this._saveNow(), 5000);
2160 IOUtils.profileBeforeChange.addBlocker(
2161 "Flush WebExtension StartupCache",
2163 await this._saveTask.finalize();
2164 this._saveTask = null;
2169 return this._saveTask.arm();
2174 let result = new Map();
2176 Glean.extensions.startupCacheLoadTime.start();
2177 let { buffer } = await IOUtils.read(this.file);
2179 result = lazy.aomStartup.decodeBlob(buffer);
2180 Glean.extensions.startupCacheLoadTime.stop();
2182 Glean.extensions.startupCacheLoadTime.cancel();
2183 if (!DOMException.isInstance(e) || e.name !== "NotFoundError") {
2186 let error = lazy.getErrorNameForTelemetry(e);
2187 Glean.extensions.startupCacheReadErrors[error].add(1);
2190 this._data = result;
2195 if (!this._dataPromise) {
2196 this._dataPromise = this._readData();
2198 return this._dataPromise;
2201 clearAddonData(id) {
2202 return Promise.all([
2203 this.general.delete(id),
2204 this.locales.delete(id),
2205 this.manifests.delete(id),
2206 this.permissions.delete(id),
2207 this.menus.delete(id),
2209 // Ignore the error. It happens when we try to flush the add-on
2210 // data after the AddonManager has flushed the entire startup cache.
2214 observe(subject, topic, data) {
2215 if (topic === "startupcache-invalidate") {
2216 this._data = new Map();
2217 this._dataPromise = Promise.resolve(this._data);
2221 get(extension, path, createFunc) {
2222 return this.general.get(
2223 [extension.id, extension.version, ...path],
2228 delete(extension, path) {
2229 return this.general.delete([extension.id, extension.version, ...path]);
2232 general: new CacheStore("general"),
2233 locales: new CacheStore("locales"),
2234 manifests: new CacheStore("manifests"),
2235 other: new CacheStore("other"),
2236 permissions: new CacheStore("permissions"),
2237 schemas: new CacheStore("schemas"),
2238 menus: new CacheStore("menus"),
2241 Services.obs.addObserver(StartupCache, "startupcache-invalidate");
2243 export var ExtensionParent = {
2245 HiddenExtensionPage,
2251 promiseBackgroundViewLoaded,
2252 watchExtensionProxyContextLoad,
2253 watchExtensionWorkerContextLoaded,
2257 // browserPaintedPromise and browserStartupPromise are promises that
2258 // resolve after the first browser window is painted and after browser
2259 // windows have been restored, respectively. Alternatively,
2260 // browserStartupPromise also resolves from the extensions-late-startup
2261 // notification sent by Firefox Reality on desktop platforms, because it
2262 // doesn't support SessionStore.
2263 // _resetStartupPromises should only be called from outside this file in tests.
2264 ExtensionParent._resetStartupPromises = () => {
2265 ExtensionParent.browserPaintedPromise = promiseObserved(
2266 "browser-delayed-startup-finished"
2268 ExtensionParent.browserStartupPromise = Promise.race([
2269 promiseObserved("sessionstore-windows-restored"),
2270 promiseObserved("extensions-late-startup"),
2273 ExtensionParent._resetStartupPromises();
2275 ChromeUtils.defineLazyGetter(ExtensionParent, "PlatformInfo", () => {
2276 return Object.freeze({
2278 let os = AppConstants.platform;
2279 if (os == "macosx") {
2284 arch: (function () {
2285 let abi = Services.appinfo.XPCOMABI;
2286 let [arch] = abi.split("-");
2287 if (arch == "x86") {
2289 } else if (arch == "x86_64") {
2298 * Retreives the browser_style stylesheets needed for extension popups and sidebars.
2300 * @returns {Array<string>} an array of stylesheets needed for the current platform.
2302 ChromeUtils.defineLazyGetter(ExtensionParent, "extensionStylesheets", () => {
2303 let stylesheets = ["chrome://browser/content/extension.css"];
2305 if (AppConstants.platform === "macosx") {
2306 stylesheets.push("chrome://browser/content/extension-mac.css");