summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-captivePortal.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/browser-captivePortal.js')
-rw-r--r--browser/base/content/browser-captivePortal.js382
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;
+ },
+};