Backed out 4 changesets (bug 1651522) for causing dt failures on devtools/shared...
[gecko.git] / devtools / server / actors / targets / webextension.js
blobe94580378a43326ce7ef27491eaac660d6688f18
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/. */
5 "use strict";
7 /*
8  * Target actor for a WebExtension add-on.
9  *
10  * This actor extends ParentProcessTargetActor.
11  *
12  * See devtools/docs/backend/actor-hierarchy.md for more details.
13  */
15 const {
16   ParentProcessTargetActor,
17 } = require("resource://devtools/server/actors/targets/parent-process.js");
18 const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
19 const {
20   webExtensionTargetSpec,
21 } = require("resource://devtools/shared/specs/targets/webextension.js");
23 const {
24   getChildDocShells,
25 } = require("resource://devtools/server/actors/targets/window-global.js");
27 loader.lazyRequireGetter(
28   this,
29   "unwrapDebuggerObjectGlobal",
30   "resource://devtools/server/actors/thread.js",
31   true
34 const lazy = {};
35 ChromeUtils.defineESModuleGetters(lazy, {
36   getAddonIdForWindowGlobal:
37     "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
38 });
40 const FALLBACK_DOC_URL =
41   "chrome://devtools/content/shared/webextension-fallback.html";
43 class WebExtensionTargetActor extends ParentProcessTargetActor {
44   /**
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).
49    *
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).
55    *
56    * A WebExtensionTargetActor contains all target-scoped actors, like a regular
57    * ParentProcessTargetActor or WindowGlobalTargetActor.
58    *
59    * History lecture:
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
63    *   Browser Toolbox.
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.
72    *
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
89    */
90   constructor(
91     conn,
92     {
93       addonId,
94       addonBrowsingContextGroupId,
95       chromeGlobal,
96       isTopLevelTarget,
97       prefix,
98       sessionContext,
99     }
100   ) {
101     super(conn, {
102       isTopLevelTarget,
103       sessionContext,
104       customSpec: webExtensionTargetSpec,
105     });
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", {
124       enumerable: true,
125       configurable: true,
126       get: () => {
127         return this._chromeGlobal;
128       },
129     });
131     this._onParentExit = this._onParentExit.bind(this);
133     this._chromeGlobal.addMessageListener(
134       "debug:webext_parent_exit",
135       this._onParentExit
136     );
138     // Set the consoleAPIListener filtering options
139     // (retrieved and used in the related webconsole child actor).
140     this.consoleAPIListenerOptions = {
141       addonId: this.addonId,
142     };
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);
148       },
149       shouldAddNewGlobalAsDebuggee:
150         this._shouldAddNewGlobalAsDebuggee.bind(this),
151     });
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);
162   }
164   // Override the ParentProcessTargetActor's override in order to only iterate
165   // over the docshells specific to this add-on
166   get docShells() {
167     // Iterate over all top-level windows and all their docshells.
168     let docShells = [];
169     for (const window of Services.ww.getWindowEnumerator(null)) {
170       docShells = docShells.concat(getChildDocShells(window.docShell));
171     }
172     // Then filter out the ones specific to the add-on
173     return docShells.filter(docShell => {
174       return this.isExtensionWindowDescendent(docShell.domWindow);
175     });
176   }
178   /**
179    * Called when the actor is removed from the connection.
180    */
181   destroy() {
182     if (this._chromeGlobal) {
183       const chromeGlobal = this._chromeGlobal;
184       this._chromeGlobal = null;
186       chromeGlobal.removeMessageListener(
187         "debug:webext_parent_exit",
188         this._onParentExit
189       );
191       chromeGlobal.sendAsyncMessage("debug:webext_child_exit", {
192         actor: this.actorID,
193       });
194     }
196     if (this.fallbackWindow) {
197       this.fallbackWindow = null;
198     }
200     this.addon = null;
201     this.addonId = null;
203     return super.destroy();
204   }
206   // Private helpers.
208   _searchFallbackWindow() {
209     if (this.fallbackWindow) {
210       // Skip if there is already an existent fallback window.
211       return this.fallbackWindow;
212     }
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;
227   }
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)
238     );
239     if (docShell) {
240       return docShell.domWindow;
241     }
243     return this._searchFallbackWindow();
244   }
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) {
253       return;
254     }
255     super._onDocShellCreated(docShell);
256   }
258   _onDocShellDestroy(docShell) {
259     if (docShell.browsingContext.group.id != this.addonBrowsingContextGroupId) {
260       return;
261     }
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
278     if (
279       !this.isDestroyed() &&
280       docShell == this.docShell &&
281       !docShell.domWindow.location.href.includes(
282         "_generated_background_page.html"
283       )
284     ) {
285       this._changeTopLevelDocument(this._searchForExtensionWindow());
286     }
287   }
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;
296     }
297   }
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;
305   }
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;
313   }
315   /**
316    * Return true if the given global is associated with this addon and should be
317    * added as a debuggee, false otherwise.
318    */
319   _shouldAddNewGlobalAsDebuggee(newGlobal) {
320     const global = unwrapDebuggerObjectGlobal(newGlobal);
322     if (global instanceof Ci.nsIDOMWindow) {
323       try {
324         global.document;
325       } catch (e) {
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
329         // its own proto.
330         return false;
331       }
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;
335       if (!window) {
336         return false;
337       }
339       // Change top level document as a simulated frame switching.
340       if (this.isTopLevelExtensionWindow(window)) {
341         this._onNewExtensionWindow(window);
342       }
344       return this.isExtensionWindowDescendent(window);
345     }
347     try {
348       // This will fail for non-Sandbox objects, hence the try-catch block.
349       const metadata = Cu.getSandboxMetadata(global);
350       if (metadata) {
351         return metadata.addonID === this.addonId;
352       }
353     } catch (e) {
354       // Unable to retrieve the sandbox metadata.
355     }
357     return false;
358   }
360   // Handlers for the messages received from the parent actor.
362   _onParentExit(msg) {
363     if (msg.json.actor !== this.actorID) {
364       return;
365     }
367     this.destroy();
368   }
371 exports.WebExtensionTargetActor = WebExtensionTargetActor;