diff options
Diffstat (limited to 'devtools/server/actors/targets/webextension.js')
-rw-r--r-- | devtools/server/actors/targets/webextension.js | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/devtools/server/actors/targets/webextension.js b/devtools/server/actors/targets/webextension.js new file mode 100644 index 0000000000..24950941fc --- /dev/null +++ b/devtools/server/actors/targets/webextension.js @@ -0,0 +1,376 @@ +/* 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"; + +/* + * Target actor for a WebExtension add-on. + * + * This actor extends ParentProcessTargetActor. + * + * See devtools/docs/backend/actor-hierarchy.md for more details. + */ + +const { extend } = require("resource://devtools/shared/extend.js"); + +const { + ParentProcessTargetActor, + parentProcessTargetPrototype, +} = require("resource://devtools/server/actors/targets/parent-process.js"); +const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js"); +const { + webExtensionTargetSpec, +} = require("resource://devtools/shared/specs/targets/webextension.js"); + +const Targets = require("resource://devtools/server/actors/targets/index.js"); +const TargetActorMixin = require("resource://devtools/server/actors/targets/target-actor-mixin.js"); +const { + getChildDocShells, +} = require("resource://devtools/server/actors/targets/window-global.js"); + +loader.lazyRequireGetter( + this, + "unwrapDebuggerObjectGlobal", + "resource://devtools/server/actors/thread.js", + true +); + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + getAddonIdForWindowGlobal: + "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", +}); + +const FALLBACK_DOC_URL = + "chrome://devtools/content/shared/webextension-fallback.html"; + +/** + * Protocol.js expects only the prototype object, and does not maintain the prototype + * chain when it constructs the ActorClass. For this reason we are using `extend` to + * maintain the properties of WindowGlobalTargetActor.prototype + */ +const webExtensionTargetPrototype = extend({}, parentProcessTargetPrototype); + +/** + * Creates a target actor for debugging all the contexts associated to a target + * WebExtensions add-on running in a child extension process. Most of the implementation + * is inherited from ParentProcessTargetActor (which inherits most of its implementation + * from WindowGlobalTargetActor). + * + * WebExtensionTargetActor is created by a WebExtensionActor counterpart, when its + * parent actor's `connect` method has been called (on the listAddons RDP package), + * it runs in the same process that the extension is running into (which can be the main + * process if the extension is running in non-oop mode, or the child extension process + * if the extension is running in oop-mode). + * + * A WebExtensionTargetActor contains all target-scoped actors, like a regular + * ParentProcessTargetActor or WindowGlobalTargetActor. + * + * History lecture: + * - The add-on actors used to not inherit WindowGlobalTargetActor 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, popups etc.), + * besides the Content Scripts which run in the content process, hooked to an existent + * tab, by creating a new WebExtensionActor which inherits from + * ParentProcessTargetActor, 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). + * - When the WebExtensions OOP mode has been introduced, this actor has been refactored + * and moved from the main process to the new child extension process. + * + * @param {DevToolsServerConnection} conn + * The connection to the client. + * @param {nsIMessageSender} chromeGlobal. + * The chromeGlobal where this actor has been injected by the + * frame-connector.js connectToFrame method. + * @param {Object} options + * - addonId: {String} the addonId of the target WebExtension. + * - addonBrowsingContextGroupId: {String} the BrowsingContextGroupId used by this addon. + * - chromeGlobal: {nsIMessageSender} The chromeGlobal where this actor + * has been injected by the frame-connector.js connectToFrame method. + * - isTopLevelTarget: {Boolean} flag to indicate if this is the top + * level target of the DevTools session + * - prefix: {String} the custom RDP prefix to use. + * - sessionContext Object + * The Session Context to help know what is debugged. + * See devtools/server/actors/watcher/session-context.js + */ +webExtensionTargetPrototype.initialize = function( + conn, + { + addonId, + addonBrowsingContextGroupId, + chromeGlobal, + isTopLevelTarget, + prefix, + sessionContext, + } +) { + this.addonId = addonId; + this.addonBrowsingContextGroupId = addonBrowsingContextGroupId; + this._chromeGlobal = chromeGlobal; + this._prefix = prefix; + + // Expose the BrowsingContext of the fallback document, + // which is the one this target actor will always refer to via its form() + // and all resources should be related to this one as we currently spawn + // only just this one target actor to debug all webextension documents. + this.devtoolsSpawnedBrowsingContextForWebExtension = + chromeGlobal.browsingContext; + + // Try to discovery an existent extension page to attach (which will provide the initial + // URL shown in the window tittle when the addon debugger is opened). + const extensionWindow = this._searchForExtensionWindow(); + + parentProcessTargetPrototype.initialize.call(this, conn, { + isTopLevelTarget, + window: extensionWindow, + sessionContext, + }); + + // Redefine the messageManager getter to return the chromeGlobal + // as the messageManager for this actor (which is the browser XUL + // element used by the parent actor running in the main process to + // connect to the extension process). + Object.defineProperty(this, "messageManager", { + enumerable: true, + configurable: true, + get: () => { + return this._chromeGlobal; + }, + }); + + this._onParentExit = this._onParentExit.bind(this); + + this._chromeGlobal.addMessageListener( + "debug:webext_parent_exit", + this._onParentExit + ); + + // Set the consoleAPIListener filtering options + // (retrieved and used in the related webconsole child actor). + this.consoleAPIListenerOptions = { + addonId: this.addonId, + }; + + // 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), + }); +}; + +// 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. +webExtensionTargetPrototype.isRootActor = true; + +// Override the ParentProcessTargetActor's override in order to only iterate +// over the docshells specific to this add-on +Object.defineProperty(webExtensionTargetPrototype, "docShells", { + get() { + // Iterate over all top-level windows and all their docshells. + let docShells = []; + for (const window of Services.ww.getWindowEnumerator(null)) { + docShells = docShells.concat(getChildDocShells(window.docShell)); + } + // Then filter out the ones specific to the add-on + return docShells.filter(docShell => { + return this.isExtensionWindowDescendent(docShell.domWindow); + }); + }, +}); + +/** + * Called when the actor is removed from the connection. + */ +webExtensionTargetPrototype.destroy = function() { + if (this._chromeGlobal) { + const chromeGlobal = this._chromeGlobal; + this._chromeGlobal = null; + + chromeGlobal.removeMessageListener( + "debug:webext_parent_exit", + this._onParentExit + ); + + chromeGlobal.sendAsyncMessage("debug:webext_child_exit", { + actor: this.actorID, + }); + } + + if (this.fallbackWindow) { + this.fallbackWindow = null; + } + + this.addon = null; + this.addonId = null; + + return ParentProcessTargetActor.prototype.destroy.apply(this); +}; + +// Private helpers. + +webExtensionTargetPrototype._searchFallbackWindow = function() { + if (this.fallbackWindow) { + // Skip if there is already an existent fallback window. + return this.fallbackWindow; + } + + // Set and initialize the fallbackWindow (which initially is a empty + // about:blank browser), this window is related to a XUL browser element + // specifically created for the devtools server and it is never used + // or navigated anywhere else. + this.fallbackWindow = this._chromeGlobal.content; + + // 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.fallbackWindow.document.location.href = `${FALLBACK_DOC_URL}#${this.addonId}`; + + return this.fallbackWindow; +}; + +// Discovery an extension page to use as a default target window. +// NOTE: This currently fail to discovery an extension page running in a +// windowless browser when running in non-oop mode, and the background page +// is set later using _onNewExtensionWindow. +webExtensionTargetPrototype._searchForExtensionWindow = function() { + // Looks if there is any top level add-on document: + // (we do not want to pass any nested add-on iframe) + const docShell = this.docShells.find(d => + this.isTopLevelExtensionWindow(d.domWindow) + ); + if (docShell) { + return docShell.domWindow; + } + + return this._searchFallbackWindow(); +}; + +// Customized ParentProcessTargetActor/WindowGlobalTargetActor hooks. + +webExtensionTargetPrototype._onDocShellCreated = function(docShell) { + // Compare against the BrowsingContext's group ID as the document's principal addonId + // won't be set yet for freshly created docshells. It will be later set, when loading the addon URL. + // But right now, it is still on the initial about:blank document and the principal isn't related to the add-on. + if (docShell.browsingContext.group.id != this.addonBrowsingContextGroupId) { + return; + } + ParentProcessTargetActor.prototype._onDocShellCreated.call(this, docShell); +}; + +webExtensionTargetPrototype._onDocShellDestroy = function(docShell) { + if (docShell.browsingContext.group.id != this.addonBrowsingContextGroupId) { + return; + } + // Stop watching this docshell (the unwatch() method will check if we + // started watching it before). + this._unwatchDocShell(docShell); + + // Let the _onDocShellDestroy notify that the docShell has been destroyed. + const webProgress = docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + this._notifyDocShellDestroy(webProgress); + + // If the destroyed docShell was the current docShell and the actor is + // not destroyed, switch to the fallback window + if (!this.isDestroyed() && docShell == this.docShell) { + this._changeTopLevelDocument(this._searchForExtensionWindow()); + } +}; + +webExtensionTargetPrototype._onNewExtensionWindow = function(window) { + if (!this.window || this.window === this.fallbackWindow) { + this._changeTopLevelDocument(window); + // For new extension windows, the BrowsingContext group id might have + // changed, for instance when reloading the addon. + this.addonBrowsingContextGroupId = window.docShell.browsingContext.group.id; + } +}; + +webExtensionTargetPrototype.isTopLevelExtensionWindow = function(window) { + const { docShell } = window; + const isTopLevel = docShell.sameTypeRootTreeItem == docShell; + // Note: We are not using getAddonIdForWindowGlobal here because the + // fallback window should not be considered as a top level extension window. + return isTopLevel && window.document.nodePrincipal.addonId == this.addonId; +}; + +webExtensionTargetPrototype.isExtensionWindowDescendent = function(window) { + // Check if the source is coming from a descendant docShell of an extension window. + // We may have an iframe that loads http content which won't use the add-on principal. + const rootWin = window.docShell.sameTypeRootTreeItem.domWindow; + const addonId = lazy.getAddonIdForWindowGlobal(rootWin.windowGlobalChild); + return addonId == this.addonId; +}; + +/** + * Return true if the given global is associated with this addon and should be + * added as a debuggee, false otherwise. + */ +webExtensionTargetPrototype._shouldAddNewGlobalAsDebuggee = function( + newGlobal +) { + const global = unwrapDebuggerObjectGlobal(newGlobal); + + if (global instanceof Ci.nsIDOMWindow) { + try { + global.document; + } catch (e) { + // The global might be a sandbox with a window object in its proto chain. If the + // window navigated away since the sandbox was created, it can throw a security + // exception during this property check as the sandbox no longer has access to + // its own proto. + return false; + } + // When `global` is a sandbox it may be a nsIDOMWindow object, + // but won't be the real Window object. Retrieve it via document's ownerGlobal. + const window = global.document.ownerGlobal; + if (!window) { + return false; + } + + // Change top level document as a simulated frame switching. + if (this.isTopLevelExtensionWindow(window)) { + this._onNewExtensionWindow(window); + } + + return this.isExtensionWindowDescendent(window); + } + + try { + // This will fail for non-Sandbox objects, hence the try-catch block. + const metadata = Cu.getSandboxMetadata(global); + if (metadata) { + return metadata.addonID === this.addonId; + } + } catch (e) { + // Unable to retrieve the sandbox metadata. + } + + return false; +}; + +// Handlers for the messages received from the parent actor. + +webExtensionTargetPrototype._onParentExit = function(msg) { + if (msg.json.actor !== this.actorID) { + return; + } + + this.destroy(); +}; + +exports.WebExtensionTargetActor = TargetActorMixin( + Targets.TYPES.FRAME, + webExtensionTargetSpec, + webExtensionTargetPrototype +); |