diff options
Diffstat (limited to '')
-rw-r--r-- | browser/base/content/browser-captivePortal.js | 382 |
1 files changed, 382 insertions, 0 deletions
diff --git a/browser/base/content/browser-captivePortal.js b/browser/base/content/browser-captivePortal.js new file mode 100644 index 0000000000..71bd4449ed --- /dev/null +++ b/browser/base/content/browser-captivePortal.js @@ -0,0 +1,382 @@ +/* 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/. */ + +var CaptivePortalWatcher = { + // 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, + + /** + * 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, + + // This holds a weak reference to the captive portal tab so we can close the tab + // after successful login if we're redirected to the canonicalURL. + _previousCaptivePortalTab: null, + + get _captivePortalNotification() { + return gNotificationBox.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"); + Services.obs.addObserver(this, "captive-portal-login-abort"); + Services.obs.addObserver(this, "captive-portal-login-success"); + + this._cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService( + Ci.nsICaptivePortalService + ); + + if (this._cps.state == this._cps.LOCKED_PORTAL) { + // A captive portal has already been detected. + this._captivePortalDetected(); + + // Automatically open a captive portal tab if there's no other browser window. + if (BrowserWindowTracker.windowCount == 1) { + this.ensureCaptivePortalTab(); + } + } else if (this._cps.state == this._cps.UNKNOWN) { + // We trigger a portal check after delayed startup to avoid doing a network + // request before first paint. + this._delayedRecheckPending = true; + } + + // 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. + XPCOMUtils.defineLazyPreferenceGetter( + this, + "PORTAL_RECHECK_DELAY_MS", + "captivedetect.portalRecheckDelayMS", + 500 + ); + }, + + uninit() { + Services.obs.removeObserver(this, "captive-portal-login"); + Services.obs.removeObserver(this, "captive-portal-login-abort"); + Services.obs.removeObserver(this, "captive-portal-login-success"); + + this._cancelDelayedCaptivePortal(); + }, + + delayedStartup() { + if (this._delayedRecheckPending) { + delete this._delayedRecheckPending; + this._cps.recheckCaptivePortal(); + } + }, + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "captive-portal-login": + this._captivePortalDetected(); + break; + case "captive-portal-login-abort": + this._captivePortalGone(false); + break; + case "captive-portal-login-success": + this._captivePortalGone(true); + break; + case "delayed-captive-portal-handled": + this._cancelDelayedCaptivePortal(); + break; + } + }, + + onLocationChange(browser) { + if (!this._previousCaptivePortalTab) { + return; + } + + let tab = this._previousCaptivePortalTab.get(); + if (!tab || !tab.linkedBrowser) { + return; + } + + if (browser != tab.linkedBrowser) { + return; + } + + // There is a race between the release of captive portal i.e. + // the time when success/abort events are fired and the time when + // the captive portal tab redirects to the canonicalURL. We check for + // both conditions to be true and also check that we haven't already removed + // the captive portal tab in the success/abort event handlers before we remove + // it in the callback below. A tick is added to avoid removing the tab before + // onLocationChange handlers across browser code are executed. + Services.tm.dispatchToMainThread(() => { + if (!this._previousCaptivePortalTab) { + return; + } + + tab = this._previousCaptivePortalTab.get(); + let canonicalURI = Services.io.newURI(this.canonicalURL); + if ( + tab && + tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI) && + (this._cps.state == this._cps.UNLOCKED_PORTAL || + this._cps.state == this._cps.UNKNOWN) + ) { + gBrowser.removeTab(tab); + } + }); + }, + + _captivePortalDetected() { + if (this._delayedCaptivePortalDetectedInProgress) { + return; + } + + // Add an explicit permission for the last detected URI such that https-only / https-first do not + // attempt to upgrade the URI to https when following the "open network login page" button. + // We set explicit permissions for regular and private browsing windows to keep permissions + // separate. + let canonicalURI = Services.io.newURI(this.canonicalURL); + let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + let principal = Services.scriptSecurityManager.createContentPrincipal( + canonicalURI, + { + userContextId: gBrowser.contentPrincipal.userContextId, + privateBrowsingId: isPrivate ? 1 : 0, + } + ); + Services.perms.addFromPrincipal( + principal, + "https-only-load-insecure", + Ci.nsIPermissionManager.ALLOW_ACTION, + Ci.nsIPermissionManager.EXPIRE_SESSION + ); + let win = BrowserWindowTracker.getTopWindow(); + // Used by tests: ignore the main test window in order to enable testing of + // the case where we have no open windows. + if (win.document.documentElement.getAttribute("ignorecaptiveportal")) { + win = null; + } + + // 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.focus.activeWindow) { + this._delayedCaptivePortalDetectedInProgress = true; + window.addEventListener("activate", this, { once: true }); + Services.obs.addObserver(this, "delayed-captive-portal-handled"); + } + + 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() { + if (!this._delayedCaptivePortalDetectedInProgress) { + return; + } + + // Used by tests: ignore the main test window in order to enable testing of + // the case where we have no open windows. + if (window.document.documentElement.getAttribute("ignorecaptiveportal")) { + return; + } + + Services.obs.notifyObservers(null, "delayed-captive-portal-handled"); + + // Trigger a portal recheck. The user may have logged into the portal via + // another client, or changed networks. + this._cps.recheckCaptivePortal(); + this._waitingForRecheck = true; + let requestTime = Date.now(); + + let observer = () => { + let time = Date.now() - requestTime; + Services.obs.removeObserver(observer, "captive-portal-check-complete"); + this._waitingForRecheck = false; + if (this._cps.state != this._cps.LOCKED_PORTAL) { + // We're free of the portal! + return; + } + + if (time <= this.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. + this.ensureCaptivePortalTab(); + } + }; + Services.obs.addObserver(observer, "captive-portal-check-complete"); + }, + + _captivePortalGone(aSuccess) { + this._cancelDelayedCaptivePortal(); + this._removeNotification(); + + if (!this._captivePortalTab) { + return; + } + + let tab = this._captivePortalTab.get(); + let canonicalURI = Services.io.newURI(this.canonicalURL); + if ( + tab && + tab.linkedBrowser && + tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI) + ) { + this._previousCaptivePortalTab = null; + gBrowser.removeTab(tab); + } + this._captivePortalTab = null; + }, + + _cancelDelayedCaptivePortal() { + if (this._delayedCaptivePortalDetectedInProgress) { + this._delayedCaptivePortalDetectedInProgress = false; + Services.obs.removeObserver(this, "delayed-captive-portal-handled"); + window.removeEventListener("activate", this); + } + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "activate": + this._delayedCaptivePortalDetected(); + break; + case "TabSelect": + if (!this._captivePortalTab || !this._captivePortalNotification) { + break; + } + + let tab = this._captivePortalTab.get(); + let n = this._captivePortalNotification; + if (!tab || !n) { + break; + } + + let doc = tab.ownerDocument; + let button = n.buttonContainer.querySelector( + "button.notification-button" + ); + if (doc.defaultView.gBrowser.selectedTab == tab) { + button.style.visibility = "hidden"; + } else { + button.style.visibility = "visible"; + } + break; + } + }, + + _showNotification() { + if (this._captivePortalNotification) { + return; + } + + let buttons = [ + { + label: this._browserBundle.GetStringFromName( + "captivePortal.showLoginPage2" + ), + callback: () => { + this.ensureCaptivePortalTab(); + + Services.telemetry.recordEvent( + "networking.captive_portal", + "login_button_pressed", + "login_button" + ); + + // Returning true prevents the notification from closing. + return true; + }, + }, + ]; + + let message = this._browserBundle.GetStringFromName( + "captivePortal.infoMessage3" + ); + + let closeHandler = aEventName => { + if (aEventName != "removed") { + return; + } + gBrowser.tabContainer.removeEventListener("TabSelect", this); + }; + + gNotificationBox.appendNotification( + this.PORTAL_NOTIFICATION_VALUE, + { + label: message, + priority: gNotificationBox.PRIORITY_INFO_MEDIUM, + eventCallback: closeHandler, + }, + buttons + ); + + Services.telemetry.recordEvent( + "networking.captive_portal", + "login_infobar_shown", + "infobar" + ); + + gBrowser.tabContainer.addEventListener("TabSelect", this); + }, + + _removeNotification() { + 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.addWebTab(this.canonicalURL, { + ownerTab: gBrowser.selectedTab, + triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( + { + userContextId: gBrowser.contentPrincipal.userContextId, + } + ), + disableTRR: true, + }); + this._captivePortalTab = Cu.getWeakReference(tab); + this._previousCaptivePortalTab = Cu.getWeakReference(tab); + } + + gBrowser.selectedTab = tab; + }, +}; |