parent-process-document-event.js (7066B)
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 const { 8 WILL_NAVIGATE_TIME_SHIFT, 9 } = require("resource://devtools/server/actors/webconsole/listeners/document-events.js"); 10 11 class ParentProcessDocumentEventWatcher { 12 /** 13 * Start watching, from the parent process, for DOCUMENT_EVENT's "will-navigate" event related to a given Watcher Actor. 14 * 15 * All other DOCUMENT_EVENT events are implemented from another watcher class, running in the content process. 16 * Note that this other content process watcher will also emit one special edgecase of will-navigate 17 * retlated to the iframe dropdown menu. 18 * 19 * We have to move listen for navigation in the parent to better handle bfcache navigations 20 * and more generally all navigations which are initiated from the parent process. 21 * 'bfcacheInParent' feature enabled many types of navigations to be controlled from the parent process. 22 * 23 * This was especially important to have this implementation in the parent 24 * because the navigation event may be fired too late in the content process. 25 * Leading to will-navigate being emitted *after* the new target we navigate to is notified to the client. 26 * 27 * @param WatcherActor watcherActor 28 * The watcher actor from which we should observe document event 29 * @param Object options 30 * Dictionary object with following attributes: 31 * - onAvailable: mandatory function 32 * This will be called for each resource. 33 */ 34 async watch(watcherActor, { onAvailable }) { 35 this.watcherActor = watcherActor; 36 this.onAvailable = onAvailable; 37 38 // List of listeners keyed by innerWindowId. 39 // Listeners are called as soon as we emitted the will-navigate 40 // resource for the related WindowGlobal. 41 this._onceWillNavigate = new Map(); 42 43 // Filter browsing contexts to only have the top BrowsingContext of each tree of BrowsingContexts… 44 const topLevelBrowsingContexts = this.watcherActor 45 .getAllBrowsingContexts() 46 .filter(browsingContext => browsingContext.top == browsingContext); 47 48 // Only register one WebProgressListener per BrowsingContext tree. 49 // We will be notified about children BrowsingContext navigations/state changes via the top level BrowsingContextWebProgressListener, 50 // and BrowsingContextWebProgress.browsingContext attribute will be updated dynamically everytime 51 // we get notified about a child BrowsingContext. 52 // Note that regular web page toolbox will only have one BrowsingContext tree, for the given tab. 53 // But the Browser Toolbox will have many trees to listen to, one per top-level Window, and also one per tab, 54 // as tabs's BrowsingContext context aren't children of their top level window! 55 // 56 // Also save the WebProgress and not the BrowsingContext because `BrowsingContext.webProgress` will be undefined in destroy(), 57 // while it is still valuable to call `webProgress.removeProgressListener`. Otherwise events keeps being notified!! 58 this.webProgresses = topLevelBrowsingContexts.map( 59 browsingContext => browsingContext.webProgress 60 ); 61 this.webProgresses.forEach(webProgress => { 62 webProgress.addProgressListener( 63 this, 64 Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT 65 ); 66 }); 67 } 68 69 /** 70 * Wait for the emission of will-navigate for a given WindowGlobal 71 * 72 * @param Number innerWindowId 73 * WindowGlobal's id we want to track 74 * @return Promise 75 * Resolves immediatly if the WindowGlobal isn't tracked by any target 76 * -or- resolve later, once the WindowGlobal navigates to another document 77 * and will-navigate has been emitted. 78 */ 79 onceWillNavigateIsEmitted(innerWindowId) { 80 // Only delay the target-destroyed event if the target is for BrowsingContext for which we will emit will-navigate 81 const isTracked = this.webProgresses.find( 82 webProgress => 83 webProgress.browsingContext.currentWindowGlobal?.innerWindowId == 84 innerWindowId 85 ); 86 if (isTracked) { 87 return new Promise(resolve => { 88 this._onceWillNavigate.set(innerWindowId, resolve); 89 }); 90 } 91 return Promise.resolve(); 92 } 93 94 onStateChange(progress, request, flag) { 95 const isStart = flag & Ci.nsIWebProgressListener.STATE_START; 96 const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; 97 if (isDocument && isStart) { 98 const { browsingContext } = progress; 99 // Ignore if we are still on the initial document, 100 // as that's the navigation from it (about:blank) to the actual first location. 101 // The target isn't created yet. 102 if (browsingContext.currentWindowGlobal.isUncommittedInitialDocument) { 103 return; 104 } 105 106 // Never emit will-navigate for content browsing contexts in the Browser Toolbox. 107 // They might verify `browsingContext.top == browsingContext` because of the chrome/content 108 // boundary, but they do not represent a top-level target for this DevTools session. 109 if ( 110 this.watcherActor.sessionContext.type == "all" && 111 browsingContext.isContent 112 ) { 113 return; 114 } 115 // Also ignore will-navigate for Web Extensions as we shouldn't clear things up when 116 // any of its WindowGlobal starts navigating away. Instead things should be cleared when 117 // we destroy the targets. 118 if (this.watcherActor.sessionContext.type == "webextension") { 119 return; 120 } 121 122 // Only emit will-navigate for top-level targets. 123 const isTopLevel = browsingContext.top == browsingContext; 124 if (!isTopLevel) { 125 return; 126 } 127 128 const newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null; 129 const { innerWindowId } = browsingContext.currentWindowGlobal; 130 this.onAvailable([ 131 { 132 browsingContextID: browsingContext.id, 133 innerWindowId, 134 name: "will-navigate", 135 time: Date.now() - WILL_NAVIGATE_TIME_SHIFT, 136 isFrameSwitching: false, 137 newURI, 138 }, 139 ]); 140 const callback = this._onceWillNavigate.get(innerWindowId); 141 if (callback) { 142 this._onceWillNavigate.delete(innerWindowId); 143 callback(); 144 } 145 146 // Also emit the event on the watcher actor for other actors to catch this event 147 if (browsingContext == this.watcherActor.browserElement.browsingContext) { 148 this.watcherActor.emit("top-browsing-context-will-navigate"); 149 } 150 } 151 } 152 153 get QueryInterface() { 154 return ChromeUtils.generateQI([ 155 "nsIWebProgressListener", 156 "nsISupportsWeakReference", 157 ]); 158 } 159 160 destroy() { 161 this.webProgresses.forEach(webProgress => { 162 webProgress.removeProgressListener( 163 this, 164 Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT 165 ); 166 }); 167 this.webProgresses = null; 168 } 169 } 170 171 module.exports = ParentProcessDocumentEventWatcher;