1
0
Fork 0
firefox/devtools/server/actors/descriptors/webextension.js
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

320 lines
10 KiB
JavaScript

/* 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 <browser> 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;