1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 * Target actor for a WebExtension add-on.
10 * This actor extends ParentProcessTargetActor.
12 * See devtools/docs/backend/actor-hierarchy.md for more details.
16 ParentProcessTargetActor,
17 } = require("resource://devtools/server/actors/targets/parent-process.js");
18 const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
20 webExtensionTargetSpec,
21 } = require("resource://devtools/shared/specs/targets/webextension.js");
25 } = require("resource://devtools/server/actors/targets/window-global.js");
27 loader.lazyRequireGetter(
29 "unwrapDebuggerObjectGlobal",
30 "resource://devtools/server/actors/thread.js",
35 ChromeUtils.defineESModuleGetters(lazy, {
36 getAddonIdForWindowGlobal:
37 "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
40 const FALLBACK_DOC_URL =
41 "chrome://devtools/content/shared/webextension-fallback.html";
43 class WebExtensionTargetActor extends ParentProcessTargetActor {
45 * Creates a target actor for debugging all the contexts associated to a target
46 * WebExtensions add-on running in a child extension process. Most of the implementation
47 * is inherited from ParentProcessTargetActor (which inherits most of its implementation
48 * from WindowGlobalTargetActor).
50 * WebExtensionTargetActor is created by a WebExtensionActor counterpart, when its
51 * parent actor's `connect` method has been called (on the listAddons RDP package),
52 * it runs in the same process that the extension is running into (which can be the main
53 * process if the extension is running in non-oop mode, or the child extension process
54 * if the extension is running in oop-mode).
56 * A WebExtensionTargetActor contains all target-scoped actors, like a regular
57 * ParentProcessTargetActor or WindowGlobalTargetActor.
60 * - The add-on actors used to not inherit WindowGlobalTargetActor because of the
61 * different way the add-on APIs where exposed to the add-on itself, and for this reason
62 * the Addon Debugger has only a sub-set of the feature available in the Tab or in the
64 * - In a WebExtensions add-on all the provided contexts (background, popups etc.),
65 * besides the Content Scripts which run in the content process, hooked to an existent
66 * tab, by creating a new WebExtensionActor which inherits from
67 * ParentProcessTargetActor, we can provide a full features Addon Toolbox (which is
68 * basically like a BrowserToolbox which filters the visible sources and frames to the
69 * one that are related to the target add-on).
70 * - When the WebExtensions OOP mode has been introduced, this actor has been refactored
71 * and moved from the main process to the new child extension process.
73 * @param {DevToolsServerConnection} conn
74 * The connection to the client.
75 * @param {nsIMessageSender} chromeGlobal.
76 * The chromeGlobal where this actor has been injected by the
77 * frame-connector.js connectToFrame method.
78 * @param {Object} options
79 * - addonId: {String} the addonId of the target WebExtension.
80 * - addonBrowsingContextGroupId: {String} the BrowsingContextGroupId used by this addon.
81 * - chromeGlobal: {nsIMessageSender} The chromeGlobal where this actor
82 * has been injected by the frame-connector.js connectToFrame method.
83 * - isTopLevelTarget: {Boolean} flag to indicate if this is the top
84 * level target of the DevTools session
85 * - prefix: {String} the custom RDP prefix to use.
86 * - sessionContext Object
87 * The Session Context to help know what is debugged.
88 * See devtools/server/actors/watcher/session-context.js
94 addonBrowsingContextGroupId,
104 customSpec: webExtensionTargetSpec,
107 this.addonId = addonId;
108 this.addonBrowsingContextGroupId = addonBrowsingContextGroupId;
109 this._chromeGlobal = chromeGlobal;
110 this._prefix = prefix;
112 // Expose the BrowsingContext of the fallback document,
113 // which is the one this target actor will always refer to via its form()
114 // and all resources should be related to this one as we currently spawn
115 // only just this one target actor to debug all webextension documents.
116 this.devtoolsSpawnedBrowsingContextForWebExtension =
117 chromeGlobal.browsingContext;
119 // Redefine the messageManager getter to return the chromeGlobal
120 // as the messageManager for this actor (which is the browser XUL
121 // element used by the parent actor running in the main process to
122 // connect to the extension process).
123 Object.defineProperty(this, "messageManager", {
127 return this._chromeGlobal;
131 this._onParentExit = this._onParentExit.bind(this);
133 this._chromeGlobal.addMessageListener(
134 "debug:webext_parent_exit",
138 // Set the consoleAPIListener filtering options
139 // (retrieved and used in the related webconsole child actor).
140 this.consoleAPIListenerOptions = {
141 addonId: this.addonId,
144 // This creates a Debugger instance for debugging all the add-on globals.
145 this.makeDebugger = makeDebugger.bind(null, {
146 findDebuggees: dbg => {
147 return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
149 shouldAddNewGlobalAsDebuggee:
150 this._shouldAddNewGlobalAsDebuggee.bind(this),
153 // NOTE: This is needed to catch in the webextension webconsole all the
154 // errors raised by the WebExtension internals that are not currently
155 // associated with any window.
156 this.isRootActor = true;
158 // Try to discovery an existent extension page to attach (which will provide the initial
159 // URL shown in the window tittle when the addon debugger is opened).
160 const extensionWindow = this._searchForExtensionWindow();
161 this.setDocShell(extensionWindow.docShell);
164 // Override the ParentProcessTargetActor's override in order to only iterate
165 // over the docshells specific to this add-on
167 // Iterate over all top-level windows and all their docshells.
169 for (const window of Services.ww.getWindowEnumerator(null)) {
170 docShells = docShells.concat(getChildDocShells(window.docShell));
172 // Then filter out the ones specific to the add-on
173 return docShells.filter(docShell => {
174 return this.isExtensionWindowDescendent(docShell.domWindow);
179 * Called when the actor is removed from the connection.
182 if (this._chromeGlobal) {
183 const chromeGlobal = this._chromeGlobal;
184 this._chromeGlobal = null;
186 chromeGlobal.removeMessageListener(
187 "debug:webext_parent_exit",
191 chromeGlobal.sendAsyncMessage("debug:webext_child_exit", {
196 if (this.fallbackWindow) {
197 this.fallbackWindow = null;
203 return super.destroy();
208 _searchFallbackWindow() {
209 if (this.fallbackWindow) {
210 // Skip if there is already an existent fallback window.
211 return this.fallbackWindow;
214 // Set and initialize the fallbackWindow (which initially is a empty
215 // about:blank browser), this window is related to a XUL browser element
216 // specifically created for the devtools server and it is never used
217 // or navigated anywhere else.
218 this.fallbackWindow = this._chromeGlobal.content;
220 // Add the addonId in the URL to retrieve this information in other devtools
221 // helpers. The addonId is usually populated in the principal, but this will
222 // not be the case for the fallback window because it is loaded from chrome://
223 // instead of moz-extension://${addonId}
224 this.fallbackWindow.document.location.href = `${FALLBACK_DOC_URL}#${this.addonId}`;
226 return this.fallbackWindow;
229 // Discovery an extension page to use as a default target window.
230 // NOTE: This currently fail to discovery an extension page running in a
231 // windowless browser when running in non-oop mode, and the background page
232 // is set later using _onNewExtensionWindow.
233 _searchForExtensionWindow() {
234 // Looks if there is any top level add-on document:
235 // (we do not want to pass any nested add-on iframe)
236 const docShell = this.docShells.find(d =>
237 this.isTopLevelExtensionWindow(d.domWindow)
240 return docShell.domWindow;
243 return this._searchFallbackWindow();
246 // Customized ParentProcessTargetActor/WindowGlobalTargetActor hooks.
248 _onDocShellCreated(docShell) {
249 // Compare against the BrowsingContext's group ID as the document's principal addonId
250 // won't be set yet for freshly created docshells. It will be later set, when loading the addon URL.
251 // But right now, it is still on the initial about:blank document and the principal isn't related to the add-on.
252 if (docShell.browsingContext.group.id != this.addonBrowsingContextGroupId) {
255 super._onDocShellCreated(docShell);
258 _onDocShellDestroy(docShell) {
259 if (docShell.browsingContext.group.id != this.addonBrowsingContextGroupId) {
262 // Stop watching this docshell (the unwatch() method will check if we
263 // started watching it before).
264 this._unwatchDocShell(docShell);
266 // Let the _onDocShellDestroy notify that the docShell has been destroyed.
267 const webProgress = docShell
268 .QueryInterface(Ci.nsIInterfaceRequestor)
269 .getInterface(Ci.nsIWebProgress);
270 this._notifyDocShellDestroy(webProgress);
272 // If the destroyed docShell:
273 // * was the current docShell,
274 // * the actor is not destroyed,
275 // * isn't the background page, as it means the addon is being shutdown or reloaded
276 // and the target would be replaced by a new one to come, or everything is closing.
277 // => switch to the fallback window
279 !this.isDestroyed() &&
280 docShell == this.docShell &&
281 !docShell.domWindow.location.href.includes(
282 "_generated_background_page.html"
285 this._changeTopLevelDocument(this._searchForExtensionWindow());
289 _onNewExtensionWindow(window) {
290 if (!this.window || this.window === this.fallbackWindow) {
291 this._changeTopLevelDocument(window);
292 // For new extension windows, the BrowsingContext group id might have
293 // changed, for instance when reloading the addon.
294 this.addonBrowsingContextGroupId =
295 window.docShell.browsingContext.group.id;
299 isTopLevelExtensionWindow(window) {
300 const { docShell } = window;
301 const isTopLevel = docShell.sameTypeRootTreeItem == docShell;
302 // Note: We are not using getAddonIdForWindowGlobal here because the
303 // fallback window should not be considered as a top level extension window.
304 return isTopLevel && window.document.nodePrincipal.addonId == this.addonId;
307 isExtensionWindowDescendent(window) {
308 // Check if the source is coming from a descendant docShell of an extension window.
309 // We may have an iframe that loads http content which won't use the add-on principal.
310 const rootWin = window.docShell.sameTypeRootTreeItem.domWindow;
311 const addonId = lazy.getAddonIdForWindowGlobal(rootWin.windowGlobalChild);
312 return addonId == this.addonId;
316 * Return true if the given global is associated with this addon and should be
317 * added as a debuggee, false otherwise.
319 _shouldAddNewGlobalAsDebuggee(newGlobal) {
320 const global = unwrapDebuggerObjectGlobal(newGlobal);
322 if (global instanceof Ci.nsIDOMWindow) {
326 // The global might be a sandbox with a window object in its proto chain. If the
327 // window navigated away since the sandbox was created, it can throw a security
328 // exception during this property check as the sandbox no longer has access to
332 // When `global` is a sandbox it may be a nsIDOMWindow object,
333 // but won't be the real Window object. Retrieve it via document's ownerGlobal.
334 const window = global.document.ownerGlobal;
339 // Change top level document as a simulated frame switching.
340 if (this.isTopLevelExtensionWindow(window)) {
341 this._onNewExtensionWindow(window);
344 return this.isExtensionWindowDescendent(window);
348 // This will fail for non-Sandbox objects, hence the try-catch block.
349 const metadata = Cu.getSandboxMetadata(global);
351 return metadata.addonID === this.addonId;
354 // Unable to retrieve the sandbox metadata.
360 // Handlers for the messages received from the parent actor.
363 if (msg.json.actor !== this.actorID) {
371 exports.WebExtensionTargetActor = WebExtensionTargetActor;