/* 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/. */ "use strict"; /* * Represents a WebExtension add-on in the parent process. This gives some metadata about * the add-on and watches for uninstall events. This uses a proxy to access the * WebExtension in the WebExtension process via the message manager. * * See devtools/docs/backend/actor-hierarchy.md for more details. */ const { Actor } = require("resource://devtools/shared/protocol.js"); const { webExtensionDescriptorSpec, } = require("resource://devtools/shared/specs/descriptors/webextension.js"); const { createWebExtensionSessionContext, } = require("resource://devtools/server/actors/watcher/session-context.js"); const lazy = {}; loader.lazyGetter(lazy, "AddonManager", () => { return ChromeUtils.importESModule( "resource://gre/modules/AddonManager.sys.mjs", { global: "shared" } ).AddonManager; }); loader.lazyGetter(lazy, "ExtensionParent", () => { return ChromeUtils.importESModule( "resource://gre/modules/ExtensionParent.sys.mjs", { global: "shared" } ).ExtensionParent; }); loader.lazyRequireGetter( this, "WatcherActor", "resource://devtools/server/actors/watcher.js", true ); const { WEBEXTENSION_FALLBACK_DOC_URL } = ChromeUtils.importESModule( "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", { global: "contextual" } ); const BGSCRIPT_STATUSES = { RUNNING: "RUNNING", STOPPED: "STOPPED", }; /** * Creates the actor that represents the addon in the parent process, which relies * on its child Watcher Actor to expose all WindowGlobal target actors for all * the active documents involved in the debugged addon. * * The WebExtensionDescriptorActor subscribes itself as an AddonListener on the AddonManager * and forwards this events to child actor (e.g. on addon reload or when the addon is * uninstalled completely) and connects to the child extension process using a `browser` * element provided by the extension internals (it is not related to any single extension, * but it will be created automatically to the currently selected "WebExtensions OOP mode" * and it persist across the extension reloads. * * The descriptor will also be persisted when the target actor is destroyed, so * that we can reuse the same descriptor for several remote debugging toolboxes * from about:debugging. * * WebExtensionDescriptorActor is a child of RootActor, it can be retrieved via * RootActor.listAddons request. * * @param {DevToolsServerConnection} conn * The connection to the client. * @param {AddonWrapper} addon * The target addon. */ class WebExtensionDescriptorActor extends Actor { constructor(conn, addon) { super(conn, webExtensionDescriptorSpec); this.addon = addon; this.addonId = addon.id; this.destroy = this.destroy.bind(this); lazy.AddonManager.addAddonListener(this); } form() { const { addonId } = this; const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(addonId); const persistentBackgroundScript = lazy.ExtensionParent.DebugUtils.hasPersistentBackgroundScript(addonId); const backgroundScriptStatus = this._getBackgroundScriptStatus(); return { actor: this.actorID, backgroundScriptStatus, // Note that until the policy becomes active, // getWatcher will fail attaching to the web extension: // https://searchfox.org/mozilla-central/rev/526a5089c61db85d4d43eb0e46edaf1f632e853a/toolkit/components/extensions/WebExtensionPolicy.cpp#551-553 debuggable: policy?.active && this.addon.isDebuggable, hidden: this.addon.hidden, // iconDataURL is available after calling loadIconDataURL iconDataURL: this._iconDataURL, iconURL: this.addon.iconURL, id: addonId, isSystem: this.addon.isSystem, isWebExtension: this.addon.isWebExtension, manifestURL: policy && policy.getURL("manifest.json"), name: this.addon.name, persistentBackgroundScript, temporarilyInstalled: this.addon.temporarilyInstalled, traits: { supportsReloadDescriptor: true, // Supports the Watcher actor. Can be removed as part of Bug 1680280. watcher: true, }, url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined, warnings: lazy.ExtensionParent.DebugUtils.getExtensionManifestWarnings( this.addonId ), }; } /** * Return a Watcher actor, allowing to keep track of targets which * already exists or will be created. It also helps knowing when they * are destroyed. */ async getWatcher(config = {}) { if (!this.watcher) { // Spawn an empty document so that we always have an active WindowGlobal, // so that we can always instantiate a top level WindowGlobal target to the frontend. await this.#createFallbackDocument(); this.watcher = new WatcherActor( this.conn, createWebExtensionSessionContext( { addonId: this.addonId, }, config ) ); this.manage(this.watcher); } return this.watcher; } /** * Create an empty document to circumvant the lack of any WindowGlobal/document * running for this addon. * * For now DevTools always expect at least one Target to be functional, * and we need a document to spawn a target actor. */ async #createFallbackDocument() { if (this._browser) { return; } // The extension process browser will only be released on descriptor destruction and can // be reused for subsequent watchers if we close and reopen a toolbox from about:debugging. // // Note that this `getExtensionProcessBrowser` will register the DevTools to the extension codebase. // If we stop creating a fallback document, we should register DevTools by some other means. this._browser = await lazy.ExtensionParent.DebugUtils.getExtensionProcessBrowser(this); // As "load" event isn't fired on the element, use a Web Progress Listener // in order to wait for the full loading of that fallback document. // It prevents having to deal with the initial about:blank document in the content processes. // We have various checks to identify the fallback document based on its URL. // It also ensure that the fallback document is created before the watcher starts // and helps spawning the target for that document first. const onLocationChanged = new Promise(resolve => { const listener = { onLocationChange: () => { this._browser.webProgress.removeProgressListener(listener); resolve(); }, QueryInterface: ChromeUtils.generateQI([ "nsIWebProgressListener", "nsISupportsWeakReference", ]), }; this._browser.webProgress.addProgressListener( listener, Ci.nsIWebProgress.NOTIFY_LOCATION ); }); // Add the addonId in the URL to retrieve this information in other devtools // helpers. The addonId is usually populated in the principal, but this will // not be the case for the fallback window because it is loaded from chrome:// // instead of moz-extension://${addonId} this._browser.setAttribute( "src", `${WEBEXTENSION_FALLBACK_DOC_URL}#${this.addonId}` ); await onLocationChanged; } /** * Note that reloadDescriptor is the common API name for descriptors * which support to be reloaded, while WebExtensionDescriptorActor::reload * is a legacy API which is for instance used from web-ext. * * bypassCache has no impact for addon reloads. */ reloadDescriptor() { return this.reload(); } async reload() { await this.addon.reload(); return {}; } async terminateBackgroundScript() { await lazy.ExtensionParent.DebugUtils.terminateBackgroundScript( this.addonId ); } // This function will be called from RootActor in case that the devtools client // retrieves list of addons with `iconDataURL` option. async loadIconDataURL() { this._iconDataURL = await this.getIconDataURL(); } async getIconDataURL() { if (!this.addon.iconURL) { return null; } const xhr = new XMLHttpRequest(); xhr.responseType = "blob"; xhr.open("GET", this.addon.iconURL, true); if (this.addon.iconURL.toLowerCase().endsWith(".svg")) { // Maybe SVG, thus force to change mime type. xhr.overrideMimeType("image/svg+xml"); } try { const blob = await new Promise((resolve, reject) => { xhr.onload = () => resolve(xhr.response); xhr.onerror = reject; xhr.send(); }); const reader = new FileReader(); return await new Promise((resolve, reject) => { reader.onloadend = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } catch (_) { console.warn(`Failed to create data url from [${this.addon.iconURL}]`); return null; } } // Private Methods _getBackgroundScriptStatus() { const isRunning = lazy.ExtensionParent.DebugUtils.isBackgroundScriptRunning( this.addonId ); // The background script status doesn't apply to this addon (e.g. the addon // type doesn't have any code, like staticthemes/langpacks/dictionaries, or // the extension does not have a background script at all). if (isRunning === undefined) { return undefined; } return isRunning ? BGSCRIPT_STATUSES.RUNNING : BGSCRIPT_STATUSES.STOPPED; } // AddonManagerListener callbacks. onInstalled(addon) { if (addon.id != this.addonId) { return; } // Update the AddonManager's addon object on reload/update. this.addon = addon; } onUninstalled(addon) { if (addon != this.addon) { return; } this.destroy(); } destroy() { lazy.AddonManager.removeAddonListener(this); this.addon = null; if (this.watcher) { this.watcher = null; } if (this._browser) { lazy.ExtensionParent.DebugUtils.releaseExtensionProcessBrowser(this); this._browser = null; } this.emit("descriptor-destroyed"); super.destroy(); } } exports.WebExtensionDescriptorActor = WebExtensionDescriptorActor;