Bug 1614761, move refresh blocking handling from framescript to an actor, r=Gijs

Differential Revision: https://phabricator.services.mozilla.com/D87206
This commit is contained in:
Neil Deakin 2020-08-25 10:21:38 +00:00
parent 7a092a0059
commit 3bcbd15d51
8 changed files with 245 additions and 205 deletions

View File

@ -2,14 +2,20 @@
* 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/. */
/* eslint-env mozilla/frame-script */
/**
* 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.
*/
var EXPORTED_SYMBOLS = ["RefreshBlockerChild", "RefreshBlockerObserverChild"];
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
var RefreshBlocker = {
PREF: "accessibility.blockautorefresh",
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,
@ -42,68 +48,6 @@ var RefreshBlocker = {
// otherwise, null is set as the value of the mapping.
blockedWindows: new WeakMap(),
init() {
if (Services.prefs.getBoolPref(this.PREF)) {
this.enable();
}
Services.prefs.addObserver(this.PREF, this);
},
uninit() {
if (Services.prefs.getBoolPref(this.PREF)) {
this.disable();
}
Services.prefs.removeObserver(this.PREF, this);
},
observe(subject, topic, data) {
if (topic == "nsPref:changed" && data == this.PREF) {
if (Services.prefs.getBoolPref(this.PREF)) {
this.enable();
} else {
this.disable();
}
}
},
enable() {
this._filter = Cc[
"@mozilla.org/appshell/component/browser-status-filter;1"
].createInstance(Ci.nsIWebProgress);
this._filter.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_ALL);
this._filter.target = tabEventTarget;
let webProgress = docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this._filter, Ci.nsIWebProgress.NOTIFY_ALL);
addMessageListener("RefreshBlocker:Refresh", this);
},
disable() {
let webProgress = docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.removeProgressListener(this._filter);
this._filter.removeProgressListener(this);
this._filter = null;
removeMessageListener("RefreshBlocker:Refresh", this);
},
send(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(() => sendAsyncMessage("RefreshBlocker:Blocked", data), 0);
},
/**
* Notices when the nsIWebProgress transitions to STATE_STOP for
* the STATE_IS_WINDOW case, which will clear any mappings from
@ -130,7 +74,7 @@ var RefreshBlocker = {
if (data) {
// We saw onRefreshAttempted before onLocationChange, so
// send the message to the parent to show the notification.
this.send(data);
this.send(win, data);
}
} else {
this.blockedWindows.set(win, null);
@ -144,19 +88,18 @@ var RefreshBlocker = {
*/
onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
let win = aWebProgress.DOMWindow;
let outerWindowID = win.docShell.outerWindowID;
let data = {
browsingContext: win.browsingContext,
URI: aURI.spec,
delay: aDelay,
sameURI: aSameURI,
outerWindowID,
};
if (this.blockedWindows.has(win)) {
// onLocationChange must have fired before, so we can tell the
// parent to show the notification.
this.send(data);
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.
@ -166,17 +109,22 @@ var RefreshBlocker = {
return false;
},
receiveMessage(message) {
let data = message.data;
if (message.name == "RefreshBlocker:Refresh") {
let win = Services.wm.getOuterWindowWithId(data.outerWindowID);
let refreshURI = win.docShell.QueryInterface(Ci.nsIRefreshURI);
let URI = Services.io.newURI(data.URI);
refreshURI.forceRefreshURI(URI, null, data.delay, true);
}
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([
@ -186,8 +134,104 @@ var RefreshBlocker = {
]),
};
RefreshBlocker.init();
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);
}
}
addEventListener("unload", () => {
RefreshBlocker.uninit();
});
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, true);
break;
case "PreferenceChanged":
if (data.isEnabled) {
this.enable(this.docShell);
} else {
this.disable(this.docShell);
}
}
}
}
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);
}
}

View File

