browser-captivePortal.js (13032B)
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/. */ 4 5 var CaptivePortalWatcher = { 6 // This is the value used to identify the captive portal notification. 7 PORTAL_NOTIFICATION_VALUE: "captive-portal-detected", 8 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, 12 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, 20 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, 24 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, 28 29 // Stores the time at which the banner was displayed 30 _bannerDisplayTime: Date.now(), 31 32 get _captivePortalNotification() { 33 return gNotificationBox.getNotificationWithValue( 34 this.PORTAL_NOTIFICATION_VALUE 35 ); 36 }, 37 38 get canonicalURL() { 39 return Services.prefs.getCharPref("captivedetect.canonicalURL"); 40 }, 41 42 get _browserBundle() { 43 delete this._browserBundle; 44 return (this._browserBundle = Services.strings.createBundle( 45 "chrome://browser/locale/browser.properties" 46 )); 47 }, 48 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"); 53 54 this._cps = Cc["@mozilla.org/network/captive-portal-service;1"].getService( 55 Ci.nsICaptivePortalService 56 ); 57 58 if (this._cps.state == this._cps.LOCKED_PORTAL) { 59 // A captive portal has already been detected. 60 this._captivePortalDetected(); 61 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 } 71 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 }, 82 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"); 87 88 this._cancelDelayedCaptivePortal(); 89 }, 90 91 delayedStartup() { 92 if (this._delayedRecheckPending) { 93 delete this._delayedRecheckPending; 94 this._cps.recheckCaptivePortal(); 95 } 96 }, 97 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 }, 114 115 onLocationChange(browser) { 116 if (!this._previousCaptivePortalTab) { 117 return; 118 } 119 120 let tab = this._previousCaptivePortalTab.get(); 121 if (!tab || !tab.linkedBrowser) { 122 return; 123 } 124 125 if (browser != tab.linkedBrowser) { 126 return; 127 } 128 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 } 140 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 }, 154 155 _captivePortalDetected() { 156 if (this._delayedCaptivePortalDetectedInProgress) { 157 return; 158 } 159 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 } 185 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 } 195 196 this._showNotification(); 197 }, 198 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 } 208 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 } 214 215 Services.obs.notifyObservers(null, "delayed-captive-portal-handled"); 216 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(); 222 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 } 231 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 }, 241 242 _captivePortalGone(aSuccess) { 243 this._cancelDelayedCaptivePortal(); 244 this._removeNotification(); 245 246 let durationInSeconds = Math.round( 247 (Date.now() - this._bannerDisplayTime) / 1000 248 ); 249 250 if (aSuccess) { 251 Glean.networking.captivePortalBannerDisplayTime.success.add( 252 durationInSeconds 253 ); 254 } else { 255 Glean.networking.captivePortalBannerDisplayTime.abort.add( 256 durationInSeconds 257 ); 258 } 259 260 if (!this._captivePortalTab) { 261 return; 262 } 263 264 let tab = this._captivePortalTab.get(); 265 let canonicalURI = Services.io.newURI(this.canonicalURL); 266 if ( 267 tab && 268 tab.linkedBrowser && 269 (tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI) || 270 tab.linkedBrowser.currentURI.host == "support.mozilla.org") 271 ) { 272 this._previousCaptivePortalTab = null; 273 gBrowser.removeTab(tab); 274 } 275 this._captivePortalTab = null; 276 }, 277 278 _cancelDelayedCaptivePortal() { 279 if (this._delayedCaptivePortalDetectedInProgress) { 280 this._delayedCaptivePortalDetectedInProgress = false; 281 Services.obs.removeObserver(this, "delayed-captive-portal-handled"); 282 window.removeEventListener("activate", this); 283 } 284 }, 285 286 async handleEvent(aEvent) { 287 switch (aEvent.type) { 288 case "activate": 289 this._delayedCaptivePortalDetected(); 290 break; 291 case "TabSelect": { 292 if (this._notificationPromise) { 293 await this._notificationPromise; 294 } 295 if (!this._captivePortalTab || !this._captivePortalNotification) { 296 break; 297 } 298 299 let tab = this._captivePortalTab.get(); 300 let n = this._captivePortalNotification; 301 if (!tab || !n) { 302 break; 303 } 304 305 let doc = tab.ownerDocument; 306 let button = n.buttonContainer.querySelector( 307 "button.notification-button" 308 ); 309 if (doc.defaultView.gBrowser.selectedTab == tab) { 310 button.style.visibility = "hidden"; 311 } else { 312 button.style.visibility = "visible"; 313 } 314 break; 315 } 316 } 317 }, 318 319 _showNotification() { 320 if (this._captivePortalNotification) { 321 return; 322 } 323 324 Glean.networking.captivePortalBannerDisplayed.add(1); 325 this._bannerDisplayTime = Date.now(); 326 327 let buttons = [ 328 { 329 label: this._browserBundle.GetStringFromName( 330 "captivePortal.showLoginPage2" 331 ), 332 callback: () => { 333 this.ensureCaptivePortalTab(); 334 335 // Returning true prevents the notification from closing. 336 return true; 337 }, 338 }, 339 ]; 340 341 let message = this._browserBundle.GetStringFromName( 342 "captivePortal.infoMessage3" 343 ); 344 345 let closeHandler = aEventName => { 346 if (aEventName == "dismissed") { 347 let durationInSeconds = Math.round( 348 (Date.now() - this._bannerDisplayTime) / 1000 349 ); 350 351 Glean.networking.captivePortalBannerDisplayTime.dismiss.add( 352 durationInSeconds 353 ); 354 } 355 356 if (aEventName != "removed") { 357 return; 358 } 359 gBrowser.tabContainer.removeEventListener("TabSelect", this); 360 }; 361 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 ); 371 372 gBrowser.tabContainer.addEventListener("TabSelect", this); 373 }, 374 375 _removeNotification() { 376 let n = this._captivePortalNotification; 377 if (!n || !n.parentNode) { 378 return; 379 } 380 n.close(); 381 }, 382 383 ensureCaptivePortalTab() { 384 let tab; 385 if (this._captivePortalTab) { 386 tab = this._captivePortalTab.get(); 387 } 388 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 isCaptivePortalTab: true, 399 }); 400 this._captivePortalTab = Cu.getWeakReference(tab); 401 this._previousCaptivePortalTab = Cu.getWeakReference(tab); 402 } 403 404 gBrowser.selectedTab = tab; 405 }, 406 };