Bug 1313568 - Handle captive portal UI in per-window script. r=MattN
authorNihanth Subramanya <nhnt11@gmail.com>
Sun, 08 Jan 2017 03:23:09 +0100
changeset 377766 7957edd71a116c3dbb3132985e9e3f8d0de2a3e1
parent 377765 706dc77f15545cbd4322b551fa4e11766e348838
child 377767 e8a4e520dcb76bbf3a6813afcbecd3e769c4c4f2
push id1419
push userjlund@mozilla.com
push dateMon, 10 Apr 2017 20:44:07 +0000
treeherdermozilla-release@5e6801b73ef6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1313568
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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