Bug 989193 - Open captive portal automatically in a new tab when detected. r=MattN draft
authorNihanth Subramanya <nhnt11@gmail.com>
Wed, 23 Mar 2016 19:15:00 -0700
changeset 372618 57f9d33acfdb3077e679df283dee0382feeb6de4
parent 371529 7b2fc29e5d9db05f171dea679a3e5d50f8a9e2d6
child 372619 b6848fbc8096d5c122254526c5ab1d74da1406ce
push id19542
push usernhnt11@gmail.com
push dateSun, 29 May 2016 09:06:21 +0000
reviewersMattN
bugs989193
milestone49.0a1
Bug 989193 - Open captive portal automatically in a new tab when detected. r=MattN MozReview-Commit-ID: 2vbK1KQDgpd
browser/app/profile/firefox.js
browser/components/nsBrowserGlue.js
browser/modules/CaptivePortalWatcher.jsm
browser/modules/moz.build
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -572,16 +572,19 @@ pref("mousewheel.with_shift.action", 2);
 pref("mousewheel.with_meta.action", 1); // win key on Win, Super/Hyper on Linux
 #endif
 pref("mousewheel.with_control.action",3);
 pref("mousewheel.with_win.action", 1);
 
 pref("browser.xul.error_pages.enabled", true);
 pref("browser.xul.error_pages.expert_bad_cert", false);
 
+// Enable captive portal detection.
+pref("network.captive-portal-service.enabled", true);
+
 // If true, network link events will change the value of navigator.onLine
 pref("network.manage-offline-status", true);
 
 // We want to make sure mail URLs are handled externally...
 pref("network.protocol-handler.external.mailto", true); // for mail
 pref("network.protocol-handler.external.news", true);   // for news
 pref("network.protocol-handler.external.snews", true);  // for secure news
 pref("network.protocol-handler.external.nntp", true);   // also news
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -16,16 +16,19 @@ Cu.import("resource://gre/modules/AppCon
 Cu.import("resource://gre/modules/AsyncPrefs.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
                                   "resource:///modules/AboutHome.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AboutNewTab",
                                   "resource:///modules/AboutNewTab.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "CaptivePortalWatcher",
+                                  "resource:///modules/CaptivePortalWatcher.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "DirectoryLinksProvider",
                                   "resource:///modules/DirectoryLinksProvider.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
                                   "resource://gre/modules/NewTabUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabMessages",
                                   "resource:///modules/NewTabMessages.jsm");
@@ -1071,16 +1074,18 @@ BrowserGlue.prototype = {
         if (removalSuccessful && uninstalledValue == "True") {
           this._resetProfileNotification("uninstall");
         }
       }
     }
 
     this._checkForOldBuildUpdates();
 
