forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			2310 lines
		
	
	
	
		
			69 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2310 lines
		
	
	
	
		
			69 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | |
| /* vim: set sts=2 sw=2 et tw=80: */
 | |
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| /**
 | |
|  * This module contains code for managing APIs that need to run in the
 | |
|  * parent process, and handles the parent side of operations that need
 | |
|  * to be proxied from ExtensionChild.jsm.
 | |
|  */
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| 
 | |
| import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
 | |
|   AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
 | |
|   BroadcastConduit: "resource://gre/modules/ConduitsParent.sys.mjs",
 | |
|   DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
 | |
|   DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
 | |
|   ExtensionActivityLog: "resource://gre/modules/ExtensionActivityLog.sys.mjs",
 | |
|   ExtensionData: "resource://gre/modules/Extension.sys.mjs",
 | |
|   GeckoViewConnection: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
 | |
|   MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.sys.mjs",
 | |
|   NativeApp: "resource://gre/modules/NativeMessaging.sys.mjs",
 | |
|   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | |
|   Schemas: "resource://gre/modules/Schemas.sys.mjs",
 | |
|   getErrorNameForTelemetry: "resource://gre/modules/ExtensionTelemetry.sys.mjs",
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyServiceGetters(lazy, {
 | |
|   aomStartup: [
 | |
|     "@mozilla.org/addons/addon-manager-startup;1",
 | |
|     "amIAddonManagerStartup",
 | |
|   ],
 | |
| });
 | |
| 
 | |
| import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
 | |
| import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
 | |
| 
 | |
| const DUMMY_PAGE_URI = Services.io.newURI(
 | |
|   "chrome://extensions/content/dummy.xhtml"
 | |
| );
 | |
| 
 | |
| var { BaseContext, CanOfAPIs, SchemaAPIManager, SpreadArgs, redefineGetter } =
 | |
|   ExtensionCommon;
 | |
| 
 | |
| var {
 | |
|   DefaultMap,
 | |
|   DefaultWeakMap,
 | |
|   ExtensionError,
 | |
|   promiseDocumentLoaded,
 | |
|   promiseEvent,
 | |
|   promiseObserved,
 | |
| } = ExtensionUtils;
 | |
| 
 | |
| const ERROR_NO_RECEIVERS =
 | |
|   "Could not establish connection. Receiving end does not exist.";
 | |
| 
 | |
| const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
 | |
| const CATEGORY_EXTENSION_MODULES = "webextension-modules";
 | |
| const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
 | |
| const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
 | |
| 
 | |
| let schemaURLs = new Set();
 | |
| 
 | |
| schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
 | |
| 
 | |
| let GlobalManager;
 | |
| let ParentAPIManager;
 | |
| 
 | |
