summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/webextension.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/webextension.js')
-rw-r--r--devtools/server/actors/webextension.js333
1 files changed, 333 insertions, 0 deletions
diff --git a/devtools/server/actors/webextension.js b/devtools/server/actors/webextension.js
new file mode 100644
index 000000000..0e83fc999
--- /dev/null
+++ b/devtools/server/actors/webextension.js
@@ -0,0 +1,333 @@
+/* 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";
+
+const { Ci, Cu } = require("chrome");
+const Services = require("Services");
+const { ChromeActor } = require("./chrome");
+const makeDebugger = require("./utils/make-debugger");
+
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { assert } = DevToolsUtils;
+
+loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
+loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
+
+loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+loader.lazyImporter(this, "XPIProvider", "resource://gre/modules/addons/XPIProvider.jsm");
+
+const FALLBACK_DOC_MESSAGE = "Your addon does not have any document opened yet.";
+
+/**
+ * Creates a TabActor for debugging all the contexts associated to a target WebExtensions
+ * add-on.
+ * Most of the implementation is inherited from ChromeActor (which inherits most of its
+ * implementation from TabActor).
+ * WebExtensionActor is a child of RootActor, it can be retrieved via
+ * RootActor.listAddons request.
+ * WebExtensionActor exposes all tab actors via its form() request, like TabActor.
+ *
+ * History lecture:
+ * The add-on actors used to not inherit TabActor because of the different way the
+ * add-on APIs where exposed to the add-on itself, and for this reason the Addon Debugger
+ * has only a sub-set of the feature available in the Tab or in the Browser Toolbox.
+ * In a WebExtensions add-on all the provided contexts (background and popup pages etc.),
+ * besides the Content Scripts which run in the content process, hooked to an existent
+ * tab, by creating a new WebExtensionActor which inherits from ChromeActor, we can
+ * provide a full features Addon Toolbox (which is basically like a BrowserToolbox which
+ * filters the visible sources and frames to the one that are related to the target
+ * add-on).
+ *
+ * @param conn DebuggerServerConnection
+ * The connection to the client.
+ * @param addon AddonWrapper
+ * The target addon.
+ */
+function WebExtensionActor(conn, addon) {
+ ChromeActor.call(this, conn);
+
+ this.id = addon.id;
+ this.addon = addon;
+
+ // Bind the _allowSource helper to this, it is used in the
+ // TabActor to lazily create the TabSources instance.
+ this._allowSource = this._allowSource.bind(this);
+
+ // Set the consoleAPIListener filtering options
+ // (retrieved and used in the related webconsole child actor).
+ this.consoleAPIListenerOptions = {
+ addonId: addon.id,
+ };
+
+ // This creates a Debugger instance for debugging all the add-on globals.
+ this.makeDebugger = makeDebugger.bind(null, {
+ findDebuggees: dbg => {
+ return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
+ },
+ shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee.bind(this),
+ });
+
+ // Discover the preferred debug global for the target addon
+ this.preferredTargetWindow = null;
+ this._findAddonPreferredTargetWindow();
+
+ AddonManager.addAddonListener(this);
+}
+exports.WebExtensionActor = WebExtensionActor;
+
+WebExtensionActor.prototype = Object.create(ChromeActor.prototype);
+
+WebExtensionActor.prototype.actorPrefix = "webExtension";
+WebExtensionActor.prototype.constructor = WebExtensionActor;
+
+// NOTE: This is needed to catch in the webextension webconsole all the
+// errors raised by the WebExtension internals that are not currently
+// associated with any window.
+WebExtensionActor.prototype.isRootActor = true;
+
+WebExtensionActor.prototype.form = function () {
+ assert(this.actorID, "addon should have an actorID.");
+
+ let baseForm = ChromeActor.prototype.form.call(this);
+
+ return Object.assign(baseForm, {
+ actor: this.actorID,
+ id: this.id,
+ name: this.addon.name,
+ url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined,
+ iconURL: this.addon.iconURL,
+ debuggable: this.addon.isDebuggable,
+ temporarilyInstalled: this.addon.temporarilyInstalled,
+ isWebExtension: this.addon.isWebExtension,
+ });
+};
+
+WebExtensionActor.prototype._attach = function () {
+ // NOTE: we need to be sure that `this.window` can return a
+ // window before calling the ChromeActor.onAttach, or the TabActor
+ // will not be subscribed to the child doc shell updates.
+
+ // If a preferredTargetWindow exists, set it as the target for this actor
+ // when the client request to attach this actor.
+ if (this.preferredTargetWindow) {
+ this._setWindow(this.preferredTargetWindow);
+ } else {
+ this._createFallbackWindow();
+ }
+
+ // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell
+ ChromeActor.prototype._attach.apply(this);
+};
+
+WebExtensionActor.prototype._detach = function () {
+ this._destroyFallbackWindow();
+
+ // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners.
+ ChromeActor.prototype._detach.apply(this);
+};
+
+/**
+ * Called when the actor is removed from the connection.
+ */
+WebExtensionActor.prototype.exit = function () {
+ AddonManager.removeAddonListener(this);
+
+ this.preferredTargetWindow = null;
+ this.addon = null;
+ this.id = null;
+
+ return ChromeActor.prototype.exit.apply(this);
+};
+
+// Addon Specific Remote Debugging requestTypes and methods.
+
+/**
+ * Reloads the addon.
+ */
+WebExtensionActor.prototype.onReload = function () {
+ return this.addon.reload()
+ .then(() => {
+ // send an empty response
+ return {};
+ });
+};
+
+/**
+ * Set the preferred global for the add-on (called from the AddonManager).
+ */
+WebExtensionActor.prototype.setOptions = function (addonOptions) {
+ if ("global" in addonOptions) {
+ // Set the proposed debug global as the preferred target window
+ // (the actor will eventually set it as the target once it is attached)
+ this.preferredTargetWindow = addonOptions.global;
+ }
+};
+
+// AddonManagerListener callbacks.
+
+WebExtensionActor.prototype.onInstalled = function (addon) {
+ if (addon.id != this.id) {
+ return;
+ }
+
+ // Update the AddonManager's addon object on reload/update.
+ this.addon = addon;
+};
+
+WebExtensionActor.prototype.onUninstalled = function (addon) {
+ if (addon != this.addon) {
+ return;
+ }
+
+ this.exit();
+};
+
+WebExtensionActor.prototype.onPropertyChanged = function (addon, changedPropNames) {
+ if (addon != this.addon) {
+ return;
+ }
+
+ // Refresh the preferred debug global on disabled/reloaded/upgraded addon.
+ if (changedPropNames.includes("debugGlobal")) {
+ this._findAddonPreferredTargetWindow();
+ }
+};
+
+// Private helpers
+
+WebExtensionActor.prototype._createFallbackWindow = function () {
+ if (this.fallbackWindow) {
+ // Skip if there is already an existent fallback window.
+ return;
+ }
+
+ // Create an empty hidden window as a fallback (e.g. the background page could be
+ // not defined for the target add-on or not yet when the actor instance has been
+ // created).
+ this.fallbackWebNav = Services.appShell.createWindowlessBrowser(true);
+ this.fallbackWebNav.loadURI(
+ `data:text/html;charset=utf-8,${FALLBACK_DOC_MESSAGE}`,
+ 0, null, null, null
+ );
+
+ this.fallbackDocShell = this.fallbackWebNav
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+
+ Object.defineProperty(this, "docShell", {
+ value: this.fallbackDocShell,
+ configurable: true
+ });
+
+ // Save the reference to the fallback DOMWindow
+ this.fallbackWindow = this.fallbackDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+};
+
+WebExtensionActor.prototype._destroyFallbackWindow = function () {
+ if (this.fallbackWebNav) {
+ // Explicitly close the fallback windowless browser to prevent it to leak
+ // (and to prevent it to freeze devtools xpcshell tests).
+ this.fallbackWebNav.loadURI("about:blank", 0, null, null, null);
+ this.fallbackWebNav.close();
+
+ this.fallbackWebNav = null;
+ this.fallbackWindow = null;
+ }
+};
+
+/**
+ * Discover the preferred debug global and switch to it if the addon has been attached.
+ */
+WebExtensionActor.prototype._findAddonPreferredTargetWindow = function () {
+ return new Promise(resolve => {
+ let activeAddon = XPIProvider.activeAddons.get(this.id);
+
+ if (!activeAddon) {
+ // The addon is not active, the background page is going to be destroyed,
+ // navigate to the fallback window (if it already exists).
+ resolve(null);
+ } else {
+ AddonManager.getAddonByInstanceID(activeAddon.instanceID)
+ .then(privateWrapper => {
+ let targetWindow = privateWrapper.getDebugGlobal();
+
+ // Do not use the preferred global if it is not a DOMWindow as expected.
+ if (!(targetWindow instanceof Ci.nsIDOMWindow)) {
+ targetWindow = null;
+ }
+
+ resolve(targetWindow);
+ });
+ }
+ }).then(preferredTargetWindow => {
+ this.preferredTargetWindow = preferredTargetWindow;
+
+ if (!preferredTargetWindow) {
+ // Create a fallback window if no preferred target window has been found.
+ this._createFallbackWindow();
+ } else if (this.attached) {
+ // Change the top level document if the actor is already attached.
+ this._changeTopLevelDocument(preferredTargetWindow);
+ }
+ });
+};
+
+/**
+ * Return an array of the json details related to an array/iterator of docShells.
+ */
+WebExtensionActor.prototype._docShellsToWindows = function (docshells) {
+ return ChromeActor.prototype._docShellsToWindows.call(this, docshells)
+ .filter(windowDetails => {
+ // filter the docShells based on the addon id
+ return windowDetails.addonID == this.id;
+ });
+};
+
+/**
+ * Return true if the given source is associated with this addon and should be
+ * added to the visible sources (retrieved and used by the webbrowser actor module).
+ */
+WebExtensionActor.prototype._allowSource = function (source) {
+ try {
+ let uri = Services.io.newURI(source.url, null, null);
+ let addonID = mapURIToAddonID(uri);
+
+ return addonID == this.id;
+ } catch (e) {
+ return false;
+ }
+};
+
+/**
+ * Return true if the given global is associated with this addon and should be
+ * added as a debuggee, false otherwise.
+ */
+WebExtensionActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal) {
+ const global = unwrapDebuggerObjectGlobal(newGlobal);
+
+ if (global instanceof Ci.nsIDOMWindow) {
+ return global.document.nodePrincipal.originAttributes.addonId == this.id;
+ }
+
+ try {
+ // This will fail for non-Sandbox objects, hence the try-catch block.
+ let metadata = Cu.getSandboxMetadata(global);
+ if (metadata) {
+ return metadata.addonID === this.id;
+ }
+ } catch (e) {
+ // Unable to retrieve the sandbox metadata.
+ }
+
+ return false;
+};
+
+/**
+ * Override WebExtensionActor requestTypes:
+ * - redefined `reload`, which should reload the target addon
+ * (instead of the entire browser as the regular ChromeActor does).
+ */
+WebExtensionActor.prototype.requestTypes.reload = WebExtensionActor.prototype.onReload;