380 lines
14 KiB
JavaScript
380 lines
14 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";
|
|
|
|
/*
|
|
* Target actor for a WebExtension add-on.
|
|
*
|
|
* This actor extends ParentProcessTargetActor.
|
|
*
|
|
* See devtools/docs/backend/actor-hierarchy.md for more details.
|
|
*/
|
|
|
|
const {
|
|
ParentProcessTargetActor,
|
|
} = 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 {
|
|
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";
|
|
|
|
class WebExtensionTargetActor extends ParentProcessTargetActor {
|
|
/**
|
|
* 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
|
|
*/
|
|
constructor(
|
|
conn,
|
|
{
|
|
addonId,
|
|
addonBrowsingContextGroupId,
|
|
chromeGlobal,
|
|
isTopLevelTarget,
|
|
prefix,
|
|
sessionContext,
|
|
}
|
|
) {
|
|
super(conn, {
|
|
isTopLevelTarget,
|
|
sessionContext,
|
|
customSpec: webExtensionTargetSpec,
|
|
});
|
|
|
|
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;
|
|
|
|
// 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)
|
|
.map(g => g.unsafeDereference());
|
|
},
|
|
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.
|
|
this.isRootActor = true;
|
|
|
|
// 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();
|
|
this.setDocShell(extensionWindow.docShell);
|
|
|
|
// `setDocShell` will force the instantiation of the thread actor.
|
|
// We now have to initialize it in order to listen for new global
|
|
// which allows to properly detect addon reload via _shouldAddNewGlobalAsDebuggee
|
|
// which may call _onNewExtensionWindow.
|
|
this.threadActor.attach({});
|
|
}
|
|
|
|
// Override the ParentProcessTargetActor's override in order to only iterate
|
|
// over the docshells specific to this add-on
|
|
get docShells() {
|
|
// 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.
|
|
*/
|
|
destroy() {
|
|
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 super.destroy();
|
|
}
|
|
|
|
// Private helpers.
|
|
|
|
_searchFallbackWindow() {
|
|
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.
|
|
_searchForExtensionWindow() {
|
|
// 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.
|
|
|
|
_onDocShellCreated(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;
|
|
}
|
|
super._onDocShellCreated(docShell);
|
|
}
|
|
|
|
_onDocShellDestroy(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,
|
|
// * the actor is not destroyed,
|
|
// * isn't the background page, as it means the addon is being shutdown or reloaded
|
|
// and the target would be replaced by a new one to come, or everything is closing.
|
|
// => switch to the fallback window
|
|
if (
|
|
!this.isDestroyed() &&
|
|
docShell == this.docShell &&
|
|
!docShell.domWindow.location.href.includes(
|
|
"_generated_background_page.html"
|
|
)
|
|
) {
|
|
this._changeTopLevelDocument(this._searchForExtensionWindow());
|
|
}
|
|
}
|
|
|
|
_onNewExtensionWindow(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;
|
|
}
|
|
}
|
|
|
|
isTopLevelExtensionWindow(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;
|
|
}
|
|
|
|
isExtensionWindowDescendent(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.
|
|
*/
|
|
_shouldAddNewGlobalAsDebuggee(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.
|
|
|
|
_onParentExit(msg) {
|
|
if (msg.json.actor !== this.actorID) {
|
|
return;
|
|
}
|
|
|
|
this.destroy();
|
|
}
|
|
}
|
|
|
|
exports.WebExtensionTargetActor = WebExtensionTargetActor;
|