RefreshBlockerChild.sys.mjs (7726B)
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 /** 6 * This file has two actors, RefreshBlockerChild js a window actor which 7 * handles the refresh notifications. RefreshBlockerObserverChild is a process 8 * actor that enables refresh blocking on each docshell that is created. 9 */ 10 11 import { setTimeout } from "resource://gre/modules/Timer.sys.mjs"; 12 13 const REFRESHBLOCKING_PREF = "accessibility.blockautorefresh"; 14 15 var progressListener = { 16 // Bug 1247100 - When a refresh is caused by an HTTP header, 17 // onRefreshAttempted will be fired before onLocationChange. 18 // When a refresh is caused by a <meta> tag in the document, 19 // onRefreshAttempted will be fired after onLocationChange. 20 // 21 // We only ever want to send a message to the parent after 22 // onLocationChange has fired, since the parent uses the 23 // onLocationChange update to clear transient notifications. 24 // Sending the message before onLocationChange will result in 25 // us creating the notification, and then clearing it very 26 // soon after. 27 // 28 // To account for both cases (onRefreshAttempted before 29 // onLocationChange, and onRefreshAttempted after onLocationChange), 30 // we'll hold a mapping of DOM Windows that we see get 31 // sent through both onLocationChange and onRefreshAttempted. 32 // When either run, they'll check the WeakMap for the existence 33 // of the DOM Window. If it doesn't exist, it'll add it. If 34 // it finds it, it'll know that it's safe to send the message 35 // to the parent, since we know that both have fired. 36 // 37 // The DOM Window is removed from blockedWindows when we notice 38 // the nsIWebProgress change state to STATE_STOP for the 39 // STATE_IS_WINDOW case. 40 // 41 // DOM Windows are mapped to a JS object that contains the data 42 // to be sent to the parent to show the notification. Since that 43 // data is only known when onRefreshAttempted is fired, it's only 44 // ever stashed in the map if onRefreshAttempted fires first - 45 // otherwise, null is set as the value of the mapping. 46 blockedWindows: new WeakMap(), 47 48 /** 49 * Notices when the nsIWebProgress transitions to STATE_STOP for 50 * the STATE_IS_WINDOW case, which will clear any mappings from 51 * blockedWindows. 52 */ 53 onStateChange(aWebProgress, aRequest, aStateFlags) { 54 if ( 55 aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW && 56 aStateFlags & Ci.nsIWebProgressListener.STATE_STOP 57 ) { 58 this.blockedWindows.delete(aWebProgress.DOMWindow); 59 } 60 }, 61 62 /** 63 * Notices when the location has changed. If, when running, 64 * onRefreshAttempted has already fired for this DOM Window, will 65 * send the appropriate refresh blocked data to the parent. 66 */ 67 onLocationChange(aWebProgress) { 68 let win = aWebProgress.DOMWindow; 69 if (this.blockedWindows.has(win)) { 70 let data = this.blockedWindows.get(win); 71 if (data) { 72 // We saw onRefreshAttempted before onLocationChange, so 73 // send the message to the parent to show the notification. 74 this.send(win, data); 75 } 76 } else { 77 this.blockedWindows.set(win, null); 78 } 79 }, 80 81 /** 82 * Notices when a refresh / reload was attempted. If, when running, 83 * onLocationChange has not yet run, will stash the appropriate data 84 * into the blockedWindows map to be sent when onLocationChange fires. 85 */ 86 onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) { 87 let win = aWebProgress.DOMWindow; 88 89 let data = { 90 browsingContext: win.browsingContext, 91 URI: aURI.spec, 92 delay: aDelay, 93 sameURI: aSameURI, 94 }; 95 96 if (this.blockedWindows.has(win)) { 97 // onLocationChange must have fired before, so we can tell the 98 // parent to show the notification. 99 this.send(win, data); 100 } else { 101 // onLocationChange hasn't fired yet, so stash the data in the 102 // map so that onLocationChange can send it when it fires. 103 this.blockedWindows.set(win, data); 104 } 105 106 return false; 107 }, 108 109 send(win, data) { 110 // Due to the |nsDocLoader| calling its |nsIWebProgressListener|s in 111 // reverse order, this will occur *before* the |BrowserChild| can send its 112 // |OnLocationChange| event to the parent, but we need this message to 113 // arrive after to ensure that the refresh blocker notification is not 114 // immediately cleared by the |OnLocationChange| from |BrowserChild|. 115 setTimeout(() => { 116 // An exception can occur if refresh blocking was turned off 117 // during a pageload. 118 try { 119 let actor = win.windowGlobalChild.getActor("RefreshBlocker"); 120 if (actor) { 121 actor.sendAsyncMessage("RefreshBlocker:Blocked", data); 122 } 123 } catch (ex) {} 124 }, 0); 125 }, 126 127 QueryInterface: ChromeUtils.generateQI([ 128 "nsIWebProgressListener2", 129 "nsIWebProgressListener", 130 "nsISupportsWeakReference", 131 ]), 132 }; 133 134 export class RefreshBlockerChild extends JSWindowActorChild { 135 didDestroy() { 136 // If the refresh blocking preference is turned off, all of the 137 // RefreshBlockerChild actors will get destroyed, so disable 138 // refresh blocking only in this case. 139 if (!Services.prefs.getBoolPref(REFRESHBLOCKING_PREF)) { 140 this.disable(this.docShell); 141 } 142 } 143 144 enable() { 145 ChromeUtils.domProcessChild 146 .getActor("RefreshBlockerObserver") 147 .enable(this.docShell); 148 } 149 150 disable() { 151 ChromeUtils.domProcessChild 152 .getActor("RefreshBlockerObserver") 153 .disable(this.docShell); 154 } 155 156 receiveMessage(message) { 157 let data = message.data; 158 159 switch (message.name) { 160 case "RefreshBlocker:Refresh": { 161 let docShell = data.browsingContext.docShell; 162 let refreshURI = docShell.QueryInterface(Ci.nsIRefreshURI); 163 let URI = Services.io.newURI(data.URI); 164 refreshURI.forceRefreshURI(URI, null, data.delay); 165 break; 166 } 167 168 case "PreferenceChanged": 169 if (data.isEnabled) { 170 this.enable(this.docShell); 171 } else { 172 this.disable(this.docShell); 173 } 174 } 175 } 176 } 177 178 export class RefreshBlockerObserverChild extends JSProcessActorChild { 179 constructor() { 180 super(); 181 this.filtersMap = new Map(); 182 } 183 184 observe(subject, topic) { 185 switch (topic) { 186 case "webnavigation-create": 187 case "chrome-webnavigation-create": 188 if (Services.prefs.getBoolPref(REFRESHBLOCKING_PREF)) { 189 this.enable(subject.QueryInterface(Ci.nsIDocShell)); 190 } 191 break; 192 193 case "webnavigation-destroy": 194 case "chrome-webnavigation-destroy": 195 if (Services.prefs.getBoolPref(REFRESHBLOCKING_PREF)) { 196 this.disable(subject.QueryInterface(Ci.nsIDocShell)); 197 } 198 break; 199 } 200 } 201 202 enable(docShell) { 203 if (this.filtersMap.has(docShell)) { 204 return; 205 } 206 207 let filter = Cc[ 208 "@mozilla.org/appshell/component/browser-status-filter;1" 209 ].createInstance(Ci.nsIWebProgress); 210 211 filter.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL); 212 213 this.filtersMap.set(docShell, filter); 214 215 let webProgress = docShell 216 .QueryInterface(Ci.nsIInterfaceRequestor) 217 .getInterface(Ci.nsIWebProgress); 218 webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL); 219 } 220 221 disable(docShell) { 222 let filter = this.filtersMap.get(docShell); 223 if (!filter) { 224 return; 225 } 226 227 let webProgress = docShell 228 .QueryInterface(Ci.nsIInterfaceRequestor) 229 .getInterface(Ci.nsIWebProgress); 230 webProgress.removeProgressListener(filter); 231 232 filter.removeProgressListener(progressListener); 233 this.filtersMap.delete(docShell); 234 } 235 }