webextension.js (10469B)
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 5 "use strict"; 6 7 /* 8 * Represents a WebExtension add-on in the parent process. This gives some metadata about 9 * the add-on and watches for uninstall events. This uses a proxy to access the 10 * WebExtension in the WebExtension process via the message manager. 11 * 12 * See devtools/docs/backend/actor-hierarchy.md for more details. 13 */ 14 15 const { Actor } = require("resource://devtools/shared/protocol.js"); 16 const { 17 webExtensionDescriptorSpec, 18 } = require("resource://devtools/shared/specs/descriptors/webextension.js"); 19 20 const { 21 createWebExtensionSessionContext, 22 } = require("resource://devtools/server/actors/watcher/session-context.js"); 23 24 const lazy = {}; 25 loader.lazyGetter(lazy, "AddonManager", () => { 26 return ChromeUtils.importESModule( 27 "resource://gre/modules/AddonManager.sys.mjs", 28 { global: "shared" } 29 ).AddonManager; 30 }); 31 loader.lazyGetter(lazy, "ExtensionParent", () => { 32 return ChromeUtils.importESModule( 33 "resource://gre/modules/ExtensionParent.sys.mjs", 34 { global: "shared" } 35 ).ExtensionParent; 36 }); 37 loader.lazyRequireGetter( 38 this, 39 "WatcherActor", 40 "resource://devtools/server/actors/watcher.js", 41 true 42 ); 43 44 const { WEBEXTENSION_FALLBACK_DOC_URL } = ChromeUtils.importESModule( 45 "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", 46 { global: "contextual" } 47 ); 48 49 const BGSCRIPT_STATUSES = { 50 RUNNING: "RUNNING", 51 STOPPED: "STOPPED", 52 }; 53 54 /** 55 * Creates the actor that represents the addon in the parent process, which relies 56 * on its child Watcher Actor to expose all WindowGlobal target actors for all 57 * the active documents involved in the debugged addon. 58 * 59 * The WebExtensionDescriptorActor subscribes itself as an AddonListener on the AddonManager 60 * and forwards this events to child actor (e.g. on addon reload or when the addon is 61 * uninstalled completely) and connects to the child extension process using a `browser` 62 * element provided by the extension internals (it is not related to any single extension, 63 * but it will be created automatically to the currently selected "WebExtensions OOP mode" 64 * and it persist across the extension reloads. 65 * 66 * The descriptor will also be persisted when the target actor is destroyed, so 67 * that we can reuse the same descriptor for several remote debugging toolboxes 68 * from about:debugging. 69 * 70 * WebExtensionDescriptorActor is a child of RootActor, it can be retrieved via 71 * RootActor.listAddons request. 72 * 73 * @param {DevToolsServerConnection} conn 74 * The connection to the client. 75 * @param {AddonWrapper} addon 76 * The target addon. 77 */ 78 class WebExtensionDescriptorActor extends Actor { 79 constructor(conn, addon) { 80 super(conn, webExtensionDescriptorSpec); 81 this.addon = addon; 82 this.addonId = addon.id; 83 84 this.destroy = this.destroy.bind(this); 85 86 lazy.AddonManager.addAddonListener(this); 87 } 88 89 form() { 90 const { addonId } = this; 91 const policy = lazy.ExtensionParent.WebExtensionPolicy.getByID(addonId); 92 const persistentBackgroundScript = 93 lazy.ExtensionParent.DebugUtils.hasPersistentBackgroundScript(addonId); 94 const backgroundScriptStatus = this._getBackgroundScriptStatus(); 95 96 return { 97 actor: this.actorID, 98 backgroundScriptStatus, 99 // Note that until the policy becomes active, 100 // getWatcher will fail attaching to the web extension: 101 // https://searchfox.org/mozilla-central/rev/526a5089c61db85d4d43eb0e46edaf1f632e853a/toolkit/components/extensions/WebExtensionPolicy.cpp#551-553 102 debuggable: policy?.active && this.addon.isDebuggable, 103 hidden: this.addon.hidden, 104 // iconDataURL is available after calling loadIconDataURL 105 iconDataURL: this._iconDataURL, 106 iconURL: this.addon.iconURL, 107 id: addonId, 108 isSystem: this.addon.isSystem, 109 isWebExtension: this.addon.isWebExtension, 110 manifestURL: policy && policy.getURL("manifest.json"), 111 name: this.addon.name, 112 persistentBackgroundScript, 113 temporarilyInstalled: this.addon.temporarilyInstalled, 114 traits: { 115 supportsReloadDescriptor: true, 116 // Supports the Watcher actor. Can be removed as part of Bug 1680280. 117 watcher: true, 118 }, 119 url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined, 120 warnings: lazy.ExtensionParent.DebugUtils.getExtensionManifestWarnings( 121 this.addonId 122 ), 123 }; 124 } 125 126 /** 127 * Return a Watcher actor, allowing to keep track of targets which 128 * already exists or will be created. It also helps knowing when they 129 * are destroyed. 130 */ 131 async getWatcher(config = {}) { 132 if (!this.watcher) { 133 // Spawn an empty document so that we always have an active WindowGlobal, 134 // so that we can always instantiate a top level WindowGlobal target to the frontend. 135 await this.#createFallbackDocument(); 136 137 this.watcher = new WatcherActor( 138 this.conn, 139 createWebExtensionSessionContext( 140 { 141 addonId: this.addonId, 142 }, 143 config 144 ) 145 ); 146 this.manage(this.watcher); 147 } 148 return this.watcher; 149 } 150 151 /** 152 * Create an empty document to circumvant the lack of any WindowGlobal/document 153 * running for this addon. 154 * 155 * For now DevTools always expect at least one Target to be functional, 156 * and we need a document to spawn a target actor. 157 */ 158 async #createFallbackDocument() { 159 if (this._browser) { 160 return; 161 } 162 163 // The extension process browser will only be released on descriptor destruction and can 164 // be reused for subsequent watchers if we close and reopen a toolbox from about:debugging. 165 // 166 // Note that this `getExtensionProcessBrowser` will register the DevTools to the extension codebase. 167 // If we stop creating a fallback document, we should register DevTools by some other means. 168 this._browser = 169 await lazy.ExtensionParent.DebugUtils.getExtensionProcessBrowser(this); 170 171 // As "load" event isn't fired on the <browser> element, use a Web Progress Listener 172 // in order to wait for the full loading of that fallback document. 173 // It prevents having to deal with the initial about:blank document in the content processes. 174 // We have various checks to identify the fallback document based on its URL. 175 // It also ensure that the fallback document is created before the watcher starts 176 // and helps spawning the target for that document first. 177 const onLocationChanged = new Promise(resolve => { 178 const listener = { 179 onLocationChange: () => { 180 this._browser.webProgress.removeProgressListener(listener); 181 resolve(); 182 }, 183 QueryInterface: ChromeUtils.generateQI([ 184 "nsIWebProgressListener", 185 "nsISupportsWeakReference", 186 ]), 187 }; 188 189 this._browser.webProgress.addProgressListener( 190 listener, 191 Ci.nsIWebProgress.NOTIFY_LOCATION 192 ); 193 }); 194 195 // Add the addonId in the URL to retrieve this information in other devtools 196 // helpers. The addonId is usually populated in the principal, but this will 197 // not be the case for the fallback window because it is loaded from chrome:// 198 // instead of moz-extension://${addonId} 199 this._browser.setAttribute( 200 "src", 201 `${WEBEXTENSION_FALLBACK_DOC_URL}#${this.addonId}` 202 ); 203 await onLocationChanged; 204 } 205 206 /** 207 * Note that reloadDescriptor is the common API name for descriptors 208 * which support to be reloaded, while WebExtensionDescriptorActor::reload 209 * is a legacy API which is for instance used from web-ext. 210 * 211 * bypassCache has no impact for addon reloads. 212 */ 213 reloadDescriptor() { 214 return this.reload(); 215 } 216 217 async reload() { 218 await this.addon.reload(); 219 return {}; 220 } 221 222 async terminateBackgroundScript() { 223 await lazy.ExtensionParent.DebugUtils.terminateBackgroundScript( 224 this.addonId 225 ); 226 } 227 228 // This function will be called from RootActor in case that the devtools client 229 // retrieves list of addons with `iconDataURL` option. 230 async loadIconDataURL() { 231 this._iconDataURL = await this.getIconDataURL(); 232 } 233 234 async getIconDataURL() { 235 if (!this.addon.iconURL) { 236 return null; 237 } 238 239 const xhr = new XMLHttpRequest(); 240 xhr.responseType = "blob"; 241 xhr.open("GET", this.addon.iconURL, true); 242 243 if (this.addon.iconURL.toLowerCase().endsWith(".svg")) { 244 // Maybe SVG, thus force to change mime type. 245 xhr.overrideMimeType("image/svg+xml"); 246 } 247 248 try { 249 const blob = await new Promise((resolve, reject) => { 250 xhr.onload = () => resolve(xhr.response); 251 xhr.onerror = reject; 252 xhr.send(); 253 }); 254 255 const reader = new FileReader(); 256 return await new Promise((resolve, reject) => { 257 reader.onloadend = () => resolve(reader.result); 258 reader.onerror = reject; 259 reader.readAsDataURL(blob); 260 }); 261 } catch (_) { 262 console.warn(`Failed to create data url from [${this.addon.iconURL}]`); 263 return null; 264 } 265 } 266 267 // Private Methods 268 _getBackgroundScriptStatus() { 269 const isRunning = lazy.ExtensionParent.DebugUtils.isBackgroundScriptRunning( 270 this.addonId 271 ); 272 // The background script status doesn't apply to this addon (e.g. the addon 273 // type doesn't have any code, like staticthemes/langpacks/dictionaries, or 274 // the extension does not have a background script at all). 275 if (isRunning === undefined) { 276 return undefined; 277 } 278 279 return isRunning ? BGSCRIPT_STATUSES.RUNNING : BGSCRIPT_STATUSES.STOPPED; 280 } 281 282 // AddonManagerListener callbacks. 283 onInstalled(addon) { 284 if (addon.id != this.addonId) { 285 return; 286 } 287 288 // Update the AddonManager's addon object on reload/update. 289 this.addon = addon; 290 } 291 292 onUninstalled(addon) { 293 if (addon != this.addon) { 294 return; 295 } 296 297 this.destroy(); 298 } 299 300 destroy() { 301 lazy.AddonManager.removeAddonListener(this); 302 303 this.addon = null; 304 305 if (this.watcher) { 306 this.watcher = null; 307 } 308 309 if (this._browser) { 310 lazy.ExtensionParent.DebugUtils.releaseExtensionProcessBrowser(this); 311 this._browser = null; 312 } 313 314 this.emit("descriptor-destroyed"); 315 316 super.destroy(); 317 } 318 } 319 320 exports.WebExtensionDescriptorActor = WebExtensionDescriptorActor;