@ -0,0 +1,20 @@
/* 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["RefreshBlockerParent"];
class RefreshBlockerParent extends JSWindowActorParent {
receiveMessage(message) {
if (message.name == "RefreshBlocker:Blocked") {
let browser = this.browsingContext.top.embedderElement;
if (browser) {
let gBrowser = browser.ownerGlobal.gBrowser;
if (gBrowser) {
gBrowser.refreshBlocked(this, browser, message.data);
}
}
}
}
}

View File

@ -72,6 +72,8 @@ FINAL_TARGET_FILES.actors += [
'PluginChild.jsm',
'PluginParent.jsm',
'PromptParent.jsm',
'RefreshBlockerChild.jsm',
'RefreshBlockerParent.jsm',
'RFPHelperChild.jsm',
'RFPHelperParent.jsm',
'SearchTelemetryChild.jsm',

View File

@ -1871,7 +1871,6 @@ var gBrowserInit = {
BrowserSearch.init();
BrowserPageActions.init();
gAccessibilityServiceIndicator.init();
AccessibilityRefreshBlocker.init();
if (gToolbarKeyNavEnabled) {
ToolbarKeyboardNavigator.init();
}
@ -2457,8 +2456,6 @@ var gBrowserInit = {
gAccessibilityServiceIndicator.uninit();
AccessibilityRefreshBlocker.uninit();
if (gToolbarKeyNavEnabled) {
ToolbarKeyboardNavigator.uninit();
}
@ -5796,50 +5793,6 @@ var CombinedStopReload = {
},
};
// This helper only cares about loading the frame
// script if the pref is seen as true.
// After the frame script is loaded, it takes over
// the responsibility of watching the pref and
// enabling/disabling itself.
const AccessibilityRefreshBlocker = {
PREF: "accessibility.blockautorefresh",
init() {
if (Services.prefs.getBoolPref(this.PREF)) {
this.loadFrameScript();
} else {
Services.prefs.addObserver(this.PREF, this);
}
},
uninit() {
Services.prefs.removeObserver(this.PREF, this);
},
observe(aSubject, aTopic, aPrefName) {
if (
aTopic == "nsPref:changed" &&
aPrefName == this.PREF &&
Services.prefs.getBoolPref(this.PREF)
) {
this.loadFrameScript();
Services.prefs.removeObserver(this.PREF, this);
}
},
loadFrameScript() {
if (!this._loaded) {
this._loaded = true;
let mm = window.getGroupMessageManager("browsers");
mm.loadFrameScript(
"chrome://browser/content/content-refreshblocker.js",
true,
true
);
}
},
};
var TabsProgressListener = {
onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
// Collect telemetry data about tab load times.

View File

@ -57,9 +57,6 @@
);
}
let messageManager = window.getGroupMessageManager("browsers");
messageManager.addMessageListener("RefreshBlocker:Blocked", this);
this._setFindbarData();
XPCOMUtils.defineLazyModuleGetters(this, {
@ -5125,76 +5122,6 @@
}
},
receiveMessage(aMessage) {
let data = aMessage.data;
let browser = aMessage.target;
switch (aMessage.name) {
case "RefreshBlocker:Blocked": {
// The data object is expected to contain the following properties:
// - URI (string)
// The URI that a page is attempting to refresh or redirect to.
// - delay (int)
// The delay (in milliseconds) before the page was going to
// reload or redirect.
// - sameURI (bool)
// true if we're refreshing the page. false if we're redirecting.
// - outerWindowID (int)
// The outerWindowID of the frame that requested the refresh or
// redirect.
let brandBundle = document.getElementById("bundle_brand");
let brandShortName = brandBundle.getString("brandShortName");
let message = gNavigatorBundle.getFormattedString(
"refreshBlocked." +
(data.sameURI ? "refreshLabel" : "redirectLabel"),
[brandShortName]
);
let notificationBox = this.getNotificationBox(browser);
let notification = notificationBox.getNotificationWithValue(
"refresh-blocked"
);
if (notification) {
notification.label = message;
} else {
let refreshButtonText = gNavigatorBundle.getString(
"refreshBlocked.goButton"
);
let refreshButtonAccesskey = gNavigatorBundle.getString(
"refreshBlocked.goButton.accesskey"
);
let buttons = [
{
label: refreshButtonText,
accessKey: refreshButtonAccesskey,
callback() {
if (browser.messageManager) {
browser.messageManager.sendAsyncMessage(
"RefreshBlocker:Refresh",
data
);
}
},
},
];
notificationBox.appendNotification(
message,
"refresh-blocked",
"chrome://browser/skin/notification-icons/popup.svg",
notificationBox.PRIORITY_INFO_MEDIUM,
buttons
);
}
break;
}
}
return undefined;
},
observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "contextual-identity-updated": {
@ -5209,6 +5136,58 @@
}
},
refreshBlocked(actor, browser, data) {
// The data object is expected to contain the following properties:
// - URI (string)
// The URI that a page is attempting to refresh or redirect to.
// - delay (int)
// The delay (in milliseconds) before the page was going to
// reload or redirect.
// - sameURI (bool)
// true if we're refreshing the page. false if we're redirecting.
let brandBundle = document.getElementById("bundle_brand");
let brandShortName = brandBundle.getString("brandShortName");
let message = gNavigatorBundle.getFormattedString(
"refreshBlocked." + (data.sameURI ? "refreshLabel" : "redirectLabel"),
[brandShortName]
);
let notificationBox = this.getNotificationBox(browser);
let notification = notificationBox.getNotificationWithValue(
"refresh-blocked"
);
if (notification) {
notification.label = message;
} else {
let refreshButtonText = gNavigatorBundle.getString(
"refreshBlocked.goButton"
);
let refreshButtonAccesskey = gNavigatorBundle.getString(
"refreshBlocked.goButton.accesskey"
);
let buttons = [
{
label: refreshButtonText,
accessKey: refreshButtonAccesskey,
callback() {
actor.sendAsyncMessage("RefreshBlocker:Refresh", data);
},
},
];
notificationBox.appendNotification(
message,
"refresh-blocked",
"chrome://browser/skin/notification-icons/popup.svg",
notificationBox.PRIORITY_INFO_MEDIUM,
buttons
);
}
},
_generateUniquePanelID() {
if (!this._uniquePanelIDCounter) {
this._uniquePanelIDCounter = 0;

View File

@ -85,7 +85,6 @@ browser.jar:
content/browser/pageinfo/pageInfo.css (content/pageinfo/pageInfo.css)
content/browser/pageinfo/permissions.js (content/pageinfo/permissions.js)
content/browser/pageinfo/security.js (content/pageinfo/security.js)
content/browser/content-refreshblocker.js (content/content-refreshblocker.js)
content/browser/robot.ico (content/robot.ico)
content/browser/static-robot.png (content/static-robot.png)
content/browser/safeMode.css (content/safeMode.css)

View File

@ -92,6 +92,34 @@ let JSPROCESSACTORS = {
],
},
},
RefreshBlockerObserver: {
child: {
moduleURI: "resource:///actors/RefreshBlockerChild.jsm",
observers: [
"webnavigation-create",
"chrome-webnavigation-create",
"webnavigation-destroy",
"chrome-webnavigation-destroy",
],
},
enablePreference: "accessibility.blockautorefresh",
onPreferenceChanged: (prefName, prevValue, isEnabled) => {
BrowserWindowTracker.orderedWindows.forEach(win => {
for (let browser of win.gBrowser.browsers) {
try {
browser.sendMessageToActor(
"PreferenceChanged",
{ isEnabled },
"RefreshBlocker",
"all"
);
} catch (ex) {}
}
});
},
},
};
/**
@ -514,6 +542,18 @@ let JSWINDOWACTORS = {
allFrames: true,
},
RefreshBlocker: {
parent: {
moduleURI: "resource:///actors/RefreshBlockerParent.jsm",
},
child: {
moduleURI: "resource:///actors/RefreshBlockerChild.jsm",
},
messageManagerGroups: ["browsers"],
enablePreference: "accessibility.blockautorefresh",
},
SearchTelemetry: {
parent: {
moduleURI: "resource:///actors/SearchTelemetryParent.jsm",

View File

@ -651,6 +651,9 @@ var ActorManagerParent = {
} else {
unregister(actorName, actor);
}
if (actor.onPreferenceChanged) {
actor.onPreferenceChanged(prefName, prevValue, isEnabled);
}
}
);
if (!this[actorNameProp]) {