tab.js (9747B)
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 * Descriptor Actor that represents a Tab in the parent process. It 9 * launches a WindowGlobalTargetActor in the content process to do the real work and tunnels the 10 * data. 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 tabDescriptorSpec, 18 } = require("resource://devtools/shared/specs/descriptors/tab.js"); 19 20 const lazy = {}; 21 ChromeUtils.defineESModuleGetters( 22 lazy, 23 { 24 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 25 }, 26 { global: "contextual" } 27 ); 28 29 const { AppConstants } = ChromeUtils.importESModule( 30 "resource://gre/modules/AppConstants.sys.mjs", 31 { global: "contextual" } 32 ); 33 const { 34 createBrowserElementSessionContext, 35 } = require("resource://devtools/server/actors/watcher/session-context.js"); 36 37 loader.lazyRequireGetter( 38 this, 39 "WatcherActor", 40 "resource://devtools/server/actors/watcher.js", 41 true 42 ); 43 loader.lazyRequireGetter( 44 this, 45 "connectToFrame", 46 "resource://devtools/server/connectors/frame-connector.js", 47 true 48 ); 49 50 /** 51 * Creates a target actor proxy for handling requests to a single browser frame. 52 * Both <xul:browser> and <iframe mozbrowser> are supported. 53 * This actor is a shim that connects to a WindowGlobalTargetActor in a remote browser process. 54 * All RDP packets get forwarded using the message manager. 55 * 56 * @param connection The main RDP connection. 57 * @param browser <xul:browser> or <iframe mozbrowser> element to connect to. 58 */ 59 class TabDescriptorActor extends Actor { 60 constructor(connection, browser) { 61 super(connection, tabDescriptorSpec); 62 this._browser = browser; 63 } 64 65 form() { 66 const form = { 67 actor: this.actorID, 68 browserId: this._browser.browserId, 69 browsingContextID: 70 this._browser && this._browser.browsingContext 71 ? this._browser.browsingContext.id 72 : null, 73 isZombieTab: this._isZombieTab(), 74 outerWindowID: this._getOuterWindowId(), 75 selected: this.selected, 76 title: this._getTitle(), 77 traits: { 78 // Supports the Watcher actor. Can be removed as part of Bug 1680280. 79 watcher: true, 80 supportsReloadDescriptor: true, 81 // Tab descriptor is the only one to support navigation 82 supportsNavigation: true, 83 }, 84 url: this._getUrl(), 85 }; 86 87 return form; 88 } 89 90 _getTitle() { 91 // If the content already provides a title, use it. 92 if (this._browser.contentTitle) { 93 return this._browser.contentTitle; 94 } 95 96 // For zombie or lazy tabs (tab created, but content has not been loaded), 97 // try to retrieve the title from the XUL Tab itself. 98 // Note: this only works on Firefox desktop. 99 if (this._tabbrowser) { 100 const tab = this._tabbrowser.getTabForBrowser(this._browser); 101 if (tab) { 102 return tab.label; 103 } 104 } 105 106 // No title available. 107 return null; 108 } 109 110 _getUrl() { 111 if (!this._browser || !this._browser.browsingContext) { 112 return ""; 113 } 114 115 const { browsingContext } = this._browser; 116 return browsingContext.currentWindowGlobal.documentURI.spec; 117 } 118 119 _getOuterWindowId() { 120 if (!this._browser || !this._browser.browsingContext) { 121 return ""; 122 } 123 124 const { browsingContext } = this._browser; 125 return browsingContext.currentWindowGlobal.outerWindowId; 126 } 127 128 get selected() { 129 // getMostRecentBrowserWindow will find the appropriate window on Firefox 130 // Desktop and on GeckoView. 131 const topAppWindow = Services.wm.getMostRecentBrowserWindow(); 132 133 const selectedBrowser = topAppWindow?.gBrowser?.selectedBrowser; 134 if (!selectedBrowser) { 135 // Note: gBrowser is not available on GeckoView. 136 // We should find another way to know if this browser is the selected 137 // browser. See Bug 1631020. 138 return false; 139 } 140 141 return this._browser === selectedBrowser; 142 } 143 144 async getTarget() { 145 if (!this.conn) { 146 return { 147 error: "tabDestroyed", 148 message: "Tab destroyed while performing a TabDescriptorActor update", 149 }; 150 } 151 152 /* eslint-disable-next-line no-async-promise-executor */ 153 return new Promise(async (resolve, reject) => { 154 const onDestroy = () => { 155 // Reject the update promise if the tab was destroyed while requesting an update 156 reject({ 157 error: "tabDestroyed", 158 message: "Tab destroyed while performing a TabDescriptorActor update", 159 }); 160 161 // Targets created from the TabDescriptor are not created via JSWindowActors and 162 // we need to notify the watcher manually about their destruction. 163 // TabDescriptor's targets are created via TabDescriptor.getTarget and are still using 164 // message manager instead of JSWindowActors. 165 if (this.watcher && this.targetActorForm) { 166 this.watcher.notifyTargetDestroyed(this.targetActorForm); 167 } 168 }; 169 170 try { 171 // Check if the browser is still connected before calling connectToFrame 172 if (!this._browser.isConnected) { 173 onDestroy(); 174 return; 175 } 176 177 const connectForm = await connectToFrame( 178 this.conn, 179 this._browser, 180 onDestroy 181 ); 182 this.targetActorForm = connectForm; 183 resolve(connectForm); 184 } catch (e) { 185 reject({ 186 error: "tabDestroyed", 187 message: "Tab destroyed while connecting to the frame", 188 }); 189 } 190 }); 191 } 192 193 /** 194 * Return a Watcher actor, allowing to keep track of targets which 195 * already exists or will be created. It also helps knowing when they 196 * are destroyed. 197 */ 198 getWatcher(config) { 199 if (!this.watcher) { 200 this.watcher = new WatcherActor( 201 this.conn, 202 createBrowserElementSessionContext(this._browser, { 203 isServerTargetSwitchingEnabled: config.isServerTargetSwitchingEnabled, 204 isPopupDebuggingEnabled: config.isPopupDebuggingEnabled, 205 }) 206 ); 207 this.manage(this.watcher); 208 } 209 return this.watcher; 210 } 211 212 get _tabbrowser() { 213 if (this._browser && typeof this._browser.getTabBrowser == "function") { 214 return this._browser.getTabBrowser(); 215 } 216 return null; 217 } 218 219 async getFavicon() { 220 if (!AppConstants.MOZ_PLACES) { 221 // PlacesUtils is not supported 222 return null; 223 } 224 225 try { 226 const favicon = await lazy.PlacesUtils.favicons.getFaviconForPage( 227 lazy.PlacesUtils.toURI(this._getUrl()) 228 ); 229 return favicon.rawData; 230 } catch (e) { 231 // Favicon unavailable for this url. 232 return null; 233 } 234 } 235 236 _isZombieTab() { 237 // Note: GeckoView doesn't support zombie tabs 238 const tabbrowser = this._tabbrowser; 239 const tab = tabbrowser ? tabbrowser.getTabForBrowser(this._browser) : null; 240 return tab?.hasAttribute && tab.hasAttribute("pending"); 241 } 242 243 /** 244 * Navigate this tab to a new URL. 245 * 246 * @param {string} url 247 * @param {boolean} waitForLoad 248 * @return {Promise} 249 * A promise which resolves only once the requested URL is fully loaded. 250 */ 251 async navigateTo(url, waitForLoad = true) { 252 if (!this._browser || !this._browser.browsingContext) { 253 throw new Error("Tab is destroyed"); 254 } 255 256 let validURL; 257 try { 258 validURL = Services.io.newURI(url); 259 } catch (e) { 260 throw new Error("Error: Cannot navigate to invalid URL: " + url); 261 } 262 263 // Setup a nsIWebProgressListener in order to be able to know when the 264 // new document is done loading. 265 const deferred = Promise.withResolvers(); 266 const listener = { 267 onStateChange(webProgress, request, stateFlags) { 268 if ( 269 webProgress.isTopLevel && 270 stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW && 271 stateFlags & 272 // Either wait for the start or the end of the document load 273 (waitForLoad 274 ? Ci.nsIWebProgressListener.STATE_STOP 275 : Ci.nsIWebProgressListener.STATE_START) 276 ) { 277 const loadedURL = request.QueryInterface(Ci.nsIChannel).originalURI 278 .spec; 279 if (loadedURL === validURL.spec) { 280 deferred.resolve(); 281 } 282 } 283 }, 284 285 QueryInterface: ChromeUtils.generateQI([ 286 "nsIWebProgressListener", 287 "nsISupportsWeakReference", 288 ]), 289 }; 290 this._browser.addProgressListener( 291 listener, 292 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW 293 ); 294 295 this._browser.browsingContext.loadURI(validURL, { 296 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 297 }); 298 299 await deferred.promise; 300 301 this._browser.removeProgressListener( 302 listener, 303 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW 304 ); 305 } 306 307 goBack() { 308 if (!this._browser || !this._browser.browsingContext) { 309 throw new Error("Tab is destroyed"); 310 } 311 312 this._browser.browsingContext.goBack(); 313 } 314 315 goForward() { 316 if (!this._browser || !this._browser.browsingContext) { 317 throw new Error("Tab is destroyed"); 318 } 319 320 this._browser.browsingContext.goForward(); 321 } 322 323 reloadDescriptor({ bypassCache }) { 324 if (!this._browser || !this._browser.browsingContext) { 325 return; 326 } 327 328 this._browser.browsingContext.reload( 329 bypassCache 330 ? Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE 331 : Ci.nsIWebNavigation.LOAD_FLAGS_NONE 332 ); 333 } 334 335 destroy() { 336 this.emit("descriptor-destroyed"); 337 this._browser = null; 338 339 super.destroy(); 340 } 341 } 342 343 exports.TabDescriptorActor = TabDescriptorActor;