Merge mozilla-central to autoland. a=merge CLOSED TREE
[gecko.git] / browser / base / content / browser-captivePortal.js
blob1fd5497273faf8f3e1804b6babe5e18127dc74ed
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 var CaptivePortalWatcher = {
6   // This is the value used to identify the captive portal notification.
7   PORTAL_NOTIFICATION_VALUE: "captive-portal-detected",
9   // This holds a weak reference to the captive portal tab so that we
10   // don't leak it if the user closes it.
11   _captivePortalTab: null,
13   /**
14    * If a portal is detected when we don't have focus, we first wait for focus
15    * and then add the tab if, after a recheck, the portal is still active. This
16    * is set to true while we wait so that in the unlikely event that we receive
17    * another notification while waiting, we don't do things twice.
18    */
19   _delayedCaptivePortalDetectedInProgress: false,
21   // In the situation above, this is set to true while we wait for the recheck.
22   // This flag exists so that tests can appropriately simulate a recheck.
23   _waitingForRecheck: false,
25   // This holds a weak reference to the captive portal tab so we can close the tab
26   // after successful login if we're redirected to the canonicalURL.
27   _previousCaptivePortalTab: null,
29   // Stores the time at which the banner was displayed
30   _bannerDisplayTime: Date.now(),
32   get _captivePortalNotification() {
33     return gNotificationBox.getNotificationWithValue(
34       this.PORTAL_NOTIFICATION_VALUE
35     );
36   },
38   get canonicalURL() {
39     return Services.prefs.getCharPref("captivedetect.canonicalURL");
40   },
42   get _browserBundle() {
43     delete this._browserBundle;
44     return (this._browserBundle = Services.strings.createBundle(
45       "chrome://browser/locale/browser.properties"
46     ));
47   },
49   init() {
50     Services.obs.addObserver(this, "captive-portal-login");
51     Services.obs.addObserver(this, "captive-portal-login-abort");
52     Services.obs.addObserver(this, "captive-portal-login-success");
54     this._cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService(
55       Ci.nsICaptivePortalService
56     );
58     if (this._cps.state == this._cps.LOCKED_PORTAL) {
59       // A captive portal has already been detected.
60       this._captivePortalDetected();
62       // Automatically open a captive portal tab if there's no other browser window.
63       if (BrowserWindowTracker.windowCount == 1) {
64         this.ensureCaptivePortalTab();
65       }
66     } else if (this._cps.state == this._cps.UNKNOWN) {
67       // We trigger a portal check after delayed startup to avoid doing a network
68       // request before first paint.
69       this._delayedRecheckPending = true;
70     }
72     // This constant is chosen to be large enough for a portal recheck to complete,
73     // and small enough that the delay in opening a tab isn't too noticeable.
74     // Please see comments for _delayedCaptivePortalDetected for more details.
75     XPCOMUtils.defineLazyPreferenceGetter(
76       this,
77       "PORTAL_RECHECK_DELAY_MS",
78       "captivedetect.portalRecheckDelayMS",
79       500
80     );
81   },
83   uninit() {
84     Services.obs.removeObserver(this, "captive-portal-login");
85     Services.obs.removeObserver(this, "captive-portal-login-abort");
86     Services.obs.removeObserver(this, "captive-portal-login-success");
88     this._cancelDelayedCaptivePortal();
89   },
91   delayedStartup() {
92     if (this._delayedRecheckPending) {
93       delete this._delayedRecheckPending;
94       this._cps.recheckCaptivePortal();
95     }
96   },
98   observe(aSubject, aTopic) {
99     switch (aTopic) {
100       case "captive-portal-login":
101         this._captivePortalDetected();
102         break;
103       case "captive-portal-login-abort":
104         this._captivePortalGone(false);
105         break;
106       case "captive-portal-login-success":
107         this._captivePortalGone(true);
108         break;
109       case "delayed-captive-portal-handled":
110         this._cancelDelayedCaptivePortal();
111         break;
112     }
113   },
115   onLocationChange(browser) {
116     if (!this._previousCaptivePortalTab) {
117       return;
118     }
120     let tab = this._previousCaptivePortalTab.get();
121     if (!tab || !tab.linkedBrowser) {
122       return;
123     }
125     if (browser != tab.linkedBrowser) {
126       return;
127     }
129     // There is a race between the release of captive portal i.e.
130     // the time when success/abort events are fired and the time when
131     // the captive portal tab redirects to the canonicalURL. We check for
132     // both conditions to be true and also check that we haven't already removed
133     // the captive portal tab in the success/abort event handlers before we remove
134     // it in the callback below. A tick is added to avoid removing the tab before
135     // onLocationChange handlers across browser code are executed.
136     Services.tm.dispatchToMainThread(() => {
137       if (!this._previousCaptivePortalTab) {
138         return;
139       }
141       tab = this._previousCaptivePortalTab.get();
142       let canonicalURI = Services.io.newURI(this.canonicalURL);
143       if (
144         tab &&
145         (tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI) ||
146           tab.linkedBrowser.currentURI.host == "support.mozilla.org") &&
147         (this._cps.state == this._cps.UNLOCKED_PORTAL ||
148           this._cps.state == this._cps.UNKNOWN)
149       ) {
150         gBrowser.removeTab(tab);
151       }
152     });
153   },
155   _captivePortalDetected() {
156     if (this._delayedCaptivePortalDetectedInProgress) {
157       return;
158     }
160     // Add an explicit permission for the last detected URI such that https-only / https-first do not
161     // attempt to upgrade the URI to https when following the "open network login page" button.
162     // We set explicit permissions for regular and private browsing windows to keep permissions
163     // separate.
164     let canonicalURI = Services.io.newURI(this.canonicalURL);
165     let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
166     let principal = Services.scriptSecurityManager.createContentPrincipal(
167       canonicalURI,
168       {
169         userContextId: gBrowser.contentPrincipal.userContextId,
170         privateBrowsingId: isPrivate ? 1 : 0,
171       }
172     );
173     Services.perms.addFromPrincipal(
174       principal,
175       "https-only-load-insecure",
176       Ci.nsIPermissionManager.ALLOW_ACTION,
177       Ci.nsIPermissionManager.EXPIRE_SESSION
178     );
179     let win = BrowserWindowTracker.getTopWindow();
180     // Used by tests: ignore the main test window in order to enable testing of
181     // the case where we have no open windows.
182     if (win.document.documentElement.getAttribute("ignorecaptiveportal")) {
183       win = null;
184     }
186     // If no browser window has focus, open and show the tab when we regain focus.
187     // This is so that if a different application was focused, when the user
188     // (re-)focuses a browser window, we open the tab immediately in that window
189     // so they can log in before continuing to browse.
190     if (win != Services.focus.activeWindow) {
191       this._delayedCaptivePortalDetectedInProgress = true;
192       window.addEventListener("activate", this, { once: true });
193       Services.obs.addObserver(this, "delayed-captive-portal-handled");
194     }
196     this._showNotification();
197   },
199   /**
200    * Called after we regain focus if we detect a portal while a browser window
201    * doesn't have focus. Triggers a portal recheck to reaffirm state, and adds
202    * the tab if needed after a short delay to allow the recheck to complete.
203    */
204   _delayedCaptivePortalDetected() {
205     if (!this._delayedCaptivePortalDetectedInProgress) {
206       return;
207     }
209     // Used by tests: ignore the main test window in order to enable testing of
210     // the case where we have no open windows.
211     if (window.document.documentElement.getAttribute("ignorecaptiveportal")) {
212       return;
213     }
215     Services.obs.notifyObservers(null, "delayed-captive-portal-handled");
217     // Trigger a portal recheck. The user may have logged into the portal via
218     // another client, or changed networks.
219     this._cps.recheckCaptivePortal();
220     this._waitingForRecheck = true;
221     let requestTime = Date.now();
223     let observer = () => {
224       let time = Date.now() - requestTime;
225       Services.obs.removeObserver(observer, "captive-portal-check-complete");
226       this._waitingForRecheck = false;
227       if (this._cps.state != this._cps.LOCKED_PORTAL) {
228         // We're free of the portal!
229         return;
230       }
232       if (time <= this.PORTAL_RECHECK_DELAY_MS) {
233         // The amount of time elapsed since we requested a recheck (i.e. since
234         // the browser window was focused) was small enough that we can add and
235         // focus a tab with the login page with no noticeable delay.
236         this.ensureCaptivePortalTab();
237       }
238     };
239     Services.obs.addObserver(observer, "captive-portal-check-complete");
240   },
242   _captivePortalGone(aSuccess) {
243     this._cancelDelayedCaptivePortal();
244     this._removeNotification();
246     let durationInSeconds = Math.round(
247       (Date.now() - this._bannerDisplayTime) / 1000
248     );
250     Services.telemetry.keyedScalarAdd(
251       "networking.captive_portal_banner_display_time",
252       aSuccess ? "success" : "abort",
253       durationInSeconds
254     );
256     if (!this._captivePortalTab) {
257       return;
258     }
260     let tab = this._captivePortalTab.get();
261     let canonicalURI = Services.io.newURI(this.canonicalURL);
262     if (
263       tab &&
264       tab.linkedBrowser &&
265       (tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI) ||
266         tab.linkedBrowser.currentURI.host == "support.mozilla.org")
267     ) {
268       this._previousCaptivePortalTab = null;
269       gBrowser.removeTab(tab);
270     }
271     this._captivePortalTab = null;
272   },
274   _cancelDelayedCaptivePortal() {
275     if (this._delayedCaptivePortalDetectedInProgress) {
276       this._delayedCaptivePortalDetectedInProgress = false;
277       Services.obs.removeObserver(this, "delayed-captive-portal-handled");
278       window.removeEventListener("activate", this);
279     }
280   },
282   async handleEvent(aEvent) {
283     switch (aEvent.type) {
284       case "activate":
285         this._delayedCaptivePortalDetected();
286         break;
287       case "TabSelect":
288         if (this._notificationPromise) {
289           await this._notificationPromise;
290         }
291         if (!this._captivePortalTab || !this._captivePortalNotification) {
292           break;
293         }
295         let tab = this._captivePortalTab.get();
296         let n = this._captivePortalNotification;
297         if (!tab || !n) {
298           break;
299         }
301         let doc = tab.ownerDocument;
302         let button = n.buttonContainer.querySelector(
303           "button.notification-button"
304         );
305         if (doc.defaultView.gBrowser.selectedTab == tab) {
306           button.style.visibility = "hidden";
307         } else {
308           button.style.visibility = "visible";
309         }
310         break;
311     }
312   },
314   _showNotification() {
315     if (this._captivePortalNotification) {
316       return;
317     }
319     Services.telemetry.scalarAdd(
320       "networking.captive_portal_banner_displayed",
321       1
322     );
323     this._bannerDisplayTime = Date.now();
325     let buttons = [
326       {
327         label: this._browserBundle.GetStringFromName(
328           "captivePortal.showLoginPage2"
329         ),
330         callback: () => {
331           this.ensureCaptivePortalTab();
333           // Returning true prevents the notification from closing.
334           return true;
335         },
336       },
337     ];
339     let message = this._browserBundle.GetStringFromName(
340       "captivePortal.infoMessage3"
341     );
343     let closeHandler = aEventName => {
344       if (aEventName == "dismissed") {
345         let durationInSeconds = Math.round(
346           (Date.now() - this._bannerDisplayTime) / 1000
347         );
349         Services.telemetry.keyedScalarAdd(
350           "networking.captive_portal_banner_display_time",
351           "dismiss",
352           durationInSeconds
353         );
354       }
356       if (aEventName != "removed") {
357         return;
358       }
359       gBrowser.tabContainer.removeEventListener("TabSelect", this);
360     };
362     this._notificationPromise = gNotificationBox.appendNotification(
363       this.PORTAL_NOTIFICATION_VALUE,
364       {
365         label: message,
366         priority: gNotificationBox.PRIORITY_INFO_MEDIUM,
367         eventCallback: closeHandler,
368       },
369       buttons
370     );
372     gBrowser.tabContainer.addEventListener("TabSelect", this);
373   },
375   _removeNotification() {
376     let n = this._captivePortalNotification;
377     if (!n || !n.parentNode) {
378       return;
379     }
380     n.close();
381   },
383   ensureCaptivePortalTab() {
384     let tab;
385     if (this._captivePortalTab) {
386       tab = this._captivePortalTab.get();
387     }
389     // If the tab is gone or going, we need to open a new one.
390     if (!tab || tab.closing || !tab.parentNode) {
391       tab = gBrowser.addWebTab(this.canonicalURL, {
392         ownerTab: gBrowser.selectedTab,
393         triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
394           {
395             userContextId: gBrowser.contentPrincipal.userContextId,
396           }
397         ),
398         disableTRR: true,
399       });
400       this._captivePortalTab = Cu.getWeakReference(tab);
401       this._previousCaptivePortalTab = Cu.getWeakReference(tab);
402     }
404     gBrowser.selectedTab = tab;
405   },