diff --git a/browser/base/content/content-refreshblocker.js b/browser/actors/RefreshBlockerChild.jsm similarity index 61% rename from browser/base/content/content-refreshblocker.js rename to browser/actors/RefreshBlockerChild.jsm index 5e31f8375633..030b88114e47 100644 --- a/browser/base/content/content-refreshblocker.js +++ b/browser/actors/RefreshBlockerChild.jsm @@ -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 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); + } +} diff --git a/browser/actors/RefreshBlockerParent.jsm b/browser/actors/RefreshBlockerParent.jsm new file mode 100644 index 000000000000..5da938ad1485 --- /dev/null +++ b/browser/actors/RefreshBlockerParent.jsm @@ -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); + } + } + } + } +} diff --git a/browser/actors/moz.build b/browser/actors/moz.build index 09abb15b855d..d957a93760df 100644 --- a/browser/actors/moz.build +++ b/browser/actors/moz.build @@ -72,6 +72,8 @@ FINAL_TARGET_FILES.actors += [ 'PluginChild.jsm', 'PluginParent.jsm', 'PromptParent.jsm', + 'RefreshBlockerChild.jsm', + 'RefreshBlockerParent.jsm', 'RFPHelperChild.jsm', 'RFPHelperParent.jsm', 'SearchTelemetryChild.jsm', diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 6e25542069ed..b088e0a37250 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -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. diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js index bf53bf7ee519..1f3c35e04e00 100644 --- a/browser/base/content/tabbrowser.js +++ b/browser/base/content/tabbrowser.js @@ -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; diff --git a/browser/base/jar.mn b/browser/base/jar.mn index f7fea4ecd666..5cf7af4f635d 100644 --- a/browser/base/jar.mn +++ b/browser/base/jar.mn @@ -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) diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm index 68b77e6f42cf..6cc3f2b13f88 100644 --- a/browser/components/BrowserGlue.jsm +++ b/browser/components/BrowserGlue.jsm @@ -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", diff --git a/toolkit/modules/ActorManagerParent.jsm b/toolkit/modules/ActorManagerParent.jsm index 02c4429021be..3ef91f4ac5b8 100644 --- a/toolkit/modules/ActorManagerParent.jsm +++ b/toolkit/modules/ActorManagerParent.jsm @@ -651,6 +651,9 @@ var ActorManagerParent = { } else { unregister(actorName, actor); } + if (actor.onPreferenceChanged) { + actor.onPreferenceChanged(prefName, prevValue, isEnabled); + } } ); if (!this[actorNameProp]) {