| function verifyActorForContext(actor, context) {
 | |
|   if (JSWindowActorParent.isInstance(actor)) {
 | |
|     let target = actor.browsingContext.top.embedderElement;
 | |
|     if (context.parentMessageManager !== target.messageManager) {
 | |
|       throw new Error("Got message on unexpected message manager");
 | |
|     }
 | |
|   } else if (JSProcessActorParent.isInstance(actor)) {
 | |
|     if (actor.manager.remoteType !== context.extension.remoteType) {
 | |
|       throw new Error("Got message from unexpected process");
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| // This object loads the ext-*.js scripts that define the extension API.
 | |
| let apiManager = new (class extends SchemaAPIManager {
 | |
|   constructor() {
 | |
|     super("main", lazy.Schemas);
 | |
|     this.initialized = null;
 | |
| 
 | |
|     /* eslint-disable mozilla/balanced-listeners */
 | |
|     this.on("startup", (e, extension) => {
 | |
|       return extension.apiManager.onStartup(extension);
 | |
|     });
 | |
| 
 | |
|     this.on("update", async (e, { id, resourceURI, isPrivileged }) => {
 | |
|       let modules = this.eventModules.get("update");
 | |
|       if (modules.size == 0) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       let extension = new lazy.ExtensionData(resourceURI, isPrivileged);
 | |
|       await extension.loadManifest();
 | |
| 
 | |
|       return Promise.all(
 | |
|         Array.from(modules).map(async apiName => {
 | |
|           let module = await this.asyncLoadModule(apiName);
 | |
|           module.onUpdate(id, extension.manifest);
 | |
|         })
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     this.on("uninstall", (e, { id }) => {
 | |
|       let modules = this.eventModules.get("uninstall");
 | |
|       return Promise.all(
 | |
|         Array.from(modules).map(async apiName => {
 | |
|           let module = await this.asyncLoadModule(apiName);
 | |
|           return module.onUninstall(id);
 | |
|         })
 | |
|       );
 | |
|     });
 | |
|     /* eslint-enable mozilla/balanced-listeners */
 | |
| 
 | |
|     // Handle any changes that happened during startup
 | |
|     let disabledIds = lazy.AddonManager.getStartupChanges(
 | |
|       lazy.AddonManager.STARTUP_CHANGE_DISABLED
 | |
|     );
 | |
|     if (disabledIds.length) {
 | |
|       this._callHandlers(disabledIds, "disable", "onDisable");
 | |
|     }
 | |
| 
 | |
|     let uninstalledIds = lazy.AddonManager.getStartupChanges(
 | |
|       lazy.AddonManager.STARTUP_CHANGE_UNINSTALLED
 | |
|     );
 | |
|     if (uninstalledIds.length) {
 | |
|       this._callHandlers(uninstalledIds, "uninstall", "onUninstall");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   getModuleJSONURLs() {
 | |
|     return Array.from(
 | |
|       Services.catMan.enumerateCategory(CATEGORY_EXTENSION_MODULES),
 | |
|       ({ value }) => value
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   // Loads all the ext-*.js scripts currently registered.
 | |
|   lazyInit() {
 | |
|     if (this.initialized) {
 | |
|       return this.initialized;
 | |
|     }
 | |
| 
 | |
|     let modulesPromise = StartupCache.other.get(["parentModules"], () =>
 | |
|       this.loadModuleJSON(this.getModuleJSONURLs())
 | |
|     );
 | |
| 
 | |
|     let scriptURLs = [];
 | |
|     for (let { value } of Services.catMan.enumerateCategory(
 | |
|       CATEGORY_EXTENSION_SCRIPTS
 | |
|     )) {
 | |
|       scriptURLs.push(value);
 | |
|     }
 | |
| 
 | |
|     let promise = (async () => {
 | |
|       let scripts = await Promise.all(
 | |
|         scriptURLs.map(url => ChromeUtils.compileScript(url))
 | |
|       );
 | |
| 
 | |
|       this.initModuleData(await modulesPromise);
 | |
| 
 | |
|       this.initGlobal();
 | |
|       for (let script of scripts) {
 | |
|         script.executeInGlobal(this.global);
 | |
|       }
 | |
| 
 | |
|       // Load order matters here. The base manifest defines types which are
 | |
|       // extended by other schemas, so needs to be loaded first.
 | |
|       return lazy.Schemas.load(BASE_SCHEMA).then(() => {
 | |
|         let promises = [];
 | |
|         for (let { value } of Services.catMan.enumerateCategory(
 | |
|           CATEGORY_EXTENSION_SCHEMAS
 | |
|         )) {
 | |
|           promises.push(lazy.Schemas.load(value));
 | |
|         }
 | |
|         for (let [url, { content }] of this.schemaURLs) {
 | |
|           promises.push(lazy.Schemas.load(url, content));
 | |
|         }
 | |
|         for (let url of schemaURLs) {
 | |
|           promises.push(lazy.Schemas.load(url));
 | |
|         }
 | |
|         return Promise.all(promises).then(() => {
 | |
|           lazy.Schemas.updateSharedSchemas();
 | |
|         });
 | |
|       });
 | |
|     })();
 | |
| 
 | |
|     Services.mm.addMessageListener("Extension:GetFrameData", this);
 | |
| 
 | |
|     this.initialized = promise;
 | |
|     return this.initialized;
 | |
|   }
 | |
| 
 | |
|   receiveMessage({ target }) {
 | |
|     let data = GlobalManager.frameData.get(target) || {};
 | |
|     Object.assign(data, this.global.tabTracker.getBrowserData(target));
 | |
|     return data;
 | |
|   }
 | |
| 
 | |
|   // Call static handlers for the given event on the given extension ids,
 | |
|   // and set up a shutdown blocker to ensure they all complete.
 | |
|   _callHandlers(ids, event, method) {
 | |
|     let promises = Array.from(this.eventModules.get(event))
 | |
|       .map(async modName => {
 | |
|         let module = await this.asyncLoadModule(modName);
 | |
|         return ids.map(id => module[method](id));
 | |
|       })
 | |
|       .flat();
 | |
|     if (event === "disable") {
 | |
|       promises.push(...ids.map(id => this.emit("disable", id)));
 | |
|     }
 | |
|     if (event === "enabling") {
 | |
|       promises.push(...ids.map(id => this.emit("enabling", id)));
 | |
|     }
 | |
| 
 | |
|     lazy.AsyncShutdown.profileBeforeChange.addBlocker(
 | |
|       `Extension API ${event} handlers for ${ids.join(",")}`,
 | |
|       Promise.all(promises)
 | |
|     );
 | |
|   }
 | |
| })();
 | |
| 
 | |
| /**
 | |
|  * @typedef {object} ParentPort
 | |
|  * @property {boolean} [native]
 | |
|  * @property {string} [senderChildId]
 | |
|  * @property {function(StructuredCloneHolder): any} onPortMessage
 | |
|  * @property {Function} onPortDisconnect
 | |
|  */
 | |
| 
 | |
| // Receives messages related to the extension messaging API and forwards them
 | |
| // to relevant child messengers.  Also handles Native messaging and GeckoView.
 | |
| /** @typedef {typeof ProxyMessenger} NativeMessenger */
 | |
| const ProxyMessenger = {
 | |
|   /** @type {Map<number, Partial<ParentPort>&Promise<ParentPort>>} */
 | |
|   ports: new Map(),
 | |
| 
 | |
|   init() {
 | |
|     this.conduit = new lazy.BroadcastConduit(ProxyMessenger, {
 | |
|       id: "ProxyMessenger",
 | |
|       reportOnClosed: "portId",
 | |
|       recv: ["PortConnect", "PortMessage", "NativeMessage", "RuntimeMessage"],
 | |
|       cast: ["PortConnect", "PortMessage", "PortDisconnect", "RuntimeMessage"],
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   openNative(nativeApp, sender) {
 | |
|     let context = ParentAPIManager.getContextById(sender.childId);
 | |
|     if (context.extension.hasPermission("geckoViewAddons")) {
 | |
|       return new lazy.GeckoViewConnection(
 | |
|         this.getSender(context.extension, sender),
 | |
|         sender.actor.browsingContext.top.embedderElement,
 | |
|         nativeApp,
 | |
|         context.extension.hasPermission("nativeMessagingFromContent")
 | |
|       );
 | |
|     } else if (sender.verified) {
 | |
|       return new lazy.NativeApp(context, nativeApp);
 | |
|     }
 | |
|     sender = this.getSender(context.extension, sender);
 | |
|     throw new Error(`Native messaging not allowed: ${JSON.stringify(sender)}`);
 | |
|   },
 | |
| 
 | |
|   recvNativeMessage({ nativeApp, holder }, { sender }) {
 | |
|     const app = this.openNative(nativeApp, sender);
 | |
| 
 | |
|     // Track in-flight NativeApp sendMessage requests as
 | |
|     // a NativeApp port destroyed when the request
 | |
|     // has been handled.
 | |
|     const promiseSendMessage = app.sendMessage(holder);
 | |
|     const sendMessagePort = {
 | |
|       native: true,
 | |
|       senderChildId: sender.childId,
 | |
|     };
 | |
|     this.trackNativeAppPort(sendMessagePort);
 | |
|     const untrackSendMessage = () => this.untrackNativeAppPort(sendMessagePort);
 | |
|     promiseSendMessage.then(untrackSendMessage, untrackSendMessage);
 | |
| 
 | |
|     return promiseSendMessage;
 | |
|   },
 | |
| 
 | |
|   getSender(extension, source) {
 | |
|     let sender = {
 | |
|       contextId: source.id,
 | |
|       id: source.extensionId,
 | |
|       envType: source.envType,
 | |
|       url: source.url,
 | |
|     };
 | |
| 
 | |
|     if (JSWindowActorParent.isInstance(source.actor)) {
 | |
|       let browser = source.actor.browsingContext.top.embedderElement;
 | |
|       let data =
 | |
|         browser && apiManager.global.tabTracker.getBrowserData(browser);
 | |
|       if (data?.tabId > 0) {
 | |
|         sender.tab = extension.tabManager.get(data.tabId, null)?.convert();
 | |
|         // frameId is documented to only be set if sender.tab is set.
 | |
|         sender.frameId = source.frameId;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return sender;
 | |
|   },
 | |
| 
 | |
|   getTopBrowsingContextId(tabId) {
 | |
|     // If a tab alredy has content scripts, no need to check private browsing.
 | |
|     let tab = apiManager.global.tabTracker.getTab(tabId, null);
 | |
|     if (!tab || (tab.browser || tab).getAttribute("pending") === "true") {
 | |
|       // No receivers in discarded tabs, so bail early to keep the browser lazy.
 | |
|       throw new ExtensionError(ERROR_NO_RECEIVERS);
 | |
|     }
 | |
|     let browser = tab.linkedBrowser || tab.browser;
 | |
|     return browser.browsingContext.id;
 | |
|   },
 | |
| 
 | |
|   // TODO: Rework/simplify this and getSender/getTopBC after bug 1580766.
 | |
|   async normalizeArgs(arg, sender) {
 | |
|     arg.extensionId = arg.extensionId || sender.extensionId;
 | |
|     let extension = GlobalManager.extensionMap.get(arg.extensionId);
 | |
|     if (!extension) {
 | |
|       return Promise.reject({ message: ERROR_NO_RECEIVERS });
 | |
|     }
 | |
|     // TODO bug 1852317: This should not be unconditional.
 | |
|     await extension.wakeupBackground?.();
 | |
| 
 | |
|     arg.sender = this.getSender(extension, sender);
 | |
|     arg.topBC = arg.tabId && this.getTopBrowsingContextId(arg.tabId);
 | |
|     return arg.tabId ? "tab" : "messenger";
 | |
|   },
 | |
| 
 | |
|   async recvRuntimeMessage(arg, { sender }) {
 | |
|     arg.firstResponse = true;
 | |
|     let kind = await this.normalizeArgs(arg, sender);
 | |
|     let result = await this.conduit.castRuntimeMessage(kind, arg);
 | |
|     if (!result) {
 | |
|       // "throw new ExtensionError" cannot be used because then the stack of the
 | |
|       // sendMessage call would not be added to the error object generated by
 | |
|       // context.normalizeError. Test coverage by test_ext_error_location.js.
 | |
|       return Promise.reject({ message: ERROR_NO_RECEIVERS });
 | |
|     }
 | |
|     return result.value;
 | |
|   },
 | |
| 
 | |
|   async recvPortConnect(arg, { sender }) {
 | |
|     if (arg.native) {
 | |
|       let port = this.openNative(arg.name, sender).onConnect(arg.portId, this);
 | |
|       port.senderChildId = sender.childId;
 | |
|       port.native = true;
 | |
|       this.ports.set(arg.portId, port);
 | |
|       this.trackNativeAppPort(port);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // PortMessages that follow will need to wait for the port to be opened.
 | |
|     /** @type {callback} */
 | |
|     let resolvePort;
 | |
|     this.ports.set(arg.portId, new Promise(res => (resolvePort = res)));
 | |
| 
 | |
|     let kind = await this.normalizeArgs(arg, sender);
 | |
|     let all = await this.conduit.castPortConnect(kind, arg);
 | |
|     resolvePort();
 | |
| 
 | |
|     // If there are no active onConnect listeners.
 | |
|     if (!all.some(x => x.value)) {
 | |
|       throw new ExtensionError(ERROR_NO_RECEIVERS);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   async recvPortMessage({ holder }, { sender }) {
 | |
|     if (sender.native) {
 | |
|       // If the nativeApp port connect fails (e.g. if triggered by a content
 | |
|       // script), the portId may not be in the map (because it did throw in
 | |
|       // the openNative method).
 | |
|       return this.ports.get(sender.portId)?.onPortMessage(holder);
 | |
|     }
 | |
|     // NOTE: the following await make sure we await for promised ports
 | |
|     // (ports that were not yet open when added to the Map,
 | |
|     // see recvPortConnect).
 | |
|     await this.ports.get(sender.portId);
 | |
|     this.sendPortMessage(sender.portId, holder, !sender.source);
 | |
|   },
 | |
| 
 | |
|   recvConduitClosed(sender) {
 | |
|     let app = this.ports.get(sender.portId);
 | |
|     if (this.ports.delete(sender.portId) && sender.native) {
 | |
|       this.untrackNativeAppPort(app);
 | |
|       return app.onPortDisconnect();
 | |
|     }
 | |
|     this.sendPortDisconnect(sender.portId, null, !sender.source);
 | |
|   },
 | |
| 
 | |
|   sendPortMessage(portId, holder, source = true) {
 | |
|     this.conduit.castPortMessage("port", { portId, source, holder });
 | |
|   },
 | |
| 
 | |
|   sendPortDisconnect(portId, error, source = true) {
 | |
|     let port = this.ports.get(portId);
 | |
|     this.untrackNativeAppPort(port);
 | |
|     this.conduit.castPortDisconnect("port", { portId, source, error });
 | |
|     this.ports.delete(portId);
 | |
|   },
 | |
| 
 | |
|   trackNativeAppPort(port) {
 | |
|     if (!port?.native) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       let context = ParentAPIManager.getContextById(port.senderChildId);
 | |
|       context?.trackNativeAppPort(port);
 | |
|     } catch {
 | |
|       // getContextById will throw if the context has been destroyed
 | |
|       // in the meantime.
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   untrackNativeAppPort(port) {
 | |
|     if (!port?.native) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       let context = ParentAPIManager.getContextById(port.senderChildId);
 | |
|       context?.untrackNativeAppPort(port);
 | |
|     } catch {
 | |
|       // getContextById will throw if the context has been destroyed
 | |
|       // in the meantime.
 | |
|     }
 | |
|   },
 | |
| };
 | |
| ProxyMessenger.init();
 | |
| 
 | |
| // Responsible for loading extension APIs into the right globals.
 | |
| GlobalManager = {
 | |
|   // Map[extension ID -> Extension]. Determines which extension is
 | |
|   // responsible for content under a particular extension ID.
 | |
|   extensionMap: new Map(),
 | |
|   initialized: false,
 | |
| 
 | |
|   /** @type {WeakMap<Browser, object>} Extension Context init data. */
 | |
|   frameData: new WeakMap(),
 | |
| 
 | |
|   init(extension) {
 | |
|     if (this.extensionMap.size == 0) {
 | |
|       apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
 | |
|       this.initialized = true;
 | |
|     }
 | |
|     this.extensionMap.set(extension.id, extension);
 | |
|   },
 | |
| 
 | |
|   uninit(extension) {
 | |
|     this.extensionMap.delete(extension.id);
 | |
| 
 | |
|     if (this.extensionMap.size == 0 && this.initialized) {
 | |
|       apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
 | |
|       this.initialized = false;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _onExtensionBrowser(type, browser, data = {}) {
 | |
|     data.viewType = browser.getAttribute("webextension-view-type");
 | |
|     if (data.viewType) {
 | |
|       GlobalManager.frameData.set(browser, data);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   getExtension(extensionId) {
 | |
|     return this.extensionMap.get(extensionId);
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * The proxied parent side of a context in ExtensionChild.jsm, for the
 | |
|  * parent side of a proxied API.
 | |
|  */
 | |
| class ProxyContextParent extends BaseContext {
 | |
|   constructor(envType, extension, params, browsingContext, principal) {
 | |
|     super(envType, extension);
 | |
| 
 | |
|     this.childId = params.childId;
 | |
|     this.uri = Services.io.newURI(params.url);
 | |
| 
 | |
|     this.incognito = params.incognito;
 | |
| 
 | |
|     this.listenerPromises = new Set();
 | |
| 
 | |
|     // browsingContext is null when subclassed by BackgroundWorkerContextParent.
 | |
|     const xulBrowser = browsingContext?.top.embedderElement;
 | |
|     // This message manager is used by ParentAPIManager to send messages and to
 | |
|     // close the ProxyContext if the underlying message manager closes. This
 | |
|     // message manager object may change when `xulBrowser` swaps docshells, e.g.
 | |
|     // when a tab is moved to a different window.
 | |
|     // TODO: Is xulBrowser correct for ContentScriptContextParent? Messages
 | |
|     // through the xulBrowser won't reach cross-process iframes.
 | |
|     this.messageManagerProxy =
 | |
|       xulBrowser && new lazy.MessageManagerProxy(xulBrowser);
 | |
| 
 | |
|     Object.defineProperty(this, "principal", {
 | |
|       value: principal,
 | |
|       enumerable: true,
 | |
|       configurable: true,
 | |
|     });
 | |
| 
 | |
|     this.listenerProxies = new Map();
 | |
| 
 | |
|     this.pendingEventBrowser = null;
 | |
|     this.callContextData = null;
 | |
| 
 | |
|     // Set of active NativeApp ports.
 | |
|     this.activeNativePorts = new WeakSet();
 | |
| 
 | |
|     // Set of pending queryRunListener promises.
 | |
|     this.runListenerPromises = new Set();
 | |
| 
 | |
|     apiManager.emit("proxy-context-load", this);
 | |
|   }
 | |
| 
 | |
|   get isProxyContextParent() {
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   trackRunListenerPromise(runListenerPromise) {
 | |
|     if (
 | |
|       // The extension was already shutdown.
 | |
|       !this.extension ||
 | |
|       // Not a non persistent background script context.
 | |
|       !this.isBackgroundContext ||
 | |
|       this.extension.persistentBackground
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
|     const clearFromSet = () =>
 | |
|       this.runListenerPromises.delete(runListenerPromise);
 | |
|     runListenerPromise.then(clearFromSet, clearFromSet);
 | |
|     this.runListenerPromises.add(runListenerPromise);
 | |
|   }
 | |
| 
 | |
|   clearPendingRunListenerPromises() {
 | |
|     this.runListenerPromises.clear();
 | |
|   }
 | |
| 
 | |
|   get pendingRunListenerPromisesCount() {
 | |
|     return this.runListenerPromises.size;
 | |
|   }
 | |
| 
 | |
|   trackNativeAppPort(port) {
 | |
|     if (
 | |
|       // Not a native port.
 | |
|       !port?.native ||
 | |
|       // Not a non persistent background script context.
 | |
|       !this.isBackgroundContext ||
 | |
|       this.extension?.persistentBackground ||
 | |
|       // The extension was already shutdown.
 | |
|       !this.extension
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
|     this.activeNativePorts.add(port);
 | |
|   }
 | |
| 
 | |
|   untrackNativeAppPort(port) {
 | |
|     this.activeNativePorts.delete(port);
 | |
|   }
 | |
| 
 | |
|   get hasActiveNativeAppPorts() {
 | |
|     return !!ChromeUtils.nondeterministicGetWeakSetKeys(this.activeNativePorts)
 | |
|       .length;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Call the `callable` parameter with `context.callContextData` set to the value passed
 | |
|    * as the first parameter of this method.
 | |
|    *
 | |
|    * `context.callContextData` is expected to:
 | |
|    * - don't be set when context.withCallContextData is being called
 | |
|    * - be set back to null right after calling the `callable` function, without
 | |
|    *   awaiting on any async code that the function may be running internally
 | |
|    *
 | |
|    * The callable method itself is responsabile of eventually retrieve the value initially set
 | |
|    * on the `context.callContextData` before any code executed asynchronously (e.g. from a
 | |
|    * callback or after awaiting internally on a promise if the `callable` function was async).
 | |
|    *
 | |
|    * @param {object} callContextData
 | |
|    * @param {boolean} callContextData.isHandlingUserInput
 | |
|    * @param {Function} callable
 | |
|    *
 | |
|    * @returns {any} Returns the value returned by calling the `callable` method.
 | |
|    */
 | |
|   withCallContextData({ isHandlingUserInput }, callable) {
 | |
|     if (this.callContextData) {
 | |
|       Cu.reportError(
 | |
|         `Unexpected pre-existing callContextData on "${this.extension?.policy.debugName}" contextId ${this.contextId}`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       this.callContextData = {
 | |
|         isHandlingUserInput,
 | |
|       };
 | |
|       return callable();
 | |
|     } finally {
 | |
|       this.callContextData = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async withPendingBrowser(browser, callable) {
 | |
|     let savedBrowser = this.pendingEventBrowser;
 | |
|     this.pendingEventBrowser = browser;
 | |
|     try {
 | |
|       let result = await callable();
 | |
|       return result;
 | |
|     } finally {
 | |
|       this.pendingEventBrowser = savedBrowser;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   logActivity(type, name, data) {
 | |
|     // The base class will throw so we catch any subclasses that do not implement.
 | |
|     // We do not want to throw here, but we also do not log here.
 | |
|   }
 | |
| 
 | |
|   get cloneScope() {
 | |
|     return this.sandbox;
 | |
|   }
 | |
| 
 | |
|   applySafe(callback, args) {
 | |
|     // There's no need to clone when calling listeners for a proxied
 | |
|     // context.
 | |
|     return this.applySafeWithoutClone(callback, args);
 | |
|   }
 | |
| 
 | |
|   get xulBrowser() {
 | |
|     return this.messageManagerProxy?.eventTarget;
 | |
|   }
 | |
| 
 | |
|   get parentMessageManager() {
 | |
|     // TODO bug 1595186: Replace use of parentMessageManager.
 | |
|     return this.messageManagerProxy?.messageManager;
 | |
|   }
 | |
| 
 | |
|   shutdown() {
 | |
|     this.unload();
 | |
|   }
 | |
| 
 | |
|   unload() {
 | |
|     if (this.unloaded) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.messageManagerProxy?.dispose();
 | |
| 
 | |
|     super.unload();
 | |
|     apiManager.emit("proxy-context-unload", this);
 | |
|   }
 | |
| 
 | |
|   get apiCan() {
 | |
|     const apiCan = new CanOfAPIs(this, this.extension.apiManager, {});
 | |
|     return redefineGetter(this, "apiCan", apiCan);
 | |
|   }
 | |
| 
 | |
|   get apiObj() {
 | |
|     return redefineGetter(this, "apiObj", this.apiCan.root);
 | |
|   }
 | |
| 
 | |
|   get sandbox() {
 | |
|     // Note: Blob and URL globals are used in ext-contentScripts.js.
 | |
|     const sandbox = Cu.Sandbox(this.principal, {
 | |
|       sandboxName: this.uri.spec,
 | |
|       wantGlobalProperties: ["Blob", "URL"],
 | |
|     });
 | |
|     return redefineGetter(this, "sandbox", sandbox);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * The parent side of proxied API context for extension content script
 | |
|  * running in ExtensionContent.jsm.
 | |
|  */
 | |
| class ContentScriptContextParent extends ProxyContextParent {}
 | |
| 
 | |
| /**
 | |
|  * The parent side of proxied API context for extension page, such as a
 | |
|  * background script, a tab page, or a popup, running in
 | |
|  * ExtensionChild.jsm.
 | |
|  */
 | |
| class ExtensionPageContextParent extends ProxyContextParent {
 | |
|   constructor(envType, extension, params, browsingContext) {
 | |
|     super(envType, extension, params, browsingContext, extension.principal);
 | |
| 
 | |
|     this.viewType = params.viewType;
 | |
|     this.isTopContext = browsingContext.top === browsingContext;
 | |
| 
 | |
|     this.extension.views.add(this);
 | |
| 
 | |
|     extension.emit("extension-proxy-context-load", this);
 | |
|   }
 | |
| 
 | |
|   // The window that contains this context. This may change due to moving tabs.
 | |
|   get appWindow() {
 | |
|     let win = this.xulBrowser.ownerGlobal;
 | |
|     return win.browsingContext.topChromeWindow;
 | |
|   }
 | |
| 
 | |
|   get currentWindow() {
 | |
|     if (this.viewType !== "background") {
 | |
|       return this.appWindow;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   get tabId() {
 | |
|     let { tabTracker } = apiManager.global;
 | |
|     let data = tabTracker.getBrowserData(this.xulBrowser);
 | |
|     if (data.tabId >= 0) {
 | |
|       return data.tabId;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   unload() {
 | |
|     super.unload();
 | |
|     this.extension.views.delete(this);
 | |
|   }
 | |
| 
 | |
|   shutdown() {
 | |
|     apiManager.emit("page-shutdown", this);
 | |
|     super.shutdown();
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * The parent side of proxied API context for devtools extension page, such as a
 | |
|  * devtools pages and panels running in ExtensionChild.jsm.
 | |
|  */
 | |
| class DevToolsExtensionPageContextParent extends ExtensionPageContextParent {
 | |
|   constructor(...params) {
 | |
|     super(...params);
 | |
| 
 | |
|     // Set all attributes that are lazily defined to `null` here.
 | |
|     //
 | |
|     // Note that we can't do that for `this._devToolsToolbox` because it will
 | |
|     // be defined when calling our parent constructor and so would override it back to `null`.
 | |
|     this._devToolsCommands = null;
 | |
|     this._onNavigatedListeners = null;
 | |
| 
 | |
|     this._onResourceAvailable = this._onResourceAvailable.bind(this);
 | |
|   }
 | |
| 
 | |
|   set devToolsToolbox(toolbox) {
 | |
|     if (this._devToolsToolbox) {
 | |
|       throw new Error("Cannot set the context DevTools toolbox twice");
 | |
|     }
 | |
| 
 | |
|     this._devToolsToolbox = toolbox;
 | |
|   }
 | |
| 
 | |
|   get devToolsToolbox() {
 | |
|     return this._devToolsToolbox;
 | |
|   }
 | |
| 
 | |
|   async addOnNavigatedListener(listener) {
 | |
|     if (!this._onNavigatedListeners) {
 | |
|       this._onNavigatedListeners = new Set();
 | |
| 
 | |
|       await this.devToolsToolbox.resourceCommand.watchResources(
 | |
|         [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
 | |
|         {
 | |
|           onAvailable: this._onResourceAvailable,
 | |
|           ignoreExistingResources: true,
 | |
|         }
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this._onNavigatedListeners.add(listener);
 | |
|   }
 | |
| 
 | |
|   removeOnNavigatedListener(listener) {
 | |
|     if (this._onNavigatedListeners) {
 | |
|       this._onNavigatedListeners.delete(listener);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * The returned "commands" object, exposing modules implemented from devtools/shared/commands.
 | |
|    * Each attribute being a static interface to communicate with the server backend.
 | |
|    *
 | |
|    * @returns {Promise<object>}
 | |
|    */
 | |
|   async getDevToolsCommands() {
 | |
|     // Ensure that we try to instantiate a commands only once,
 | |
|     // even if createCommandsForTabForWebExtension is async.
 | |
|     if (this._devToolsCommandsPromise) {
 | |
|       return this._devToolsCommandsPromise;
 | |
|     }
 | |
|     if (this._devToolsCommands) {
 | |
|       return this._devToolsCommands;
 | |
|     }
 | |
| 
 | |
|     this._devToolsCommandsPromise = (async () => {
 | |
|       const commands =
 | |
|         await lazy.DevToolsShim.createCommandsForTabForWebExtension(
 | |
|           this.devToolsToolbox.commands.descriptorFront.localTab
 | |
|         );
 | |
|       await commands.targetCommand.startListening();
 | |
|       this._devToolsCommands = commands;
 | |
|       this._devToolsCommandsPromise = null;
 | |
|       return commands;
 | |
|     })();
 | |
|     return this._devToolsCommandsPromise;
 | |
|   }
 | |
| 
 | |
|   unload() {
 | |
|     // Bail if the toolbox reference was already cleared.
 | |
|     if (!this.devToolsToolbox) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (this._onNavigatedListeners) {
 | |
|       this.devToolsToolbox.resourceCommand.unwatchResources(
 | |
|         [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
 | |
|         { onAvailable: this._onResourceAvailable }
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (this._devToolsCommands) {
 | |
|       this._devToolsCommands.destroy();
 | |
|       this._devToolsCommands = null;
 | |
|     }
 | |
| 
 | |
|     if (this._onNavigatedListeners) {
 | |
|       this._onNavigatedListeners.clear();
 | |
|       this._onNavigatedListeners = null;
 | |
|     }
 | |
| 
 | |
|     this._devToolsToolbox = null;
 | |
| 
 | |
|     super.unload();
 | |
|   }
 | |
| 
 | |
|   async _onResourceAvailable(resources) {
 | |
|     for (const resource of resources) {
 | |
|       const { targetFront } = resource;
 | |
|       if (targetFront.isTopLevel && resource.name === "dom-complete") {
 | |
|         for (const listener of this._onNavigatedListeners) {
 | |
|           listener(targetFront.url);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * The parent side of proxied API context for extension background service
 | |
|  * worker script.
 | |
|  */
 | |
| class BackgroundWorkerContextParent extends ProxyContextParent {
 | |
|   constructor(envType, extension, params) {
 | |
|     // TODO: split out from ProxyContextParent a base class that
 | |
|     // doesn't expect a browsingContext and one for contexts that are
 | |
|     // expected to have a browsingContext associated.
 | |
|     super(envType, extension, params, null, extension.principal);
 | |
| 
 | |
|     this.viewType = params.viewType;
 | |
|     this.workerDescriptorId = params.workerDescriptorId;
 | |
| 
 | |
|     this.extension.views.add(this);
 | |
| 
 | |
|     extension.emit("extension-proxy-context-load", this);
 | |
|   }
 | |
| }
 | |
| 
 | |
| ParentAPIManager = {
 | |
|   proxyContexts: new Map(),
 | |
| 
 | |
|   init() {
 | |
|     // TODO: Bug 1595186 - remove/replace all usage of MessageManager below.
 | |
|     Services.obs.addObserver(this, "message-manager-close");
 | |
| 
 | |
|     this.conduit = new lazy.BroadcastConduit(this, {
 | |
|       id: "ParentAPIManager",
 | |
|       reportOnClosed: "childId",
 | |
|       recv: [
 | |
|         "CreateProxyContext",
 | |
|         "ContextLoaded",
 | |
|         "APICall",
 | |
|         "AddListener",
 | |
|         "RemoveListener",
 | |
|       ],
 | |
|       send: ["CallResult"],
 | |
|       query: ["RunListener", "StreamFilterSuspendCancel"],
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   attachMessageManager(extension, processMessageManager) {
 | |
|     extension.parentMessageManager = processMessageManager;
 | |
|   },
 | |
| 
 | |
|   async observe(subject, topic, data) {
 | |
|     if (topic === "message-manager-close") {
 | |
|       let mm = subject;
 | |
|       for (let [childId, context] of this.proxyContexts) {
 | |
|         if (context.parentMessageManager === mm) {
 | |
|           this.closeProxyContext(childId);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // Reset extension message managers when their child processes shut down.
 | |
|       for (let extension of GlobalManager.extensionMap.values()) {
 | |
|         if (extension.parentMessageManager === mm) {
 | |
|           extension.parentMessageManager = null;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   shutdownExtension(extensionId, reason) {
 | |
|     if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(reason)) {
 | |
|       apiManager._callHandlers([extensionId], "disable", "onDisable");
 | |
|     }
 | |
| 
 | |
|     for (let [childId, context] of this.proxyContexts) {
 | |
|       if (context.extension.id == extensionId) {
 | |
|         context.shutdown();
 | |
|         this.proxyContexts.delete(childId);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   queryStreamFilterSuspendCancel(childId) {
 | |
|     return this.conduit.queryStreamFilterSuspendCancel(childId);
 | |
|   },
 | |
| 
 | |
|   recvCreateProxyContext(data, { actor, sender }) {
 | |
|     let { envType, extensionId, childId, principal } = data;
 | |
| 
 | |
|     if (this.proxyContexts.has(childId)) {
 | |
|       throw new Error(
 | |
|         "A WebExtension context with the given ID already exists!"
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     let extension = GlobalManager.getExtension(extensionId);
 | |
|     if (!extension) {
 | |
|       throw new Error(`No WebExtension found with ID ${extensionId}`);
 | |
|     }
 | |
| 
 | |
|     let context;
 | |
|     if (envType == "addon_parent" || envType == "devtools_parent") {
 | |
|       if (!sender.verified) {
 | |
|         throw new Error(`Bad sender context envType: ${sender.envType}`);
 | |
|       }
 | |
| 
 | |
|       let isBackgroundWorker = false;
 | |
|       if (JSWindowActorParent.isInstance(actor)) {
 | |
|         const target = actor.browsingContext.top.embedderElement;
 | |
|         let processMessageManager =
 | |
|           target.messageManager.processMessageManager ||
 | |
|           Services.ppmm.getChildAt(0);
 | |
| 
 | |
|         if (!extension.parentMessageManager) {
 | |
|           if (target.remoteType === extension.remoteType) {
 | |
|             this.attachMessageManager(extension, processMessageManager);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (processMessageManager !== extension.parentMessageManager) {
 | |
|           throw new Error(
 | |
|             "Attempt to create privileged extension parent from incorrect child process"
 | |
|           );
 | |
|         }
 | |
|       } else if (JSProcessActorParent.isInstance(actor)) {
 | |
|         if (actor.manager.remoteType !== extension.remoteType) {
 | |
|           throw new Error(
 | |
|             "Attempt to create privileged extension parent from incorrect child process"
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         if (envType !== "addon_parent") {
 | |
|           throw new Error(
 | |
|             `Unexpected envType ${envType} on an extension process actor`
 | |
|           );
 | |
|         }
 | |
|         if (data.viewType !== "background_worker") {
 | |
|           throw new Error(
 | |
|             `Unexpected viewType ${data.viewType} on an extension process actor`
 | |
|           );
 | |
|         }
 | |
|         isBackgroundWorker = true;
 | |
|       } else {
 | |
|         // Unreacheable: JSWindowActorParent and JSProcessActorParent are the
 | |
|         // only actors.
 | |
|         throw new Error(
 | |
|           "Attempt to create privileged extension parent via incorrect actor"
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       if (isBackgroundWorker) {
 | |
|         context = new BackgroundWorkerContextParent(envType, extension, data);
 | |
|       } else if (envType == "addon_parent") {
 | |
|         context = new ExtensionPageContextParent(
 | |
|           envType,
 | |
|           extension,
 | |
|           data,
 | |
|           actor.browsingContext
 | |
|         );
 | |
|       } else if (envType == "devtools_parent") {
 | |
|         context = new DevToolsExtensionPageContextParent(
 | |
|           envType,
 | |
|           extension,
 | |
|           data,
 | |
|           actor.browsingContext
 | |
|         );
 | |
|       }
 | |
|     } else if (envType == "content_parent") {
 | |
|       // Note: actor is always a JSWindowActorParent, with a browsingContext.
 | |
|       context = new ContentScriptContextParent(
 | |
|         envType,
 | |
|         extension,
 | |
|         data,
 | |
|         actor.browsingContext,
 | |
|         principal
 | |
|       );
 | |
|     } else {
 | |
|       throw new Error(`Invalid WebExtension context envType: ${envType}`);
 | |
|     }
 | |
|     this.proxyContexts.set(childId, context);
 | |
|   },
 | |
| 
 | |
|   recvContextLoaded(data, { actor, sender }) {
 | |
|     let context = this.getContextById(data.childId);
 | |
|     verifyActorForContext(actor, context);
 | |
|     const { extension } = context;
 | |
|     extension.emit("extension-proxy-context-load:completed", context);
 | |
|   },
 | |
| 
 | |
|   recvConduitClosed(sender) {
 | |
|     this.closeProxyContext(sender.id);
 | |
|   },
 | |
| 
 | |
|   closeProxyContext(childId) {
 | |
|     let context = this.proxyContexts.get(childId);
 | |
|     if (context) {
 | |
|       context.unload();
 | |
|       this.proxyContexts.delete(childId);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Call the given function and also log the call as appropriate
 | |
|    * (i.e., with activity logging and/or profiler markers)
 | |
|    *
 | |
|    * @param {BaseContext} context The context making this call.
 | |
|    * @param {object} data Additional data about the call.
 | |
|    * @param {Function} callable The actual implementation to invoke.
 | |
|    */
 | |
|   async callAndLog(context, data, callable) {
 | |
|     let { id } = context.extension;
 | |
|     // If we were called via callParentAsyncFunction we don't want
 | |
|     // to log again, check for the flag.
 | |
|     const { alreadyLogged } = data.options || {};
 | |
|     if (!alreadyLogged) {
 | |
|       lazy.ExtensionActivityLog.log(
 | |
|         id,
 | |
|         context.viewType,
 | |
|         "api_call",
 | |
|         data.path,
 | |
|         {
 | |
|           args: data.args,
 | |
|         }
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     let start = Cu.now();
 | |
|     try {
 | |
|       return callable();
 | |
|     } finally {
 | |
|       ChromeUtils.addProfilerMarker(
 | |
|         "ExtensionParent",
 | |
|         { startTime: start },
 | |
|         `${id}, api_call: ${data.path}`
 | |
|       );
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   async recvAPICall(data, { actor }) {
 | |
|     let context = this.getContextById(data.childId);
 | |
|     let target = actor.browsingContext?.top.embedderElement;
 | |
| 
 | |
|     verifyActorForContext(actor, context);
 | |
| 
 | |
|     let reply = result => {
 | |
|       if (target && !context.parentMessageManager) {
 | |
|         Services.console.logStringMessage(
 | |
|           "Cannot send function call result: other side closed connection " +
 | |
|             `(call data: ${uneval({ path: data.path, args: data.args })})`
 | |
|         );
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this.conduit.sendCallResult(data.childId, {
 | |
|         childId: data.childId,
 | |
|         callId: data.callId,
 | |
|         path: data.path,
 | |
|         ...result,
 | |
|       });
 | |
|     };
 | |
| 
 | |
|     try {
 | |
|       if (
 | |
|         context.isBackgroundContext &&
 | |
|         !context.extension.persistentBackground
 | |
|       ) {
 | |
|         context.extension.emit("background-script-reset-idle", {
 | |
|           reason: "parentApiCall",
 | |
|           path: data.path,
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       let args = data.args;
 | |
|       let { isHandlingUserInput = false } = data.options || {};
 | |
|       let pendingBrowser = context.pendingEventBrowser;
 | |
|       let fun = await context.apiCan.asyncFindAPIPath(data.path);
 | |
|       let result = this.callAndLog(context, data, () => {
 | |
|         return context.withPendingBrowser(pendingBrowser, () =>
 | |
|           context.withCallContextData({ isHandlingUserInput }, () =>
 | |
|             fun(...args)
 | |
|           )
 | |
|         );
 | |
|       });
 | |
| 
 | |
|       if (data.callId) {
 | |
|         result = result || Promise.resolve();
 | |
| 
 | |
|         result.then(
 | |
|           result => {
 | |
|             result = result instanceof SpreadArgs ? [...result] : [result];
 | |
| 
 | |
|             let holder = new StructuredCloneHolder(
 | |
|               `ExtensionParent/${context.extension.id}/recvAPICall/${data.path}`,
 | |
|               null,
 | |
|               result
 | |
|             );
 | |
| 
 | |
|             reply({ result: holder });
 | |
|           },
 | |
|           error => {
 | |
|             error = context.normalizeError(error);
 | |
|             reply({
 | |
|               error: { message: error.message, fileName: error.fileName },
 | |
|             });
 | |
|           }
 | |
|         );
 | |
|       }
 | |
|     } catch (e) {
 | |
|       if (data.callId) {
 | |
|         let error = context.normalizeError(e);
 | |
|         reply({ error: { message: error.message } });
 | |
|       } else {
 | |
|         Cu.reportError(e);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   async recvAddListener(data, { actor }) {
 | |
|     let context = this.getContextById(data.childId);
 | |
| 
 | |
|     verifyActorForContext(actor, context);
 | |
| 
 | |
|     let { childId, alreadyLogged = false } = data;
 | |
|     let handlingUserInput = false;
 | |
| 
 | |
|     let listener = async (...listenerArgs) => {
 | |
|       let startTime = Cu.now();
 | |
|       // Extract urgentSend flag to avoid deserializing args holder later.
 | |
|       let urgentSend = false;
 | |
|       if (listenerArgs[0] && data.path.startsWith("webRequest.")) {
 | |
|         urgentSend = listenerArgs[0].urgentSend;
 | |
|         delete listenerArgs[0].urgentSend;
 | |
|       }
 | |
|       let runListenerPromise = this.conduit.queryRunListener(childId, {
 | |
|         childId,
 | |
|         handlingUserInput,
 | |
|         listenerId: data.listenerId,
 | |
|         path: data.path,
 | |
|         urgentSend,
 | |
|         get args() {
 | |
|           return new StructuredCloneHolder(
 | |
|             `ExtensionParent/${context.extension.id}/recvAddListener/${data.path}`,
 | |
|             null,
 | |
|             listenerArgs
 | |
|           );
 | |
|         },
 | |
|       });
 | |
|       context.trackRunListenerPromise(runListenerPromise);
 | |
| 
 | |
|       const result = await runListenerPromise;
 | |
|       let rv = result && result.deserialize(globalThis);
 | |
|       ChromeUtils.addProfilerMarker(
 | |
|         "ExtensionParent",
 | |
|         { startTime },
 | |
|         `${context.extension.id}, api_event: ${data.path}`
 | |
|       );
 | |
|       lazy.ExtensionActivityLog.log(
 | |
|         context.extension.id,
 | |
|         context.viewType,
 | |
|         "api_event",
 | |
|         data.path,
 | |
|         { args: listenerArgs, result: rv }
 | |
|       );
 | |
|       return rv;
 | |
|     };
 | |
| 
 | |
|     context.listenerProxies.set(data.listenerId, listener);
 | |
| 
 | |
|     let args = data.args;
 | |
|     let promise = context.apiCan.asyncFindAPIPath(data.path);
 | |
| 
 | |
|     // Store pending listener additions so we can be sure they're all
 | |
|     // fully initialize before we consider extension startup complete.
 | |
|     if (context.isBackgroundContext && context.listenerPromises) {
 | |
|       const { listenerPromises } = context;
 | |
|       listenerPromises.add(promise);
 | |
|       let remove = () => {
 | |
|         listenerPromises.delete(promise);
 | |
|       };
 | |
|       promise.then(remove, remove);
 | |
|     }
 | |
| 
 | |
|     let handler = await promise;
 | |
|     if (handler.setUserInput) {
 | |
|       handlingUserInput = true;
 | |
|     }
 | |
|     handler.addListener(listener, ...args);
 | |
|     if (!alreadyLogged) {
 | |
|       lazy.ExtensionActivityLog.log(
 | |
|         context.extension.id,
 | |
|         context.viewType,
 | |
|         "api_call",
 | |
|         `${data.path}.addListener`,
 | |
|         { args }
 | |
|       );
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   async recvRemoveListener(data) {
 | |
|     let context = this.getContextById(data.childId);
 | |
|     let listener = context.listenerProxies.get(data.listenerId);
 | |
| 
 | |
|     let handler = await context.apiCan.asyncFindAPIPath(data.path);
 | |
|     handler.removeListener(listener);
 | |
| 
 | |
|     let { alreadyLogged = false } = data;
 | |
|     if (!alreadyLogged) {
 | |
|       lazy.ExtensionActivityLog.log(
 | |
|         context.extension.id,
 | |
|         context.viewType,
 | |
|         "api_call",
 | |
|         `${data.path}.removeListener`,
 | |
|         { args: [] }
 | |
|       );
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   getContextById(childId) {
 | |
|     let context = this.proxyContexts.get(childId);
 | |
|     if (!context) {
 | |
|       throw new Error("WebExtension context not found!");
 | |
|     }
 | |
|     return context;
 | |
|   },
 | |
| };
 | |
| 
 | |
| ParentAPIManager.init();
 | |
| 
 | |
| /**
 | |
|  * A hidden window which contains the extension pages that are not visible
 | |
|  * (i.e., background pages and devtools pages), and is also used by
 | |
|  * ExtensionDebuggingUtils to contain the browser elements used by the
 | |
|  * addon debugger to connect to the devtools actors running in the same
 | |
|  * process of the target extension (and be able to stay connected across
 | |
|  *  the addon reloads).
 | |
|  */
 | |
| class HiddenXULWindow {
 | |
|   constructor() {
 | |
|     this._windowlessBrowser = null;
 | |
|     this.unloaded = false;
 | |
|     this.waitInitialized = this.initWindowlessBrowser();
 | |
|   }
 | |
| 
 | |
|   shutdown() {
 | |
|     if (this.unloaded) {
 | |
|       throw new Error(
 | |
|         "Unable to shutdown an unloaded HiddenXULWindow instance"
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this.unloaded = true;
 | |
| 
 | |
|     this.waitInitialized = null;
 | |
| 
 | |
|     if (!this._windowlessBrowser) {
 | |
|       Cu.reportError("HiddenXULWindow was shut down while it was loading.");
 | |
|       // initWindowlessBrowser will close windowlessBrowser when possible.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._windowlessBrowser.close();
 | |
|     this._windowlessBrowser = null;
 | |
|   }
 | |
| 
 | |
|   get chromeDocument() {
 | |
|     return this._windowlessBrowser.document;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Private helper that create a HTMLDocument in a windowless browser.
 | |
|    *
 | |
|    * @returns {Promise<void>}
 | |
|    *          A promise which resolves when the windowless browser is ready.
 | |
|    */
 | |
|   async initWindowlessBrowser() {
 | |
|     if (this.waitInitialized) {
 | |
|       throw new Error("HiddenXULWindow already initialized");
 | |
|     }
 | |
| 
 | |
|     // The invisible page is currently wrapped in a XUL window to fix an issue
 | |
|     // with using the canvas API from a background page (See Bug 1274775).
 | |
|     let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
 | |
| 
 | |
|     // The windowless browser is a thin wrapper around a docShell that keeps
 | |
|     // its related resources alive. It implements nsIWebNavigation and
 | |
|     // forwards its methods to the underlying docShell. That .docShell
 | |
|     // needs `QueryInterface(nsIWebNavigation)` to give us access to the
 | |
|     // webNav methods that are already available on the windowless browser.
 | |
|     let chromeShell = windowlessBrowser.docShell;
 | |
|     chromeShell.QueryInterface(Ci.nsIWebNavigation);
 | |
| 
 | |
|     if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
 | |
|       let attrs = chromeShell.getOriginAttributes();
 | |
|       attrs.privateBrowsingId = 1;
 | |
|       chromeShell.setOriginAttributes(attrs);
 | |
|     }
 | |
| 
 | |
|     windowlessBrowser.browsingContext.useGlobalHistory = false;
 | |
|     chromeShell.loadURI(DUMMY_PAGE_URI, {
 | |
|       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
 | |
|     });
 | |
| 
 | |
|     await promiseObserved(
 | |
|       "chrome-document-global-created",
 | |
|       win => win.document == chromeShell.document
 | |
|     );
 | |
|     await promiseDocumentLoaded(windowlessBrowser.document);
 | |
|     if (this.unloaded) {
 | |
|       windowlessBrowser.close();
 | |
|       return;
 | |
|     }
 | |
|     this._windowlessBrowser = windowlessBrowser;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Creates the browser XUL element that will contain the WebExtension Page.
 | |
|    *
 | |
|    * @param {object} xulAttributes
 | |
|    *        An object that contains the xul attributes to set of the newly
 | |
|    *        created browser XUL element.
 | |
|    *
 | |
|    * @returns {Promise<XULElement>}
 | |
|    *          A Promise which resolves to the newly created browser XUL element.
 | |
|    */
 | |
|   async createBrowserElement(xulAttributes) {
 | |
|     if (!xulAttributes || Object.keys(xulAttributes).length === 0) {
 | |
|       throw new Error("missing mandatory xulAttributes parameter");
 | |
|     }
 | |
| 
 | |
|     await this.waitInitialized;
 | |
| 
 | |
|     const chromeDoc = this.chromeDocument;
 | |
| 
 | |
|     const browser = chromeDoc.createXULElement("browser");
 | |
|     browser.setAttribute("type", "content");
 | |
|     browser.setAttribute("disableglobalhistory", "true");
 | |
|     browser.setAttribute("messagemanagergroup", "webext-browsers");
 | |
| 
 | |
|     for (const [name, value] of Object.entries(xulAttributes)) {
 | |
|       if (value != null) {
 | |
|         browser.setAttribute(name, value);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let awaitFrameLoader;
 | |
| 
 | |
|     if (browser.getAttribute("remote") === "true") {
 | |
|       awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
 | |
|     }
 | |
| 
 | |
|     chromeDoc.documentElement.appendChild(browser);
 | |
| 
 | |
|     // Forcibly flush layout so that we get a pres shell soon enough, see
 | |
|     // bug 1274775.
 | |
|     browser.getBoundingClientRect();
 | |
| 
 | |
|     await awaitFrameLoader;
 | |
|     return browser;
 | |
|   }
 | |
| }
 | |
| 
 | |
| const SharedWindow = {
 | |
|   _window: null,
 | |
|   _count: 0,
 | |
| 
 | |
|   acquire() {
 | |
|     if (this._window == null) {
 | |
|       if (this._count != 0) {
 | |
|         throw new Error(
 | |
|           `Shared window already exists with count ${this._count}`
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       this._window = new HiddenXULWindow();
 | |
|     }
 | |
| 
 | |
|     this._count++;
 | |
|     return this._window;
 | |
|   },
 | |
| 
 | |
|   release() {
 | |
|     if (this._count < 1) {
 | |
|       throw new Error(`Releasing shared window with count ${this._count}`);
 | |
|     }
 | |
| 
 | |
|     this._count--;
 | |
|     if (this._count == 0) {
 | |
|       this._window.shutdown();
 | |
|       this._window = null;
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * This is a base class used by the ext-backgroundPage and ext-devtools API implementations
 | |
|  * to inherits the shared boilerplate code needed to create a parent document for the hidden
 | |
|  * extension pages (e.g. the background page, the devtools page) in the BackgroundPage and
 | |
|  * DevToolsPage classes.
 | |
|  *
 | |
|  * @param {Extension} extension
 | |
|  *        The Extension which owns the hidden extension page created (used to decide
 | |
|  *        if the hidden extension page parent doc is going to be a windowlessBrowser or
 | |
|  *        a visible XUL window).
 | |
|  * @param {string} viewType
 | |
|  *        The viewType of the WebExtension page that is going to be loaded
 | |
|  *        in the created browser element (e.g. "background" or "devtools_page").
 | |
|  */
 | |
| class HiddenExtensionPage {
 | |
|   constructor(extension, viewType) {
 | |
|     if (!extension || !viewType) {
 | |
|       throw new Error("extension and viewType parameters are mandatory");
 | |
|     }
 | |
| 
 | |
|     this.extension = extension;
 | |
|     this.viewType = viewType;
 | |
|     this.browser = null;
 | |
|     this.unloaded = false;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Destroy the created parent document.
 | |
|    */
 | |
|   shutdown() {
 | |
|     if (this.unloaded) {
 | |
|       throw new Error(
 | |
|         "Unable to shutdown an unloaded HiddenExtensionPage instance"
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     this.unloaded = true;
 | |
| 
 | |
|     if (this.browser) {
 | |
|       this._releaseBrowser();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _releaseBrowser() {
 | |
|     this.browser.remove();
 | |
|     this.browser = null;
 | |
|     SharedWindow.release();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Creates the browser XUL element that will contain the WebExtension Page.
 | |
|    *
 | |
|    * @returns {Promise<XULElement>}
 | |
|    *          A Promise which resolves to the newly created browser XUL element.
 | |
|    */
 | |
|   async createBrowserElement() {
 | |
|     if (this.browser) {
 | |
|       throw new Error("createBrowserElement called twice");
 | |
|     }
 | |
| 
 | |
|     let window = SharedWindow.acquire();
 | |
|     try {
 | |
|       this.browser = await window.createBrowserElement({
 | |
|         "webextension-view-type": this.viewType,
 | |
|         remote: this.extension.remote ? "true" : null,
 | |
|         remoteType: this.extension.remoteType,
 | |
|         initialBrowsingContextGroupId: this.extension.browsingContextGroupId,
 | |
|       });
 | |
|     } catch (e) {
 | |
|       SharedWindow.release();
 | |
|       throw e;
 | |
|     }
 | |
| 
 | |
|     if (this.unloaded) {
 | |
|       this._releaseBrowser();
 | |
|       throw new Error("Extension shut down before browser element was created");
 | |
|     }
 | |
| 
 | |
|     return this.browser;
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This object provides utility functions needed by the devtools actors to
 | |
|  * be able to connect and debug an extension (which can run in the main or in
 | |
|  * a child extension process).
 | |
|  */
 | |
| const DebugUtils = {
 | |
|   // A lazily created hidden XUL window, which contains the browser elements
 | |
|   // which are used to connect the webextension patent actor to the extension process.
 | |
|   hiddenXULWindow: null,
 | |
| 
 | |
|   // Map<extensionId, Promise<XULElement>>
 | |
|   debugBrowserPromises: new Map(),
 | |
|   // DefaultWeakMap<Promise<browser XULElement>, Set<WebExtensionParentActor>>
 | |
|   debugActors: new DefaultWeakMap(() => new Set()),
 | |
| 
 | |
|   _extensionUpdatedWatcher: null,
 | |
|   watchExtensionUpdated() {
 | |
|     if (!this._extensionUpdatedWatcher) {
 | |
|       // Watch the updated extension objects.
 | |
|       this._extensionUpdatedWatcher = async (evt, extension) => {
 | |
|         const browserPromise = this.debugBrowserPromises.get(extension.id);
 | |
|         if (browserPromise) {
 | |
|           const browser = await browserPromise;
 | |
|           if (
 | |
|             browser.isRemoteBrowser !== extension.remote &&
 | |
|             this.debugBrowserPromises.get(extension.id) === browserPromise
 | |
|           ) {
 | |
|             // If the cached browser element is not anymore of the same
 | |
|             // remote type of the extension, remove it.
 | |
|             this.debugBrowserPromises.delete(extension.id);
 | |
|             browser.remove();
 | |
|           }
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       apiManager.on("ready", this._extensionUpdatedWatcher);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   unwatchExtensionUpdated() {
 | |
|     if (this._extensionUpdatedWatcher) {
 | |
|       apiManager.off("ready", this._extensionUpdatedWatcher);
 | |
|       delete this._extensionUpdatedWatcher;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   getExtensionManifestWarnings(id) {
 | |
|     const addon = GlobalManager.extensionMap.get(id);
 | |
|     if (addon) {
 | |
|       return addon.warnings;
 | |
|     }
 | |
|     return [];
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determine if the extension does have a non-persistent background script
 | |
|    * (either an event page or a background service worker):
 | |
|    *
 | |
|    * Based on this the DevTools client will determine if this extension should provide
 | |
|    * to the extension developers a button to forcefully terminate the background
 | |
|    * script.
 | |
|    *
 | |
|    * @param {string} addonId
 | |
|    *   The id of the addon
 | |
|    *
 | |
|    * @returns {void|boolean}
 | |
|    *   - undefined => does not apply (no background script in the manifest)
 | |
|    *   - true => the background script is persistent.
 | |
|    *   - false => the background script is an event page or a service worker.
 | |
|    */
 | |
|   hasPersistentBackgroundScript(addonId) {
 | |
|     const policy = WebExtensionPolicy.getByID(addonId);
 | |
| 
 | |
|     // The addon doesn't have any background script or we
 | |
|     // can't be sure yet.
 | |
|     if (
 | |
|       policy?.extension?.type !== "extension" ||
 | |
|       !policy?.extension?.manifest?.background
 | |
|     ) {
 | |
|       return undefined;
 | |
|     }
 | |
| 
 | |
|     return policy.extension.persistentBackground;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determine if the extension background page is running.
 | |
|    *
 | |
|    * Based on this the DevTools client will show the status of the background
 | |
|    * script in about:debugging.
 | |
|    *
 | |
|    * @param {string} addonId
 | |
|    *   The id of the addon
 | |
|    *
 | |
|    * @returns {void|boolean}
 | |
|    *   - undefined => does not apply (no background script in the manifest)
 | |
|    *   - true => the background script is running.
 | |
|    *   - false => the background script is stopped.
 | |
|    */
 | |
|   isBackgroundScriptRunning(addonId) {
 | |
|     const policy = WebExtensionPolicy.getByID(addonId);
 | |
| 
 | |
|     // The addon doesn't have any background script or we
 | |
|     // can't be sure yet.
 | |
|     if (!(this.hasPersistentBackgroundScript(addonId) === false)) {
 | |
|       return undefined;
 | |
|     }
 | |
| 
 | |
|     const views = policy?.extension?.views || [];
 | |
|     for (const view of views) {
 | |
|       if (
 | |
|         view.viewType === "background" ||
 | |
|         (view.viewType === "background_worker" && !view.unloaded)
 | |
|       ) {
 | |
|         return true;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return false;
 | |
|   },
 | |
| 
 | |
|   async terminateBackgroundScript(addonId) {
 | |
|     // Terminate the background if the extension does have
 | |
|     // a non-persistent background script (event page or background
 | |
|     // service worker).
 | |
|     if (this.hasPersistentBackgroundScript(addonId) === false) {
 | |
|       const policy = WebExtensionPolicy.getByID(addonId);
 | |
|       // When the event page is being terminated through the Devtools
 | |
|       // action, we should terminate it even if there are DevTools
 | |
|       // toolboxes attached to the extension.
 | |
|       return policy.extension.terminateBackground({
 | |
|         ignoreDevToolsAttached: true,
 | |
|       });
 | |
|     }
 | |
|     throw Error(`Unable to terminate background script for ${addonId}`);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determine whether a devtools toolbox attached to the extension.
 | |
|    *
 | |
|    * This method is called by the background page idle timeout handler,
 | |
|    * to inhibit terminating the event page when idle while the extension
 | |
|    * developer is debugging the extension through the Addon Debugging window
 | |
|    * (similarly to how service workers are kept alive while the devtools are
 | |
|    * attached).
 | |
|    *
 | |
|    * @param {string} id
 | |
|    *        The id of the extension.
 | |
|    *
 | |
|    * @returns {boolean}
 | |
|    *          true when a devtools toolbox is attached to an extension with
 | |
|    *          the given id, false otherwise.
 | |
|    */
 | |
|   hasDevToolsAttached(id) {
 | |
|     return this.debugBrowserPromises.has(id);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Retrieve a XUL browser element which has been configured to be able to connect
 | |
|    * the devtools actor with the process where the extension is running.
 | |
|    *
 | |
|    * @param {WebExtensionParentActor} webExtensionParentActor
 | |
|    *        The devtools actor that is retrieving the browser element.
 | |
|    *
 | |
|    * @returns {Promise<XULElement>}
 | |
|    *          A promise which resolves to the configured browser XUL element.
 | |
|    */
 | |
|   async getExtensionProcessBrowser(webExtensionParentActor) {
 | |
|     const extensionId = webExtensionParentActor.addonId;
 | |
|     const extension = GlobalManager.getExtension(extensionId);
 | |
|     if (!extension) {
 | |
|       throw new Error(`Extension not found: ${extensionId}`);
 | |
|     }
 | |
| 
 | |
|     const createBrowser = () => {
 | |
|       if (!this.hiddenXULWindow) {
 | |
|         this.hiddenXULWindow = new HiddenXULWindow();
 | |
|         this.watchExtensionUpdated();
 | |
|       }
 | |
| 
 | |
|       return this.hiddenXULWindow.createBrowserElement({
 | |
|         "webextension-addon-debug-target": extensionId,
 | |
|         remote: extension.remote ? "true" : null,
 | |
|         remoteType: extension.remoteType,
 | |
|         initialBrowsingContextGroupId: extension.browsingContextGroupId,
 | |
|       });
 | |
|     };
 | |
| 
 | |
|     let browserPromise = this.debugBrowserPromises.get(extensionId);
 | |
| 
 | |
|     // Create a new promise if there is no cached one in the map.
 | |
|     if (!browserPromise) {
 | |
|       browserPromise = createBrowser();
 | |
|       this.debugBrowserPromises.set(extensionId, browserPromise);
 | |
|       browserPromise.then(browser => {
 | |
|         browserPromise.browser = browser;
 | |
|       });
 | |
|       browserPromise.catch(e => {
 | |
|         Cu.reportError(e);
 | |
|         this.debugBrowserPromises.delete(extensionId);
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     this.debugActors.get(browserPromise).add(webExtensionParentActor);
 | |
| 
 | |
|     return browserPromise;
 | |
|   },
 | |
| 
 | |
|   getFrameLoader(extensionId) {
 | |
|     let promise = this.debugBrowserPromises.get(extensionId);
 | |
|     return promise && promise.browser && promise.browser.frameLoader;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Given the devtools actor that has retrieved an addon debug browser element,
 | |
|    * it destroys the XUL browser element, and it also destroy the hidden XUL window
 | |
|    * if it is not currently needed.
 | |
|    *
 | |
|    * @param {WebExtensionParentActor} webExtensionParentActor
 | |
|    *        The devtools actor that has retrieved an addon debug browser element.
 | |
|    */
 | |
|   async releaseExtensionProcessBrowser(webExtensionParentActor) {
 | |
|     const extensionId = webExtensionParentActor.addonId;
 | |
|     const browserPromise = this.debugBrowserPromises.get(extensionId);
 | |
| 
 | |
|     if (browserPromise) {
 | |
|       const actorsSet = this.debugActors.get(browserPromise);
 | |
|       actorsSet.delete(webExtensionParentActor);
 | |
|       if (actorsSet.size === 0) {
 | |
|         this.debugActors.delete(browserPromise);
 | |
|         this.debugBrowserPromises.delete(extensionId);
 | |
|         await browserPromise.then(browser => browser.remove());
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (this.debugBrowserPromises.size === 0 && this.hiddenXULWindow) {
 | |
|       this.hiddenXULWindow.shutdown();
 | |
|       this.hiddenXULWindow = null;
 | |
|       this.unwatchExtensionUpdated();
 | |
|     }
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Returns a Promise which resolves with the message data when the given message
 | |
|  * was received by the message manager. The promise is rejected if the message
 | |
|  * manager was closed before a message was received.
 | |
|  *
 | |
|  * @param {nsIMessageListenerManager} messageManager
 | |
|  *        The message manager on which to listen for messages.
 | |
|  * @param {string} messageName
 | |
|  *        The message to listen for.
 | |
|  * @returns {Promise<*>}
 | |
|  */
 | |
| function promiseMessageFromChild(messageManager, messageName) {
 | |
|   return new Promise((resolve, reject) => {
 | |
|     let unregister;
 | |
|     function listener(message) {
 | |
|       unregister();
 | |
|       resolve(message.data);
 | |
|     }
 | |
|     function observer(subject, topic, data) {
 | |
|       if (subject === messageManager) {
 | |
|         unregister();
 | |
|         reject(
 | |
|           new Error(
 | |
|             `Message manager was disconnected before receiving ${messageName}`
 | |
|           )
 | |
|         );
 | |
|       }
 | |
|     }
 | |
|     unregister = () => {
 | |
|       Services.obs.removeObserver(observer, "message-manager-close");
 | |
|       messageManager.removeMessageListener(messageName, listener);
 | |
|     };
 | |
|     messageManager.addMessageListener(messageName, listener);
 | |
|     Services.obs.addObserver(observer, "message-manager-close");
 | |
|   });
 | |
| }
 | |
| 
 | |
| // This should be called before browser.loadURI is invoked.
 | |
| async function promiseBackgroundViewLoaded(browser) {
 | |
|   let { childId } = await promiseMessageFromChild(
 | |
|     browser.messageManager,
 | |
|     "Extension:BackgroundViewLoaded"
 | |
|   );
 | |
|   if (childId) {
 | |
|     return ParentAPIManager.getContextById(childId);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This helper is used to subscribe a listener (e.g. in the ext-devtools API implementation)
 | |
|  * to be called for every ExtensionProxyContext created for an extension page given
 | |
|  * its related extension, viewType and browser element (both the top level context and any context
 | |
|  * created for the extension urls running into its iframe descendants).
 | |
|  *
 | |
|  * @param {object} params
 | |
|  * @param {object} params.extension
 | |
|  *        The Extension on which we are going to listen for the newly created ExtensionProxyContext.
 | |
|  * @param {string} params.viewType
 | |
|  *        The viewType of the WebExtension page that we are watching (e.g. "background" or
 | |
|  *        "devtools_page").
 | |
|  * @param {XULElement} params.browser
 | |
|  *        The browser element of the WebExtension page that we are watching.
 | |
|  * @param {Function} onExtensionProxyContextLoaded
 | |
|  *        The callback that is called when a new context has been loaded (as `callback(context)`);
 | |
|  *
 | |
|  * @returns {Function}
 | |
|  *          Unsubscribe the listener.
 | |
|  */
 | |
| function watchExtensionProxyContextLoad(
 | |
|   { extension, viewType, browser },
 | |
|   onExtensionProxyContextLoaded
 | |
| ) {
 | |
|   if (typeof onExtensionProxyContextLoaded !== "function") {
 | |
|     throw new Error("Missing onExtensionProxyContextLoaded handler");
 | |
|   }
 | |
| 
 | |
|   const listener = (event, context) => {
 | |
|     if (context.viewType == viewType && context.xulBrowser == browser) {
 | |
|       onExtensionProxyContextLoaded(context);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   extension.on("extension-proxy-context-load", listener);
 | |
| 
 | |
|   return () => {
 | |
|     extension.off("extension-proxy-context-load", listener);
 | |
|   };
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * This helper is used to subscribe a listener (e.g. in the ext-backgroundPage)
 | |
|  * to be called for every ExtensionProxyContext created for an extension
 | |
|  * background service worker given its related extension.
 | |
|  *
 | |
|  * @param {object} params
 | |
|  * @param {object} params.extension
 | |
|  *        The Extension on which we are going to listen for the newly created ExtensionProxyContext.
 | |
|  * @param {Function} onExtensionWorkerContextLoaded
 | |
|  *        The callback that is called when the worker script has been fully loaded (as `callback(context)`);
 | |
|  *
 | |
|  * @returns {Function}
 | |
|  *          Unsubscribe the listener.
 | |
|  */
 | |
| function watchExtensionWorkerContextLoaded(
 | |
|   { extension },
 | |
|   onExtensionWorkerContextLoaded
 | |
| ) {
 | |
|   if (typeof onExtensionWorkerContextLoaded !== "function") {
 | |
|     throw new Error("Missing onExtensionWorkerContextLoaded handler");
 | |
|   }
 | |
| 
 | |
|   const listener = (event, context) => {
 | |
|     if (context.viewType == "background_worker") {
 | |
|       onExtensionWorkerContextLoaded(context);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   extension.on("extension-proxy-context-load:completed", listener);
 | |
| 
 | |
|   return () => {
 | |
|     extension.off("extension-proxy-context-load:completed", listener);
 | |
|   };
 | |
| }
 | |
| 
 | |
| // Manages icon details for toolbar buttons in the |pageAction| and
 | |
| // |browserAction| APIs.
 | |
| let IconDetails = {
 | |
|   DEFAULT_ICON: "chrome://mozapps/skin/extensions/extensionGeneric.svg",
 | |
| 
 | |
|   // WeakMap<Extension -> Map<url-string -> Map<iconType-string -> object>>>
 | |
|   iconCache: new DefaultWeakMap(() => {
 | |
|     return new DefaultMap(() => new DefaultMap(() => new Map()));
 | |
|   }),
 | |
| 
 | |
|   // Normalizes the various acceptable input formats into an object
 | |
|   // with icon size as key and icon URL as value.
 | |
|   //
 | |
|   // If a context is specified (function is called from an extension):
 | |
|   // Throws an error if an invalid icon size was provided or the
 | |
|   // extension is not allowed to load the specified resources.
 | |
|   //
 | |
|   // If no context is specified, instead of throwing an error, this
 | |
|   // function simply logs a warning message.
 | |
|   normalize(details, extension, context = null) {
 | |
|     if (!details.imageData && details.path != null) {
 | |
|       // Pick a cache key for the icon paths. If the path is a string,
 | |
|       // use it directly. Otherwise, stringify the path object.
 | |
|       let key = details.path;
 | |
|       if (typeof key !== "string") {
 | |
|         key = uneval(key);
 | |
|       }
 | |
| 
 | |
|       let icons = this.iconCache
 | |
|         .get(extension)
 | |
|         .get(context && context.uri.spec)
 | |
|         .get(details.iconType);
 | |
| 
 | |
|       let icon = icons.get(key);
 | |
|       if (!icon) {
 | |
|         icon = this._normalize(details, extension, context);
 | |
|         icons.set(key, icon);
 | |
|       }
 | |
|       return icon;
 | |
|     }
 | |
| 
 | |
|     return this._normalize(details, extension, context);
 | |
|   },
 | |
| 
 | |
|   _normalize(details, extension, context = null) {
 | |
|     let result = {};
 | |
| 
 | |
|     try {
 | |
|       let { imageData, path, themeIcons } = details;
 | |
| 
 | |
|       if (imageData) {
 | |
|         if (typeof imageData == "string") {
 | |
|           imageData = { 19: imageData };
 | |
|         }
 | |
| 
 | |
|         for (let size of Object.keys(imageData)) {
 | |
|           result[size] = imageData[size];
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       let baseURI = context ? context.uri : extension.baseURI;
 | |
| 
 | |
|       if (path != null) {
 | |
|         if (typeof path != "object") {
 | |
|           path = { 19: path };
 | |
|         }
 | |
| 
 | |
|         for (let size of Object.keys(path)) {
 | |
|           let url = path[size];
 | |
|           if (url) {
 | |
|             url = baseURI.resolve(path[size]);
 | |
| 
 | |
|             // The Chrome documentation specifies these parameters as
 | |
|             // relative paths. We currently accept absolute URLs as well,
 | |
|             // which means we need to check that the extension is allowed
 | |
|             // to load them. This will throw an error if it's not allowed.
 | |
|             this._checkURL(url, extension);
 | |
|           }
 | |
|           result[size] = url || this.DEFAULT_ICON;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (themeIcons) {
 | |
|         themeIcons.forEach(({ size, light, dark }) => {
 | |
|           let lightURL = baseURI.resolve(light);
 | |
|           let darkURL = baseURI.resolve(dark);
 | |
| 
 | |
|           this._checkURL(lightURL, extension);
 | |
|           this._checkURL(darkURL, extension);
 | |
| 
 | |
|           let defaultURL = result[size] || result[19]; // always fallback to default first
 | |
|           result[size] = {
 | |
|             default: defaultURL || darkURL, // Fallback to the dark url if no default is specified.
 | |
|             light: lightURL,
 | |
|             dark: darkURL,
 | |
|           };
 | |
|         });
 | |
|       }
 | |
|     } catch (e) {
 | |
|       // Function is called from extension code, delegate error.
 | |
|       if (context) {
 | |
|         throw e;
 | |
|       }
 | |
|       // If there's no context, it's because we're handling this
 | |
|       // as a manifest directive. Log a warning rather than
 | |
|       // raising an error.
 | |
|       extension.manifestError(`Invalid icon data: ${e}`);
 | |
|     }
 | |
| 
 | |
|     return result;
 | |
|   },
 | |
| 
 | |
|   // Checks if the extension is allowed to load the given URL with the specified principal.
 | |
|   // This will throw an error if the URL is not allowed.
 | |
|   _checkURL(url, extension) {
 | |
|     if (!extension.checkLoadURL(url, { allowInheritsPrincipal: true })) {
 | |
|       throw new ExtensionError(`Illegal URL ${url}`);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // Returns the appropriate icon URL for the given icons object and the
 | |
|   // screen resolution of the given window.
 | |
|   getPreferredIcon(icons, extension = null, size = 16) {
 | |
|     const DEFAULT = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
 | |
| 
 | |
|     let bestSize = null;
 | |
|     if (icons[size]) {
 | |
|       bestSize = size;
 | |
|     } else if (icons[2 * size]) {
 | |
|       bestSize = 2 * size;
 | |
|     } else {
 | |
|       let sizes = Object.keys(icons)
 | |
|         .map(key => parseInt(key, 10))
 | |
|         .sort((a, b) => a - b);
 | |
| 
 | |
|       bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
 | |
|     }
 | |
| 
 | |
|     if (bestSize) {
 | |
|       return { size: bestSize, icon: icons[bestSize] || DEFAULT };
 | |
|     }
 | |
| 
 | |
|     return { size, icon: DEFAULT };
 | |
|   },
 | |
| 
 | |
|   // These URLs should already be properly escaped, but make doubly sure CSS
 | |
|   // string escape characters are escaped here, since they could lead to a
 | |
|   // sandbox break.
 | |
|   escapeUrl(url) {
 | |
|     return url.replace(/[\\\s"]/g, encodeURIComponent);
 | |
|   },
 | |
| };
 | |
| 
 | |
| class CacheStore {
 | |
|   constructor(storeName) {
 | |
|     this.storeName = storeName;
 | |
|   }
 | |
| 
 | |
|   async getStore(path = null) {
 | |
|     let data = await StartupCache.dataPromise;
 | |
| 
 | |
|     let store = data.get(this.storeName);
 | |
|     if (!store) {
 | |
|       store = new Map();
 | |
|       data.set(this.storeName, store);
 | |
|     }
 | |
| 
 | |
|     let key = path;
 | |
|     if (Array.isArray(path)) {
 | |
|       for (let elem of path.slice(0, -1)) {
 | |
|         let next = store.get(elem);
 | |
|         if (!next) {
 | |
|           next = new Map();
 | |
|           store.set(elem, next);
 | |
|         }
 | |
|         store = next;
 | |
|       }
 | |
|       key = path[path.length - 1];
 | |
|     }
 | |
| 
 | |
|     return [store, key];
 | |
|   }
 | |
| 
 | |
|   async get(path, createFunc) {
 | |
|     let [store, key] = await this.getStore(path);
 | |
| 
 | |
|     let result = store.get(key);
 | |
| 
 | |
|     if (result === undefined) {
 | |
|       result = await createFunc(path);
 | |
|       store.set(key, result);
 | |
|       StartupCache.save();
 | |
|     }
 | |
| 
 | |
|     return result;
 | |
|   }
 | |
| 
 | |
|   async set(path, value) {
 | |
|     let [store, key] = await this.getStore(path);
 | |
| 
 | |
|     store.set(key, value);
 | |
|     StartupCache.save();
 | |
|   }
 | |
| 
 | |
|   async getAll() {
 | |
|     let [store] = await this.getStore();
 | |
| 
 | |
|     return new Map(store);
 | |
|   }
 | |
| 
 | |
|   async delete(path) {
 | |
|     let [store, key] = await this.getStore(path);
 | |
| 
 | |
|     if (store.delete(key)) {
 | |
|       StartupCache.save();
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| // A cache to support faster initialization of extensions at browser startup.
 | |
| // All cached data is removed when the browser is updated.
 | |
| // Extension-specific data is removed when the add-on is updated.
 | |
| var StartupCache = {
 | |
|   _ensureDirectoryPromise: null,
 | |
|   _saveTask: null,
 | |
| 
 | |
|   _ensureDirectory() {
 | |
|     if (this._ensureDirectoryPromise === null) {
 | |
|       this._ensureDirectoryPromise = IOUtils.makeDirectory(
 | |
|         PathUtils.parent(this.file),
 | |
|         {
 | |
|           ignoreExisting: true,
 | |
|           createAncestors: true,
 | |
|         }
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return this._ensureDirectoryPromise;
 | |
|   },
 | |
| 
 | |
|   // When the application version changes, this file is removed by
 | |
|   // RemoveComponentRegistries in nsAppRunner.cpp.
 | |
|   file: PathUtils.join(
 | |
|     Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
 | |
|     "startupCache",
 | |
|     "webext.sc.lz4"
 | |
|   ),
 | |
| 
 | |
|   async _saveNow() {
 | |
|     let data = new Uint8Array(lazy.aomStartup.encodeBlob(this._data));
 | |
|     await this._ensureDirectoryPromise;
 | |
|     await IOUtils.write(this.file, data, { tmpPath: `${this.file}.tmp` });
 | |
| 
 | |
|     Glean.extensions.startupCacheWriteBytelength.set(data.byteLength);
 | |
|   },
 | |
| 
 | |
|   save() {
 | |
|     this._ensureDirectory();
 | |
| 
 | |
|     if (!this._saveTask) {
 | |
|       this._saveTask = new lazy.DeferredTask(() => this._saveNow(), 5000);
 | |
| 
 | |
|       IOUtils.profileBeforeChange.addBlocker(
 | |
|         "Flush WebExtension StartupCache",
 | |
|         async () => {
 | |
|           await this._saveTask.finalize();
 | |
|           this._saveTask = null;
 | |
|         }
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return this._saveTask.arm();
 | |
|   },
 | |
| 
 | |
|   _data: null,
 | |
|   async _readData() {
 | |
|     let result = new Map();
 | |
|     try {
 | |
|       Glean.extensions.startupCacheLoadTime.start();
 | |
|       let { buffer } = await IOUtils.read(this.file);
 | |
| 
 | |
|       result = lazy.aomStartup.decodeBlob(buffer);
 | |
|       Glean.extensions.startupCacheLoadTime.stop();
 | |
|     } catch (e) {
 | |
|       Glean.extensions.startupCacheLoadTime.cancel();
 | |
|       if (!DOMException.isInstance(e) || e.name !== "NotFoundError") {
 | |
|         Cu.reportError(e);
 | |
|       }
 | |
|       let error = lazy.getErrorNameForTelemetry(e);
 | |
|       Glean.extensions.startupCacheReadErrors[error].add(1);
 | |
|     }
 | |
| 
 | |
|     this._data = result;
 | |
|     return result;
 | |
|   },
 | |
| 
 | |
|   get dataPromise() {
 | |
|     if (!this._dataPromise) {
 | |
|       this._dataPromise = this._readData();
 | |
|     }
 | |
|     return this._dataPromise;
 | |
|   },
 | |
| 
 | |
|   clearAddonData(id) {
 | |
|     return Promise.all([
 | |
|       this.general.delete(id),
 | |
|       this.locales.delete(id),
 | |
|       this.manifests.delete(id),
 | |
|       this.permissions.delete(id),
 | |
|       this.menus.delete(id),
 | |
|     ]).catch(e => {
 | |
|       // Ignore the error. It happens when we try to flush the add-on
 | |
|       // data after the AddonManager has flushed the entire startup cache.
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   observe(subject, topic, data) {
 | |
|     if (topic === "startupcache-invalidate") {
 | |
|       this._data = new Map();
 | |
|       this._dataPromise = Promise.resolve(this._data);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   get(extension, path, createFunc) {
 | |
|     return this.general.get(
 | |
|       [extension.id, extension.version, ...path],
 | |
|       createFunc
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   delete(extension, path) {
 | |
|     return this.general.delete([extension.id, extension.version, ...path]);
 | |
|   },
 | |
| 
 | |
|   general: new CacheStore("general"),
 | |
|   locales: new CacheStore("locales"),
 | |
|   manifests: new CacheStore("manifests"),
 | |
|   other: new CacheStore("other"),
 | |
|   permissions: new CacheStore("permissions"),
 | |
|   schemas: new CacheStore("schemas"),
 | |
|   menus: new CacheStore("menus"),
 | |
| };
 | |
| 
 | |
| Services.obs.addObserver(StartupCache, "startupcache-invalidate");
 | |
| 
 | |
| export var ExtensionParent = {
 | |
|   GlobalManager,
 | |
|   HiddenExtensionPage,
 | |
|   IconDetails,
 | |
|   ParentAPIManager,
 | |
|   StartupCache,
 | |
|   WebExtensionPolicy,
 | |
|   apiManager,
 | |
|   promiseBackgroundViewLoaded,
 | |
|   watchExtensionProxyContextLoad,
 | |
|   watchExtensionWorkerContextLoaded,
 | |
|   DebugUtils,
 | |
| };
 | |
| 
 | |
| // browserPaintedPromise and browserStartupPromise are promises that
 | |
| // resolve after the first browser window is painted and after browser
 | |
| // windows have been restored, respectively. Alternatively,
 | |
| // browserStartupPromise also resolves from the extensions-late-startup
 | |
| // notification sent by Firefox Reality on desktop platforms, because it
 | |
| // doesn't support SessionStore.
 | |
| // _resetStartupPromises should only be called from outside this file in tests.
 | |
| ExtensionParent._resetStartupPromises = () => {
 | |
|   ExtensionParent.browserPaintedPromise = promiseObserved(
 | |
|     "browser-delayed-startup-finished"
 | |
|   ).then(() => {});
 | |
|   ExtensionParent.browserStartupPromise = Promise.race([
 | |
|     promiseObserved("sessionstore-windows-restored"),
 | |
|     promiseObserved("extensions-late-startup"),
 | |
|   ]).then(() => {});
 | |
| };
 | |
| ExtensionParent._resetStartupPromises();
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(ExtensionParent, "PlatformInfo", () => {
 | |
|   return Object.freeze({
 | |
|     os: (function () {
 | |
|       let os = AppConstants.platform;
 | |
|       if (os == "macosx") {
 | |
|         os = "mac";
 | |
|       }
 | |
|       return os;
 | |
|     })(),
 | |
|     arch: (function () {
 | |
|       let abi = Services.appinfo.XPCOMABI;
 | |
|       let [arch] = abi.split("-");
 | |
|       if (arch == "x86") {
 | |
|         arch = "x86-32";
 | |
|       } else if (arch == "x86_64") {
 | |
|         arch = "x86-64";
 | |
|       }
 | |
|       return arch;
 | |
|     })(),
 | |
|   });
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Retreives the browser_style stylesheets needed for extension popups and sidebars.
 | |
|  *
 | |
|  * @returns {Array<string>} an array of stylesheets needed for the current platform.
 | |
|  */
 | |
| ChromeUtils.defineLazyGetter(ExtensionParent, "extensionStylesheets", () => {
 | |
|   let stylesheets = ["chrome://browser/content/extension.css"];
 | |
| 
 | |
|   if (AppConstants.platform === "macosx") {
 | |
|     stylesheets.push("chrome://browser/content/extension-mac.css");
 | |
|   }
 | |
| 
 | |
|   return stylesheets;
 | |
| });
 | 
