summaryrefslogtreecommitdiffstats
path: root/browser/actors/RefreshBlockerChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/actors/RefreshBlockerChild.sys.mjs234
1 files changed, 234 insertions, 0 deletions
diff --git a/browser/actors/RefreshBlockerChild.sys.mjs b/browser/actors/RefreshBlockerChild.sys.mjs
new file mode 100644
index 0000000000..6ba63298b1
--- /dev/null
+++ b/browser/actors/RefreshBlockerChild.sys.mjs
@@ -0,0 +1,234 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file has two actors, RefreshBlockerChild js a window actor which
+ * handles the refresh notifications. RefreshBlockerObserverChild is a process
+ * actor that enables refresh blocking on each docshell that is created.
+ */
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+const REFRESHBLOCKING_PREF = "accessibility.blockautorefresh";
+
+var progressListener = {
+ // Bug 1247100 - When a refresh is caused by an HTTP header,
+ // onRefreshAttempted will be fired before onLocationChange.
+ // When a refresh is caused by a <meta> tag in the document,
+ // onRefreshAttempted will be fired after onLocationChange.
+ //
+ // We only ever want to send a message to the parent after
+ // onLocationChange has fired, since the parent uses the
+ // onLocationChange update to clear transient notifications.
+ // Sending the message before onLocationChange will result in
+ // us creating the notification, and then clearing it very
+ // soon after.
+ //
+ // To account for both cases (onRefreshAttempted before
+ // onLocationChange, and onRefreshAttempted after onLocationChange),
+ // we'll hold a mapping of DOM Windows that we see get
+ // sent through both onLocationChange and onRefreshAttempted.
+ // When either run, they'll check the WeakMap for the existence
+ // of the DOM Window. If it doesn't exist, it'll add it. If
+ // it finds it, it'll know that it's safe to send the message
+ // to the parent, since we know that both have fired.
+ //
+ // The DOM Window is removed from blockedWindows when we notice
+ // the nsIWebProgress change state to STATE_STOP for the
+ // STATE_IS_WINDOW case.
+ //
+ // DOM Windows are mapped to a JS object that contains the data
+ // to be sent to the parent to show the notification. Since that
+ // data is only known when onRefreshAttempted is fired, it's only
+ // ever stashed in the map if onRefreshAttempted fires first -
+ // otherwise, null is set as the value of the mapping.
+ blockedWindows: new WeakMap(),
+
+ /**
+ * Notices when the nsIWebProgress transitions to STATE_STOP for
+ * the STATE_IS_WINDOW case, which will clear any mappings from
+ * blockedWindows.
+ */
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP
+ ) {
+ this.blockedWindows.delete(aWebProgress.DOMWindow);
+ }
+ },
+
+ /**
+ * Notices when the location has changed. If, when running,
+ * onRefreshAttempted has already fired for this DOM Window, will
+ * send the appropriate refresh blocked data to the parent.
+ */
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ let win = aWebProgress.DOMWindow;
+ if (this.blockedWindows.has(win)) {
+ let data = this.blockedWindows.get(win);
+ if (data) {
+ // We saw onRefreshAttempted before onLocationChange, so
+ // send the message to the parent to show the notification.
+ this.send(win, data);
+ }
+ } else {
+ this.blockedWindows.set(win, null);
+ }
+ },
+
+ /**
+ * Notices when a refresh / reload was attempted. If, when running,
+ * onLocationChange has not yet run, will stash the appropriate data
+ * into the blockedWindows map to be sent when onLocationChange fires.
+ */
+ onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
+ let win = aWebProgress.DOMWindow;
+
+ let data = {
+ browsingContext: win.browsingContext,
+ URI: aURI.spec,
+ delay: aDelay,
+ sameURI: aSameURI,
+ };
+
+ if (this.blockedWindows.has(win)) {
+ // onLocationChange must have fired before, so we can tell the
+ // parent to show the notification.
+ this.send(win, data);
+ } else {
+ // onLocationChange hasn't fired yet, so stash the data in the
+ // map so that onLocationChange can send it when it fires.
+ this.blockedWindows.set(win, data);
+ }
+
+ return false;
+ },
+
+ send(win, data) {
+ // Due to the |nsDocLoader| calling its |nsIWebProgressListener|s in
+ // reverse order, this will occur *before* the |BrowserChild| can send its
+ // |OnLocationChange| event to the parent, but we need this message to
+ // arrive after to ensure that the refresh blocker notification is not
+ // immediately cleared by the |OnLocationChange| from |BrowserChild|.
+ setTimeout(() => {
+ // An exception can occur if refresh blocking was turned off
+ // during a pageload.
+ try {
+ let actor = win.windowGlobalChild.getActor("RefreshBlocker");
+ if (actor) {
+ actor.sendAsyncMessage("RefreshBlocker:Blocked", data);
+ }
+ } catch (ex) {}
+ }, 0);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener2",
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+export class RefreshBlockerChild extends JSWindowActorChild {
+ didDestroy() {
+ // If the refresh blocking preference is turned off, all of the
+ // RefreshBlockerChild actors will get destroyed, so disable
+ // refresh blocking only in this case.
+ if (!Services.prefs.getBoolPref(REFRESHBLOCKING_PREF)) {
+ this.disable(this.docShell);
+ }
+ }
+
+ enable() {
+ ChromeUtils.domProcessChild
+ .getActor("RefreshBlockerObserver")
+ .enable(this.docShell);
+ }
+
+ disable() {
+ ChromeUtils.domProcessChild
+ .getActor("RefreshBlockerObserver")
+ .disable(this.docShell);
+ }
+
+ receiveMessage(message) {
+ let data = message.data;
+
+ switch (message.name) {
+ case "RefreshBlocker:Refresh":
+ let docShell = data.browsingContext.docShell;
+ let refreshURI = docShell.QueryInterface(Ci.nsIRefreshURI);
+ let URI = Services.io.newURI(data.URI);
+ refreshURI.forceRefreshURI(URI, null, data.delay);
+ break;
+
+ case "PreferenceChanged":
+ if (data.isEnabled) {
+ this.enable(this.docShell);
+ } else {
+ this.disable(this.docShell);
+ }
+ }
+ }
+}
+
+export class RefreshBlockerObserverChild extends JSProcessActorChild {
+ constructor() {
+ super();
+ this.filtersMap = new Map();
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "webnavigation-create":
+ case "chrome-webnavigation-create":
+ if (Services.prefs.getBoolPref(REFRESHBLOCKING_PREF)) {
+ this.enable(subject.QueryInterface(Ci.nsIDocShell));
+ }
+ break;
+
+ case "webnavigation-destroy":
+ case "chrome-webnavigation-destroy":
+ if (Services.prefs.getBoolPref(REFRESHBLOCKING_PREF)) {
+ this.disable(subject.QueryInterface(Ci.nsIDocShell));
+ }
+ break;
+ }
+ }
+
+ enable(docShell) {
+ if (this.filtersMap.has(docShell)) {
+ return;
+ }
+
+ let filter = Cc[
+ "@mozilla.org/appshell/component/browser-status-filter;1"
+ ].createInstance(Ci.nsIWebProgress);
+
+ filter.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ this.filtersMap.set(docShell, filter);
+
+ let webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
+ }
+
+ disable(docShell) {
+ let filter = this.filtersMap.get(docShell);
+ if (!filter) {
+ return;
+ }
+
+ let webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.removeProgressListener(filter);
+
+ filter.removeProgressListener(progressListener);
+ this.filtersMap.delete(docShell);
+ }
+}