gecko-dev/browser/actors/RefreshBlockerChild.sys.mjs

235 lines
7.5 KiB
JavaScript

/* 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) {
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) {
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) {
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);
}
}