Bug 1313568 - Handle captive portal UI in per-window script. r=MattN draft
authorNihanth Subramanya <nhnt11@gmail.com>
Sun, 08 Jan 2017 03:23:09 +0100
changeset 464606 c6c1321b591fdbd870ff40374477bbc05fbcb27c
parent 464206 3cedab21a7e65e6a1c4c2294ecfb5502575a46e3
child 464607 b827197f5f71d81c664ea556e47a5518d482a60d
push id42374
push usernhnt11@gmail.com
push dateSat, 21 Jan 2017 04:39:45 +0000
reviewersMattN
bugs1313568
milestone53.0a1
Bug 1313568 - Handle captive portal UI in per-window script. r=MattN MozReview-Commit-ID: FxjE2NblJe4
browser/base/content/browser-captivePortal.js
browser/base/content/browser.js
browser/base/content/global-scripts.inc
browser/base/content/test/captivePortal/browser_captivePortal_certErrorUI.js
browser/base/jar.mn
browser/modules/test/browser_CaptivePortalWatcher.js
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