Bug 1313568 - Handle captive portal UI in per-window script. r=MattN
MozReview-Commit-ID: FxjE2NblJe4
copy from browser/modules/CaptivePortalWatcher.jsm
copy to browser/base/content/browser-captivePortal.js
--- a/browser/modules/CaptivePortalWatcher.jsm
+++ b/browser/base/content/browser-captivePortal.js
@@ -1,89 +1,90 @@
/* 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";
-
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
-
-this.EXPORTED_SYMBOLS = [ "CaptivePortalWatcher" ];
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Timer.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource:///modules/RecentWindow.jsm");
-
XPCOMUtils.defineLazyServiceGetter(this, "cps",
"@mozilla.org/network/captive-portal-service;1",
"nsICaptivePortalService");
-this.CaptivePortalWatcher = {
+var CaptivePortalWatcher = {
/**
* This constant is chosen to be large enough for a portal recheck to complete,
* and small enough that the delay in opening a tab isn't too noticeable.
* Please see comments for _delayedCaptivePortalDetected for more details.
*/
- PORTAL_RECHECK_DELAY_MS: 150,
+ PORTAL_RECHECK_DELAY_MS: Preferences.get("captivedetect.portalRecheckDelayMS", 500),
// This is the value used to identify the captive portal notification.
PORTAL_NOTIFICATION_VALUE: "captive-portal-detected",
// This holds a weak reference to the captive portal tab so that we
// don't leak it if the user closes it.
_captivePortalTab: null,
- // This holds a weak reference to the captive portal notification.
- _captivePortalNotification: null,
-
- _initialized: false,
-
/**
* If a portal is detected when we don't have focus, we first wait for focus
* and then add the tab if, after a recheck, the portal is still active. This
* is set to true while we wait so that in the unlikely event that we receive
* another notification while waiting, we don't do things twice.
*/
_delayedCaptivePortalDetectedInProgress: false,
// In the situation above, this is set to true while we wait for the recheck.
// This flag exists so that tests can appropriately simulate a recheck.
_waitingForRecheck: false,
+ get _captivePortalNotification() {
+ let nb = document.getElementById("high-priority-global-notificationbox");
+ return nb.getNotificationWithValue(this.PORTAL_NOTIFICATION_VALUE);
+ },
+
get canonicalURL() {
return Services.prefs.getCharPref("captivedetect.canonicalURL");
},
+ get _browserBundle() {
+ delete this._browserBundle;
+ return this._browserBundle =
+ Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ },
+
init() {
Services.obs.addObserver(this, "captive-portal-login", false);
Services.obs.addObserver(this, "captive-portal-login-abort", false);
Services.obs.addObserver(this, "captive-portal-login-success", false);
- this._initialized = true;
if (cps.state == cps.LOCKED_PORTAL) {
// A captive portal has already been detected.
this._captivePortalDetected();
- return;
+
+ // Automatically open a captive portal tab if there's no other browser window.
+ let windows = Services.wm.getEnumerator("navigator:browser");
+ if (windows.getNext() == window && !windows.hasMoreElements()) {
+ this.ensureCaptivePortalTab();
+ }
}
cps.recheckCaptivePortal();
},
uninit() {
- if (!this._initialized) {
- return;
- }
Services.obs.removeObserver(this, "captive-portal-login");
Services.obs.removeObserver(this, "captive-portal-login-abort");
Services.obs.removeObserver(this, "captive-portal-login-success");
+
+
+ if (this._delayedCaptivePortalDetectedInProgress) {
+ Services.obs.removeObserver(this, "xul-window-visible");
+ }
},
- observe(subject, topic, data) {
- switch (topic) {
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
case "captive-portal-login":
this._captivePortalDetected();
break;
case "captive-portal-login-abort":
case "captive-portal-login-success":
this._captivePortalGone();
break;
case "xul-window-visible":
@@ -93,43 +94,27 @@ this.CaptivePortalWatcher = {
},
_captivePortalDetected() {
if (this._delayedCaptivePortalDetectedInProgress) {
return;
}
let win = RecentWindow.getMostRecentBrowserWindow();
- // If there's no browser window or none have focus, open and show the
- // tab when we regain focus. This is so that if a different application was
- // focused, when the user (re-)focuses a browser window, we open the tab
- // immediately in that window so they can login before continuing to browse.
- if (!win || win != Services.ww.activeWindow) {
+ // If no browser window has focus, open and show the tab when we regain focus.
+ // This is so that if a different application was focused, when the user
+ // (re-)focuses a browser window, we open the tab immediately in that window
+ // so they can log in before continuing to browse.
+ if (win != Services.ww.activeWindow) {
this._delayedCaptivePortalDetectedInProgress = true;
Services.obs.addObserver(this, "xul-window-visible", false);
return;
}
- this._showNotification(win);
- },
-
- _ensureCaptivePortalTab(win) {
- let tab;
- if (this._captivePortalTab) {
- tab = this._captivePortalTab.get();
- }
-
- // If the tab is gone or going, we need to open a new one.
- if (!tab || tab.closing || !tab.parentNode) {
- tab = win.gBrowser.addTab(this.canonicalURL,
- { ownerTab: win.gBrowser.selectedTab });
- this._captivePortalTab = Cu.getWeakReference(tab);
- }
-
- win.gBrowser.selectedTab = tab;
+ this._showNotification();
},
/**
* Called after we regain focus if we detect a portal while a browser window
* doesn't have focus. Triggers a portal recheck to reaffirm state, and adds
* the tab if needed after a short delay to allow the recheck to complete.
*/
_delayedCaptivePortalDetected() {
@@ -138,141 +123,137 @@ this.CaptivePortalWatcher = {
}
let win = RecentWindow.getMostRecentBrowserWindow();
if (win != Services.ww.activeWindow) {
// The window that got focused was not a browser window.
return;
}
Services.obs.removeObserver(this, "xul-window-visible");
+ this._delayedCaptivePortalDetectedInProgress = false;
+ if (win != window) {
+ // Some other browser window got focus, we don't have to do anything.
+ return;
+ }
// Trigger a portal recheck. The user may have logged into the portal via
// another client, or changed networks.
cps.recheckCaptivePortal();
this._waitingForRecheck = true;
let requestTime = Date.now();
let self = this;
Services.obs.addObserver(function observer() {
let time = Date.now() - requestTime;
Services.obs.removeObserver(observer, "captive-portal-check-complete");
self._waitingForRecheck = false;
- self._delayedCaptivePortalDetectedInProgress = false;
if (cps.state != cps.LOCKED_PORTAL) {
// We're free of the portal!
return;
}
- self._showNotification(win);
+ self._showNotification();
if (time <= self.PORTAL_RECHECK_DELAY_MS) {
// The amount of time elapsed since we requested a recheck (i.e. since
// the browser window was focused) was small enough that we can add and
// focus a tab with the login page with no noticeable delay.
- self._ensureCaptivePortalTab(win);
+ self.ensureCaptivePortalTab();
}
}, "captive-portal-check-complete", false);
},
_captivePortalGone() {
if (this._delayedCaptivePortalDetectedInProgress) {
Services.obs.removeObserver(this, "xul-window-visible");
this._delayedCaptivePortalDetectedInProgress = false;
}
this._removeNotification();
-
- if (!this._captivePortalTab) {
- return;
- }
-
- let tab = this._captivePortalTab.get();
- // In all the cases below, we want to stop treating the tab as a
- // captive portal tab.
- this._captivePortalTab = null;
-
- // Check parentNode in case the object hasn't been gc'd yet.
- if (!tab || tab.closing || !tab.parentNode) {
- // User has closed the tab already.
- return;
- }
-
- let tabbrowser = tab.ownerGlobal.gBrowser;
-
- // If after the login, the captive portal has redirected to some other page,
- // leave it open if the tab has focus.
- if (tab.linkedBrowser.currentURI.spec != this.canonicalURL &&
- tabbrowser.selectedTab == tab) {
- return;
- }
-
- // Remove the tab.
- tabbrowser.removeTab(tab);
- },
-
- get _browserBundle() {
- delete this._browserBundle;
- return this._browserBundle =
- Services.strings.createBundle("chrome://browser/locale/browser.properties");
},
handleEvent(aEvent) {
if (aEvent.type != "TabSelect" || !this._captivePortalTab || !this._captivePortalNotification) {
return;
}
let tab = this._captivePortalTab.get();
- let n = this._captivePortalNotification.get();
+ let n = this._captivePortalNotification;
if (!tab || !n) {
return;
}
let doc = tab.ownerDocument;
let button = n.querySelector("button.notification-button");
if (doc.defaultView.gBrowser.selectedTab == tab) {
button.style.visibility = "hidden";
} else {
button.style.visibility = "visible";
}
},
- _showNotification(win) {
+ _showNotification() {
let buttons = [
{
label: this._browserBundle.GetStringFromName("captivePortal.showLoginPage"),
callback: () => {
- this._ensureCaptivePortalTab(win);
+ this.ensureCaptivePortalTab();
// Returning true prevents the notification from closing.
return true;
},
isDefault: true,
},
];
let message = this._browserBundle.GetStringFromName("captivePortal.infoMessage2");
let closeHandler = (aEventName) => {
if (aEventName != "removed") {
return;
}
- win.gBrowser.tabContainer.removeEventListener("TabSelect", this);
+ gBrowser.tabContainer.removeEventListener("TabSelect", this);
};
- let nb = win.document.getElementById("high-priority-global-notificationbox");
- let n = nb.appendNotification(message, this.PORTAL_NOTIFICATION_VALUE, "",
- nb.PRIORITY_INFO_MEDIUM, buttons, closeHandler);
+ let nb = document.getElementById("high-priority-global-notificationbox");
+ nb.appendNotification(message, this.PORTAL_NOTIFICATION_VALUE, "",
+ nb.PRIORITY_INFO_MEDIUM, buttons, closeHandler);
- this._captivePortalNotification = Cu.getWeakReference(n);
-
- win.gBrowser.tabContainer.addEventListener("TabSelect", this);
+ gBrowser.tabContainer.addEventListener("TabSelect", this);
},
_removeNotification() {
- if (!this._captivePortalNotification)
- return;
- let n = this._captivePortalNotification.get();
- this._captivePortalNotification = null;
+ let n = this._captivePortalNotification;
if (!n || !n.parentNode) {
return;
}
n.close();
},
+
+ ensureCaptivePortalTab() {
+ let tab;
+ if (this._captivePortalTab) {
+ tab = this._captivePortalTab.get();
+ }
+
+ // If the tab is gone or going, we need to open a new one.
+ if (!tab || tab.closing || !tab.parentNode) {
+ tab = gBrowser.addTab(this.canonicalURL, { ownerTab: gBrowser.selectedTab });
+ this._captivePortalTab = Cu.getWeakReference(tab);
+ }
+
+ gBrowser.selectedTab = tab;
+
+ let canonicalURI = makeURI(this.canonicalURL);
+
+ // When we are no longer captive, close the tab if it's at the canonical URL.
+ let tabCloser = () => {
+ Services.obs.removeObserver(tabCloser, "captive-portal-login-abort");
+ Services.obs.removeObserver(tabCloser, "captive-portal-login-success");
+ if (!tab || tab.closing || !tab.parentNode || !tab.linkedBrowser ||
+ !tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI)) {
+ return;
+ }
+ gBrowser.removeTab(tab);
+ }
+ Services.obs.addObserver(tabCloser, "captive-portal-login-abort", false);
+ Services.obs.addObserver(tabCloser, "captive-portal-login-success", false);
+ },
};
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1004,16 +1004,17 @@ var gBrowserInit = {
gPageStyleMenu.init();
LanguageDetectionListener.init();
BrowserOnClick.init();
FeedHandler.init();
CompactTheme.init();
AboutPrivateBrowsingListener.init();
TrackingProtection.init();
RefreshBlocker.init();
+ CaptivePortalWatcher.init();
let mm = window.getGroupMessageManager("browsers");
mm.loadFrameScript("chrome://browser/content/tab-content.js", true);
mm.loadFrameScript("chrome://browser/content/content.js", true);
mm.loadFrameScript("chrome://browser/content/content-UITour.js", true);
mm.loadFrameScript("chrome://global/content/manifestMessages.js", true);
// initialize observers and listeners
@@ -1531,16 +1532,18 @@ var gBrowserInit = {
FeedHandler.uninit();
CompactTheme.uninit();
TrackingProtection.uninit();
RefreshBlocker.uninit();
+ CaptivePortalWatcher.uninit();
+
gMenuButtonUpdateBadge.uninit();
gMenuButtonBadgeManager.uninit();
SidebarUI.uninit();
// Now either cancel delayedStartup, or clean up the services initialized from
// it.
@@ -2847,17 +2850,17 @@ var BrowserOnClick = {
receiveMessage(msg) {
switch (msg.name) {
case "Browser:CertExceptionError":
this.onCertError(msg.target, msg.data.elementId,
msg.data.isTopFrame, msg.data.location,
msg.data.securityInfoAsString);
break;
case "Browser:OpenCaptivePortalPage":
- this.onOpenCaptivePortalPage();
+ CaptivePortalWatcher.ensureCaptivePortalTab();
break;
case "Browser:SiteBlockedError":
this.onAboutBlocked(msg.data.elementId, msg.data.reason,
msg.data.isTopFrame, msg.data.location);
break;
case "Browser:EnableOnlineMode":
if (Services.io.offline) {
// Reset network state and refresh the page.
@@ -2983,38 +2986,16 @@ var BrowserOnClick = {
let detailedInfo = getDetailedCertErrorInfo(location,
securityInfo);
gClipboardHelper.copyString(detailedInfo);
break;
}
},
- onOpenCaptivePortalPage() {
- // Open a new tab with the canonical URL that we use to check for a captive portal.
- // It will be redirected to the login page.
- let canonicalURL = Services.prefs.getCharPref("captivedetect.canonicalURL");
- let tab = gBrowser.addTab(canonicalURL);
- let canonicalURI = makeURI(canonicalURL);
- gBrowser.selectedTab = tab;
-
- // When we are no longer captive, close the tab if it's at the canonical URL.
- let tabCloser = () => {
- Services.obs.removeObserver(tabCloser, "captive-portal-login-abort");
- Services.obs.removeObserver(tabCloser, "captive-portal-login-success");
- if (!tab || tab.closing || !tab.parentNode || !tab.linkedBrowser ||
- !tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI)) {
- return;
- }
- gBrowser.removeTab(tab);
- }
- Services.obs.addObserver(tabCloser, "captive-portal-login-abort", false);
- Services.obs.addObserver(tabCloser, "captive-portal-login-success", false);
- },
-
onAboutBlocked(elementId, reason, isTopFrame, location) {
// Depending on what page we are displaying here (malware/phishing/unwanted)
// use the right strings and links for each.
let bucketName = "";
let sendTelemetry = false;
if (reason === "malware") {
sendTelemetry = true;
bucketName = "WARNING_MALWARE_PAGE_";
--- a/browser/base/content/global-scripts.inc
+++ b/browser/base/content/global-scripts.inc
@@ -6,16 +6,17 @@
<script type="application/javascript" src="chrome://global/content/printUtils.js"/>
<script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
<script type="application/javascript" src="chrome://browser/content/places/browserPlacesViews.js"/>
<script type="application/javascript" src="chrome://browser/content/browser.js"/>
<script type="application/javascript" src="chrome://browser/content/customizableui/panelUI.js"/>
<script type="application/javascript" src="chrome://global/content/viewSourceUtils.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-addons.js"/>
+<script type="application/javascript" src="chrome://browser/content/browser-captivePortal.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-ctrlTab.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-customization.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-compacttheme.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-feeds.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-fullScreenAndPointerLock.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-fullZoom.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-gestureSupport.js"/>
<script type="application/javascript" src="chrome://browser/content/browser-media.js"/>
--- a/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js
+++ b/browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js
@@ -47,16 +47,29 @@ add_task(function* checkCaptivePortalCer
info("Clicking the Open Login Page button.");
doc.getElementById("openPortalLoginPageButton").click();
});
let portalTab = yield portalTabPromise;
is(gBrowser.selectedTab, portalTab, "Login page should be open in a new foreground tab.");
+ // Make sure clicking the "Open Login Page" button again focuses the existing portal tab.
+ yield BrowserTestUtils.switchTab(gBrowser, errorTab);
+ // Passing an empty function to BrowserTestUtils.switchTab lets us wait for an arbitrary
+ // tab switch.
+ portalTabPromise = BrowserTestUtils.switchTab(gBrowser, () => {});
+ yield ContentTask.spawn(browser, null, () => {
+ info("Clicking the Open Login Page button.");
+ content.document.getElementById("openPortalLoginPageButton").click();
+ });
+
+ let portalTab2 = yield portalTabPromise;
+ is(portalTab2, portalTab, "The existing portal tab should be focused.");
+
let portalTabRemoved = BrowserTestUtils.removeTab(portalTab, {dontRemove: true});
let errorTabReloaded = waitForCertErrorLoad(browser);
Services.obs.notifyObservers(null, "captive-portal-login-success", null);
yield portalTabRemoved;
info("Waiting for error tab to be reloaded after the captive portal was freed.");
yield errorTabReloaded;
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -61,16 +61,17 @@ browser.jar:
content/browser/aboutProviderDirectory.xhtml (content/aboutProviderDirectory.xhtml)
content/browser/aboutTabCrashed.css (content/aboutTabCrashed.css)
content/browser/aboutTabCrashed.js (content/aboutTabCrashed.js)
content/browser/aboutTabCrashed.xhtml (content/aboutTabCrashed.xhtml)
* content/browser/browser.css (content/browser.css)
content/browser/browser.js (content/browser.js)
* content/browser/browser.xul (content/browser.xul)
content/browser/browser-addons.js (content/browser-addons.js)
+ content/browser/browser-captivePortal.js (content/browser-captivePortal.js)
content/browser/browser-ctrlTab.js (content/browser-ctrlTab.js)
content/browser/browser-customization.js (content/browser-customization.js)
content/browser/browser-data-submission-info-bar.js (content/browser-data-submission-info-bar.js)
content/browser/browser-compacttheme.js (content/browser-compacttheme.js)
content/browser/browser-feeds.js (content/browser-feeds.js)
content/browser/browser-fullScreenAndPointerLock.js (content/browser-fullScreenAndPointerLock.js)
content/browser/browser-fullZoom.js (content/browser-fullZoom.js)
content/browser/browser-fxaccounts.js (content/browser-fxaccounts.js)
--- a/browser/modules/test/browser_CaptivePortalWatcher.js
+++ b/browser/modules/test/browser_CaptivePortalWatcher.js
@@ -200,25 +200,30 @@ let testCasesForBothSuccessAndAbort = [
});
ensureNoPortalTab(win);
ensureNoPortalNotification(win);
yield closeWindowAndWaitForXulWindowVisible(win);
},
/**
* A portal is detected when a browser window has focus. No portal tab should
- * be opened. A notification bar should be displayed in the focused window.
+ * be opened. A notification bar should be displayed in all browser windows.
*/
function* test_detectedWithFocus(aSuccess) {
- let win = RecentWindow.getMostRecentBrowserWindow();
+ let win1 = RecentWindow.getMostRecentBrowserWindow();
+ let win2 = yield BrowserTestUtils.openNewBrowserWindow();
yield portalDetected();
- ensureNoPortalTab(win);
- ensurePortalNotification(win);
+ ensureNoPortalTab(win1);
+ ensureNoPortalTab(win2);
+ ensurePortalNotification(win1);
+ ensurePortalNotification(win2);
yield freePortal(aSuccess);
- ensureNoPortalNotification(win);
+ ensureNoPortalNotification(win1);
+ ensureNoPortalNotification(win2);
+ yield closeWindowAndWaitForXulWindowVisible(win2);
},
];
let singleRunTestCases = [
/**
* A portal is detected when there's no browser window,
* then a browser window is opened, and the portal is logged into
* and redirects to a different page. The portal tab should be added