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