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,
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.
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
39 return Services.prefs.getCharPref("captivedetect.canonicalURL");
42 get _browserBundle() {
43 delete this._browserBundle;
44 return (this._browserBundle = Services.strings.createBundle(
45 "chrome://browser/locale/browser.properties"
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
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();
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;
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(
77 "PORTAL_RECHECK_DELAY_MS",
78 "captivedetect.portalRecheckDelayMS",
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();
92 if (this._delayedRecheckPending) {
93 delete this._delayedRecheckPending;
94 this._cps.recheckCaptivePortal();
98 observe(aSubject, aTopic) {
100 case "captive-portal-login":
101 this._captivePortalDetected();
103 case "captive-portal-login-abort":
104 this._captivePortalGone(false);
106 case "captive-portal-login-success":
107 this._captivePortalGone(true);
109 case "delayed-captive-portal-handled":
110 this._cancelDelayedCaptivePortal();
115 onLocationChange(browser) {
116 if (!this._previousCaptivePortalTab) {
120 let tab = this._previousCaptivePortalTab.get();
121 if (!tab || !tab.linkedBrowser) {
125 if (browser != tab.linkedBrowser) {
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) {
141 tab = this._previousCaptivePortalTab.get();
142 let canonicalURI = Services.io.newURI(this.canonicalURL);
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)
150 gBrowser.removeTab(tab);
155 _captivePortalDetected() {
156 if (this._delayedCaptivePortalDetectedInProgress) {
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
164 let canonicalURI = Services.io.newURI(this.canonicalURL);
165 let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
166 let principal = Services.scriptSecurityManager.createContentPrincipal(
169 userContextId: gBrowser.contentPrincipal.userContextId,
170 privateBrowsingId: isPrivate ? 1 : 0,
173 Services.perms.addFromPrincipal(
175 "https-only-load-insecure",
176 Ci.nsIPermissionManager.ALLOW_ACTION,
177 Ci.nsIPermissionManager.EXPIRE_SESSION
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")) {
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");
196 this._showNotification();
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.
204 _delayedCaptivePortalDetected() {
205 if (!this._delayedCaptivePortalDetectedInProgress) {
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")) {
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!
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();
239 Services.obs.addObserver(observer, "captive-portal-check-complete");
242 _captivePortalGone(aSuccess) {
243 this._cancelDelayedCaptivePortal();
244 this._removeNotification();
246 let durationInSeconds = Math.round(
247 (Date.now() - this._bannerDisplayTime) / 1000
250 Services.telemetry.keyedScalarAdd(
251 "networking.captive_portal_banner_display_time",
252 aSuccess ? "success" : "abort",
256 if (!this._captivePortalTab) {
260 let tab = this._captivePortalTab.get();
261 let canonicalURI = Services.io.newURI(this.canonicalURL);
265 (tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI) ||
266 tab.linkedBrowser.currentURI.host == "support.mozilla.org")
268 this._previousCaptivePortalTab = null;
269 gBrowser.removeTab(tab);
271 this._captivePortalTab = null;
274 _cancelDelayedCaptivePortal() {
275 if (this._delayedCaptivePortalDetectedInProgress) {
276 this._delayedCaptivePortalDetectedInProgress = false;
277 Services.obs.removeObserver(this, "delayed-captive-portal-handled");
278 window.removeEventListener("activate", this);
282 async handleEvent(aEvent) {
283 switch (aEvent.type) {
285 this._delayedCaptivePortalDetected();
288 if (this._notificationPromise) {
289 await this._notificationPromise;
291 if (!this._captivePortalTab || !this._captivePortalNotification) {
295 let tab = this._captivePortalTab.get();
296 let n = this._captivePortalNotification;
301 let doc = tab.ownerDocument;
302 let button = n.buttonContainer.querySelector(
303 "button.notification-button"
305 if (doc.defaultView.gBrowser.selectedTab == tab) {
306 button.style.visibility = "hidden";
308 button.style.visibility = "visible";
314 _showNotification() {
315 if (this._captivePortalNotification) {
319 Services.telemetry.scalarAdd(
320 "networking.captive_portal_banner_displayed",
323 this._bannerDisplayTime = Date.now();
327 label: this._browserBundle.GetStringFromName(
328 "captivePortal.showLoginPage2"
331 this.ensureCaptivePortalTab();
333 // Returning true prevents the notification from closing.
339 let message = this._browserBundle.GetStringFromName(
340 "captivePortal.infoMessage3"
343 let closeHandler = aEventName => {
344 if (aEventName == "dismissed") {
345 let durationInSeconds = Math.round(
346 (Date.now() - this._bannerDisplayTime) / 1000
349 Services.telemetry.keyedScalarAdd(
350 "networking.captive_portal_banner_display_time",
356 if (aEventName != "removed") {
359 gBrowser.tabContainer.removeEventListener("TabSelect", this);
362 this._notificationPromise = gNotificationBox.appendNotification(
363 this.PORTAL_NOTIFICATION_VALUE,
366 priority: gNotificationBox.PRIORITY_INFO_MEDIUM,
367 eventCallback: closeHandler,
372 gBrowser.tabContainer.addEventListener("TabSelect", this);
375 _removeNotification() {
376 let n = this._captivePortalNotification;
377 if (!n || !n.parentNode) {
383 ensureCaptivePortalTab() {
385 if (this._captivePortalTab) {
386 tab = this._captivePortalTab.get();
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(
395 userContextId: gBrowser.contentPrincipal.userContextId,
400 this._captivePortalTab = Cu.getWeakReference(tab);
401 this._previousCaptivePortalTab = Cu.getWeakReference(tab);
404 gBrowser.selectedTab = tab;