webextension.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  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/. */
  4. "use strict";
  5. const { Ci, Cu } = require("chrome");
  6. const Services = require("Services");
  7. const { ChromeActor } = require("./chrome");
  8. const makeDebugger = require("./utils/make-debugger");
  9. var DevToolsUtils = require("devtools/shared/DevToolsUtils");
  10. var { assert } = DevToolsUtils;
  11. loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
  12. loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
  13. loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
  14. loader.lazyImporter(this, "XPIProvider", "resource://gre/modules/addons/XPIProvider.jsm");
  15. const FALLBACK_DOC_MESSAGE = "Your addon does not have any document opened yet.";
  16. /**
  17. * Creates a TabActor for debugging all the contexts associated to a target WebExtensions
  18. * add-on.
  19. * Most of the implementation is inherited from ChromeActor (which inherits most of its
  20. * implementation from TabActor).
  21. * WebExtensionActor is a child of RootActor, it can be retrieved via
  22. * RootActor.listAddons request.
  23. * WebExtensionActor exposes all tab actors via its form() request, like TabActor.
  24. *
  25. * History lecture:
  26. * The add-on actors used to not inherit TabActor because of the different way the
  27. * add-on APIs where exposed to the add-on itself, and for this reason the Addon Debugger
  28. * has only a sub-set of the feature available in the Tab or in the Browser Toolbox.
  29. * In a WebExtensions add-on all the provided contexts (background and popup pages etc.),
  30. * besides the Content Scripts which run in the content process, hooked to an existent
  31. * tab, by creating a new WebExtensionActor which inherits from ChromeActor, we can
  32. * provide a full features Addon Toolbox (which is basically like a BrowserToolbox which
  33. * filters the visible sources and frames to the one that are related to the target
  34. * add-on).
  35. *
  36. * @param conn DebuggerServerConnection
  37. * The connection to the client.
  38. * @param addon AddonWrapper
  39. * The target addon.
  40. */
  41. function WebExtensionActor(conn, addon) {
  42. ChromeActor.call(this, conn);
  43. this.id = addon.id;
  44. this.addon = addon;
  45. // Bind the _allowSource helper to this, it is used in the
  46. // TabActor to lazily create the TabSources instance.
  47. this._allowSource = this._allowSource.bind(this);
  48. // Set the consoleAPIListener filtering options
  49. // (retrieved and used in the related webconsole child actor).
  50. this.consoleAPIListenerOptions = {
  51. addonId: addon.id,
  52. };
  53. // This creates a Debugger instance for debugging all the add-on globals.
  54. this.makeDebugger = makeDebugger.bind(null, {
  55. findDebuggees: dbg => {
  56. return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
  57. },
  58. shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee.bind(this),
  59. });
  60. // Discover the preferred debug global for the target addon
  61. this.preferredTargetWindow = null;
  62. this._findAddonPreferredTargetWindow();
  63. AddonManager.addAddonListener(this);
  64. }
  65. exports.WebExtensionActor = WebExtensionActor;
  66. WebExtensionActor.prototype = Object.create(ChromeActor.prototype);
  67. WebExtensionActor.prototype.actorPrefix = "webExtension";
  68. WebExtensionActor.prototype.constructor = WebExtensionActor;
  69. // NOTE: This is needed to catch in the webextension webconsole all the
  70. // errors raised by the WebExtension internals that are not currently
  71. // associated with any window.
  72. WebExtensionActor.prototype.isRootActor = true;
  73. WebExtensionActor.prototype.form = function () {
  74. assert(this.actorID, "addon should have an actorID.");
  75. let baseForm = ChromeActor.prototype.form.call(this);
  76. return Object.assign(baseForm, {
  77. actor: this.actorID,
  78. id: this.id,
  79. name: this.addon.name,
  80. url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined,
  81. iconURL: this.addon.iconURL,
  82. debuggable: this.addon.isDebuggable,
  83. temporarilyInstalled: this.addon.temporarilyInstalled,
  84. isWebExtension: this.addon.isWebExtension,
  85. });
  86. };
  87. WebExtensionActor.prototype._attach = function () {
  88. // NOTE: we need to be sure that `this.window` can return a
  89. // window before calling the ChromeActor.onAttach, or the TabActor
  90. // will not be subscribed to the child doc shell updates.
  91. // If a preferredTargetWindow exists, set it as the target for this actor
  92. // when the client request to attach this actor.
  93. if (this.preferredTargetWindow) {
  94. this._setWindow(this.preferredTargetWindow);
  95. } else {
  96. this._createFallbackWindow();
  97. }
  98. // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell
  99. ChromeActor.prototype._attach.apply(this);
  100. };
  101. WebExtensionActor.prototype._detach = function () {
  102. this._destroyFallbackWindow();
  103. // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners.
  104. ChromeActor.prototype._detach.apply(this);
  105. };
  106. /**
  107. * Called when the actor is removed from the connection.
  108. */
  109. WebExtensionActor.prototype.exit = function () {
  110. AddonManager.removeAddonListener(this);
  111. this.preferredTargetWindow = null;
  112. this.addon = null;
  113. this.id = null;
  114. return ChromeActor.prototype.exit.apply(this);
  115. };
  116. // Addon Specific Remote Debugging requestTypes and methods.
  117. /**
  118. * Reloads the addon.
  119. */
  120. WebExtensionActor.prototype.onReload = function () {
  121. return this.addon.reload()
  122. .then(() => {
  123. // send an empty response
  124. return {};
  125. });
  126. };
  127. /**
  128. * Set the preferred global for the add-on (called from the AddonManager).
  129. */
  130. WebExtensionActor.prototype.setOptions = function (addonOptions) {
  131. if ("global" in addonOptions) {
  132. // Set the proposed debug global as the preferred target window
  133. // (the actor will eventually set it as the target once it is attached)
  134. this.preferredTargetWindow = addonOptions.global;
  135. }
  136. };
  137. // AddonManagerListener callbacks.
  138. WebExtensionActor.prototype.onInstalled = function (addon) {
  139. if (addon.id != this.id) {
  140. return;
  141. }
  142. // Update the AddonManager's addon object on reload/update.
  143. this.addon = addon;
  144. };
  145. WebExtensionActor.prototype.onUninstalled = function (addon) {
  146. if (addon != this.addon) {
  147. return;
  148. }
  149. this.exit();
  150. };
  151. WebExtensionActor.prototype.onPropertyChanged = function (addon, changedPropNames) {
  152. if (addon != this.addon) {
  153. return;
  154. }
  155. // Refresh the preferred debug global on disabled/reloaded/upgraded addon.
  156. if (changedPropNames.includes("debugGlobal")) {
  157. this._findAddonPreferredTargetWindow();
  158. }
  159. };
  160. // Private helpers
  161. WebExtensionActor.prototype._createFallbackWindow = function () {
  162. if (this.fallbackWindow) {
  163. // Skip if there is already an existent fallback window.
  164. return;
  165. }
  166. // Create an empty hidden window as a fallback (e.g. the background page could be
  167. // not defined for the target add-on or not yet when the actor instance has been
  168. // created).
  169. this.fallbackWebNav = Services.appShell.createWindowlessBrowser(true);
  170. this.fallbackWebNav.loadURI(
  171. `data:text/html;charset=utf-8,${FALLBACK_DOC_MESSAGE}`,
  172. 0, null, null, null
  173. );
  174. this.fallbackDocShell = this.fallbackWebNav
  175. .QueryInterface(Ci.nsIInterfaceRequestor)
  176. .getInterface(Ci.nsIDocShell);
  177. Object.defineProperty(this, "docShell", {
  178. value: this.fallbackDocShell,
  179. configurable: true
  180. });
  181. // Save the reference to the fallback DOMWindow
  182. this.fallbackWindow = this.fallbackDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
  183. .getInterface(Ci.nsIDOMWindow);
  184. };
  185. WebExtensionActor.prototype._destroyFallbackWindow = function () {
  186. if (this.fallbackWebNav) {
  187. // Explicitly close the fallback windowless browser to prevent it to leak
  188. // (and to prevent it to freeze devtools xpcshell tests).
  189. this.fallbackWebNav.loadURI("about:blank", 0, null, null, null);
  190. this.fallbackWebNav.close();
  191. this.fallbackWebNav = null;
  192. this.fallbackWindow = null;
  193. }
  194. };
  195. /**
  196. * Discover the preferred debug global and switch to it if the addon has been attached.
  197. */
  198. WebExtensionActor.prototype._findAddonPreferredTargetWindow = function () {
  199. return new Promise(resolve => {
  200. let activeAddon = XPIProvider.activeAddons.get(this.id);
  201. if (!activeAddon) {
  202. // The addon is not active, the background page is going to be destroyed,
  203. // navigate to the fallback window (if it already exists).
  204. resolve(null);
  205. } else {
  206. AddonManager.getAddonByInstanceID(activeAddon.instanceID)
  207. .then(privateWrapper => {
  208. let targetWindow = privateWrapper.getDebugGlobal();
  209. // Do not use the preferred global if it is not a DOMWindow as expected.
  210. if (!(targetWindow instanceof Ci.nsIDOMWindow)) {
  211. targetWindow = null;
  212. }
  213. resolve(targetWindow);
  214. });
  215. }
  216. }).then(preferredTargetWindow => {
  217. this.preferredTargetWindow = preferredTargetWindow;
  218. if (!preferredTargetWindow) {
  219. // Create a fallback window if no preferred target window has been found.
  220. this._createFallbackWindow();
  221. } else if (this.attached) {
  222. // Change the top level document if the actor is already attached.
  223. this._changeTopLevelDocument(preferredTargetWindow);
  224. }
  225. });
  226. };
  227. /**
  228. * Return an array of the json details related to an array/iterator of docShells.
  229. */
  230. WebExtensionActor.prototype._docShellsToWindows = function (docshells) {
  231. return ChromeActor.prototype._docShellsToWindows.call(this, docshells)
  232. .filter(windowDetails => {
  233. // filter the docShells based on the addon id
  234. return windowDetails.addonID == this.id;
  235. });
  236. };
  237. /**
  238. * Return true if the given source is associated with this addon and should be
  239. * added to the visible sources (retrieved and used by the webbrowser actor module).
  240. */
  241. WebExtensionActor.prototype._allowSource = function (source) {
  242. try {
  243. let uri = Services.io.newURI(source.url, null, null);
  244. let addonID = mapURIToAddonID(uri);
  245. return addonID == this.id;
  246. } catch (e) {
  247. return false;
  248. }
  249. };
  250. /**
  251. * Return true if the given global is associated with this addon and should be
  252. * added as a debuggee, false otherwise.
  253. */
  254. WebExtensionActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal) {
  255. const global = unwrapDebuggerObjectGlobal(newGlobal);
  256. if (global instanceof Ci.nsIDOMWindow) {
  257. return global.document.nodePrincipal.originAttributes.addonId == this.id;
  258. }
  259. try {
  260. // This will fail for non-Sandbox objects, hence the try-catch block.
  261. let metadata = Cu.getSandboxMetadata(global);
  262. if (metadata) {
  263. return metadata.addonID === this.id;
  264. }
  265. } catch (e) {
  266. // Unable to retrieve the sandbox metadata.
  267. }
  268. return false;
  269. };
  270. /**
  271. * Override WebExtensionActor requestTypes:
  272. * - redefined `reload`, which should reload the target addon
  273. * (instead of the entire browser as the regular ChromeActor does).
  274. */
  275. WebExtensionActor.prototype.requestTypes.reload = WebExtensionActor.prototype.onReload;