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