+    CaptivePortalWatcher.init();
+
     this._firstWindowTelemetry(aWindow);
     this._firstWindowLoaded();
   },
 
   /**
    * Application shutdown handler.
    */
   _onQuitApplicationGranted: function () {
@@ -1096,16 +1101,18 @@ BrowserGlue.prototype = {
       appStartup.trackStartupCrashEnd();
     } catch (e) {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
     SelfSupportBackend.uninit();
     NewTabMessages.uninit();
 
+    CaptivePortalWatcher.uninit();
+
     AboutNewTab.uninit();
     webrtcUI.uninit();
     FormValidationHandler.uninit();
     if (AppConstants.NIGHTLY_BUILD) {
       AddonWatcher.uninit();
     }
   },
 
new file mode 100644
--- /dev/null
+++ b/browser/modules/CaptivePortalWatcher.jsm
@@ -0,0 +1,181 @@
+/* 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 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 _delayedAddCaptivePortalTab for more details. */
+const PORTAL_RECHECK_DELAY_MS = 150;
+// We listen for this observer notification to detect when a browser window
+// gains focus.
+const BROWSER_FOCUS_NOTIFICATION = "xul-window-visible";
+
+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 = {
+  // This holds a weak reference to the captive portal tab so that we
+  // don't leak it if the user closes it.
+  _captivePortalTab: null,
+
+  _initialized: false,
+
+  /**
+   * If a portal is detected when we don't have focus, we first wait for focus
+   * and then add the tab after a small delay. This is set to true while we wait
+   * so that in the unlikely event that we receive another notification while
+   * waiting, we can avoid adding a second tab.
+   */
+  _waitingToAddTab: false,
+
+  get canonicalURL() {
+    return Services.prefs.getCharPref("captivedetect.canonicalURL");
+  },
+
+  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._addCaptivePortalTab();
+    }
+  },
+
+  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");
+  },
+
+  observe(subject, topic, data) {
+    switch(topic) {
+      case "captive-portal-login":
+        this._addCaptivePortalTab();
+        break;
+      case "captive-portal-login-abort":
+      case "captive-portal-login-success":
+        this._captivePortalGone();
+        break;
+      case BROWSER_FOCUS_NOTIFICATION:
+        this._delayedAddCaptivePortalTab();
+        break;
+    }
+  },
+
+  _addCaptivePortalTab() {
+    if (this._waitingToAddTab) {
+      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.document.hasFocus()) {
+      this._waitingToAddTab = true;
+      Services.obs.addObserver(this, BROWSER_FOCUS_NOTIFICATION, false);
+      return;
+    }
+
+    // The browser is in use - add the tab without selecting it.
+    let tab = win.gBrowser.addTab(this.canonicalURL);
+    this._captivePortalTab = Cu.getWeakReference(tab);
+    return;
+  },
+
+  /**
+   * 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.
+   */
+  _delayedAddCaptivePortalTab() {
+    if (!this._waitingToAddTab) {
+      return;
+    }
+
+    let win = RecentWindow.getMostRecentBrowserWindow();
+    if (!win.document.hasFocus()) {
+      // The document that got focused was not in a browser window.
+      return;
+    }
+    Services.obs.removeObserver(this, BROWSER_FOCUS_NOTIFICATION);
+
+    // Trigger a portal recheck. The user may have logged into the portal via
+    // another client, or changed networks.
+    let lastChecked = cps.lastChecked;
+    cps.recheckCaptivePortal();
+
+    // We wait for PORTAL_RECHECK_DELAY_MS after the trigger.
+    // - If the portal is no longer locked, we don't need to add a tab.
+    // - If it is, the delay is chosen to not be extremely noticeable.
+    setTimeout(() => {
+      this._waitingToAddTab = false;
+      if (cps.state != cps.LOCKED_PORTAL) {
+        // We're free of the portal!
+        return;
+      }
+
+      let tab = win.gBrowser.addTab(this.canonicalURL);
+      // Focus the tab only if the recheck has completed, i.e. we're sure
+      // that the portal is still locked. This way, if the recheck completes
+      // after we add the tab and we're free of the portal, the tab contents
+      // won't flicker.
+      if (cps.lastChecked != lastChecked) {
+        win.gBrowser.selectedTab = tab;
+      }
+
+      this._captivePortalTab = Cu.getWeakReference(tab);
+    }, PORTAL_RECHECK_DELAY_MS);
+  },
+
+  _captivePortalGone() {
+    if (this._waitingToAddTab) {
+      Services.obs.removeObserver(this, BROWSER_FOCUS_NOTIFICATION);
+      this._waitingToAddTab = false;
+    }
+
+    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.ownerDocument.defaultView.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);
+  },
+};
\ No newline at end of file
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -9,16 +9,17 @@ XPCSHELL_TESTS_MANIFESTS += [
     'test/unit/social/xpcshell.ini',
     'test/xpcshell/xpcshell.ini',
 ]
 
 EXTRA_JS_MODULES += [
     'AboutHome.jsm',
     'AboutNewTab.jsm',
     'BrowserUITelemetry.jsm',
+    'CaptivePortalWatcher.jsm',
     'CastingApps.jsm',
     'Chat.jsm',
     'ContentClick.jsm',
     'ContentCrashHandlers.jsm',
     'ContentLinkHandler.jsm',
     'ContentObservers.jsm',
     'ContentSearch.jsm',
     'ContentWebRTC.jsm',