browser-siteIdentity.js (48367B)
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 ChromeUtils.defineESModuleGetters(this, { 6 ExtensionUtils: "resource://gre/modules/ExtensionUtils.sys.mjs", 7 QWACs: "resource://gre/modules/psm/QWACs.sys.mjs", 8 }); 9 10 /** 11 * Utility object to handle manipulations of the identity indicators in the UI 12 */ 13 var gIdentityHandler = { 14 /** 15 * nsIURI for which the identity UI is displayed. This has been already 16 * processed by createExposableURI. 17 */ 18 _uri: null, 19 20 /** 21 * We only know the connection type if this._uri has a defined "host" part. 22 * 23 * These URIs, like "about:", "file:" and "data:" URIs, will usually be treated as a 24 * an unknown connection. 25 */ 26 _uriHasHost: false, 27 28 /** 29 * If this tab belongs to a WebExtension, contains its WebExtensionPolicy. 30 */ 31 _pageExtensionPolicy: null, 32 33 /** 34 * Whether this._uri refers to an internally implemented browser page. 35 * 36 * Note that this is set for some "about:" pages, but general "chrome:" URIs 37 * are not included in this category by default. 38 */ 39 _isSecureInternalUI: false, 40 41 /** 42 * Whether the content window is considered a "secure context". This 43 * includes "potentially trustworthy" origins such as file:// URLs or localhost. 44 * https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy 45 */ 46 _isSecureContext: false, 47 48 /** 49 * nsITransportSecurityInfo metadata provided by gBrowser.securityUI the last 50 * time the identity UI was updated, or null if the connection is not secure. 51 */ 52 _secInfo: null, 53 54 /** 55 * If the document is using a QWAC, this may eventually be an nsIX509Cert 56 * corresponding to it. 57 */ 58 _qwac: null, 59 60 /** 61 * Promise that will resolve when determining if the document is using a QWAC 62 * has resolved. 63 */ 64 _qwacStatusPromise: null, 65 66 /** 67 * Bitmask provided by nsIWebProgressListener.onSecurityChange. 68 */ 69 _state: 0, 70 71 /** 72 * Whether the established HTTPS connection is considered "broken". 73 * This could have several reasons, such as mixed content or weak 74 * cryptography. If this is true, _isSecureConnection is false. 75 */ 76 get _isBrokenConnection() { 77 return this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN; 78 }, 79 80 /** 81 * Whether the connection to the current site was done via secure 82 * transport. Note that this attribute is not true in all cases that 83 * the site was accessed via HTTPS, i.e. _isSecureConnection will 84 * be false when _isBrokenConnection is true, even though the page 85 * was loaded over HTTPS. 86 */ 87 get _isSecureConnection() { 88 // If a <browser> is included within a chrome document, then this._state 89 // will refer to the security state for the <browser> and not the top level 90 // document. In this case, don't upgrade the security state in the UI 91 // with the secure state of the embedded <browser>. 92 return ( 93 !this._isURILoadedFromFile && 94 this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE 95 ); 96 }, 97 98 get _isEV() { 99 // If a <browser> is included within a chrome document, then this._state 100 // will refer to the security state for the <browser> and not the top level 101 // document. In this case, don't upgrade the security state in the UI 102 // with the EV state of the embedded <browser>. 103 return ( 104 !this._isURILoadedFromFile && 105 this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL 106 ); 107 }, 108 109 get _isAssociatedIdentity() { 110 return this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_ASSOCIATED; 111 }, 112 113 get _isMixedActiveContentLoaded() { 114 return ( 115 this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT 116 ); 117 }, 118 119 get _isMixedActiveContentBlocked() { 120 return ( 121 this._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT 122 ); 123 }, 124 125 get _isMixedPassiveContentLoaded() { 126 return ( 127 this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT 128 ); 129 }, 130 131 get _isContentHttpsOnlyModeUpgraded() { 132 return ( 133 this._state & Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADED 134 ); 135 }, 136 137 get _isContentHttpsOnlyModeUpgradeFailed() { 138 return ( 139 this._state & 140 Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED 141 ); 142 }, 143 144 get _isContentHttpsFirstModeUpgraded() { 145 return ( 146 this._state & 147 Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADED_FIRST 148 ); 149 }, 150 151 get _isCertUserOverridden() { 152 return this._state & Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN; 153 }, 154 155 get _isCertErrorPage() { 156 let { documentURI } = gBrowser.selectedBrowser; 157 if (documentURI?.scheme != "about") { 158 return false; 159 } 160 161 return ( 162 documentURI.filePath == "certerror" || 163 (documentURI.filePath == "neterror" && 164 new URLSearchParams(documentURI.query).get("e") == "nssFailure2") 165 ); 166 }, 167 168 get _isSecurelyConnectedAboutNetErrorPage() { 169 let { documentURI } = gBrowser.selectedBrowser; 170 if (documentURI?.scheme != "about" || documentURI.filePath != "neterror") { 171 return false; 172 } 173 174 let error = new URLSearchParams(documentURI.query).get("e"); 175 176 // Bug 1944993 - A list of neterrors without connection issues 177 return error === "httpErrorPage" || error === "serverError"; 178 }, 179 180 get _uriIsOnionHost() { 181 return this._uriHasHost 182 ? this._uri.host.toLowerCase().endsWith(".onion") 183 : false; 184 }, 185 186 get _isAboutNetErrorPage() { 187 let { documentURI } = gBrowser.selectedBrowser; 188 return documentURI?.scheme == "about" && documentURI.filePath == "neterror"; 189 }, 190 191 get _isAboutHttpsOnlyErrorPage() { 192 let { documentURI } = gBrowser.selectedBrowser; 193 return ( 194 documentURI?.scheme == "about" && documentURI.filePath == "httpsonlyerror" 195 ); 196 }, 197 198 get _isPotentiallyTrustworthy() { 199 return ( 200 !this._isBrokenConnection && 201 (this._isSecureContext || 202 gBrowser.selectedBrowser.documentURI?.scheme == "chrome") 203 ); 204 }, 205 206 get _isAboutBlockedPage() { 207 let { documentURI } = gBrowser.selectedBrowser; 208 return documentURI?.scheme == "about" && documentURI.filePath == "blocked"; 209 }, 210 211 _popupInitialized: false, 212 _initializePopup() { 213 if (!this._popupInitialized) { 214 let wrapper = document.getElementById("template-identity-popup"); 215 wrapper.replaceWith(wrapper.content); 216 this._popupInitialized = true; 217 this._initializePopupListeners(); 218 } 219 }, 220 221 _initializePopupListeners() { 222 let popup = this._identityPopup; 223 popup.addEventListener("popupshown", event => { 224 this.onPopupShown(event); 225 }); 226 popup.addEventListener("popuphidden", event => { 227 this.onPopupHidden(event); 228 }); 229 230 const COMMANDS = { 231 "identity-popup-security-button": () => { 232 this.showSecuritySubView(); 233 }, 234 "identity-popup-security-httpsonlymode-menulist": () => { 235 this.changeHttpsOnlyPermission(); 236 }, 237 "identity-popup-clear-sitedata-button": event => { 238 this.clearSiteData(event); 239 }, 240 "identity-popup-remove-cert-exception": () => { 241 this.removeCertException(); 242 }, 243 "identity-popup-more-info": event => { 244 this.handleMoreInfoClick(event); 245 }, 246 }; 247 248 for (let [id, handler] of Object.entries(COMMANDS)) { 249 document.getElementById(id).addEventListener("command", handler); 250 } 251 }, 252 253 hidePopup() { 254 if (this._popupInitialized) { 255 PanelMultiView.hidePopup(this._identityPopup); 256 } 257 }, 258 259 // smart getters 260 get _identityPopup() { 261 if (!this._popupInitialized) { 262 return null; 263 } 264 delete this._identityPopup; 265 return (this._identityPopup = document.getElementById("identity-popup")); 266 }, 267 get _identityBox() { 268 delete this._identityBox; 269 return (this._identityBox = document.getElementById("identity-box")); 270 }, 271 get _identityIconBox() { 272 delete this._identityIconBox; 273 return (this._identityIconBox = 274 document.getElementById("identity-icon-box")); 275 }, 276 get _identityPopupMultiView() { 277 delete this._identityPopupMultiView; 278 return (this._identityPopupMultiView = document.getElementById( 279 "identity-popup-multiView" 280 )); 281 }, 282 get _identityPopupMainView() { 283 delete this._identityPopupMainView; 284 return (this._identityPopupMainView = document.getElementById( 285 "identity-popup-mainView" 286 )); 287 }, 288 get _identityPopupMainViewHeaderLabel() { 289 delete this._identityPopupMainViewHeaderLabel; 290 return (this._identityPopupMainViewHeaderLabel = document.getElementById( 291 "identity-popup-mainView-panel-header-span" 292 )); 293 }, 294 get _identityPopupSecurityView() { 295 delete this._identityPopupSecurityView; 296 return (this._identityPopupSecurityView = document.getElementById( 297 "identity-popup-securityView" 298 )); 299 }, 300 get _identityPopupHttpsOnlyMode() { 301 delete this._identityPopupHttpsOnlyMode; 302 return (this._identityPopupHttpsOnlyMode = document.getElementById( 303 "identity-popup-security-httpsonlymode" 304 )); 305 }, 306 get _identityPopupHttpsOnlyModeMenuList() { 307 delete this._identityPopupHttpsOnlyModeMenuList; 308 return (this._identityPopupHttpsOnlyModeMenuList = document.getElementById( 309 "identity-popup-security-httpsonlymode-menulist" 310 )); 311 }, 312 get _identityPopupHttpsOnlyModeMenuListOffItem() { 313 delete this._identityPopupHttpsOnlyModeMenuListOffItem; 314 return (this._identityPopupHttpsOnlyModeMenuListOffItem = 315 document.getElementById("identity-popup-security-menulist-off-item")); 316 }, 317 get _identityPopupSecurityEVContentOwner() { 318 delete this._identityPopupSecurityEVContentOwner; 319 return (this._identityPopupSecurityEVContentOwner = document.getElementById( 320 "identity-popup-security-ev-content-owner" 321 )); 322 }, 323 get _identityPopupContentOwner() { 324 delete this._identityPopupContentOwner; 325 return (this._identityPopupContentOwner = document.getElementById( 326 "identity-popup-content-owner" 327 )); 328 }, 329 get _identityPopupContentSupp() { 330 delete this._identityPopupContentSupp; 331 return (this._identityPopupContentSupp = document.getElementById( 332 "identity-popup-content-supplemental" 333 )); 334 }, 335 get _identityPopupContentVerif() { 336 delete this._identityPopupContentVerif; 337 return (this._identityPopupContentVerif = document.getElementById( 338 "identity-popup-content-verifier" 339 )); 340 }, 341 get _identityPopupCustomRootLearnMore() { 342 delete this._identityPopupCustomRootLearnMore; 343 return (this._identityPopupCustomRootLearnMore = document.getElementById( 344 "identity-popup-custom-root-learn-more" 345 )); 346 }, 347 get _identityPopupMixedContentLearnMore() { 348 delete this._identityPopupMixedContentLearnMore; 349 return (this._identityPopupMixedContentLearnMore = [ 350 ...document.querySelectorAll(".identity-popup-mcb-learn-more"), 351 ]); 352 }, 353 354 get _identityIconLabel() { 355 delete this._identityIconLabel; 356 return (this._identityIconLabel = document.getElementById( 357 "identity-icon-label" 358 )); 359 }, 360 get _overrideService() { 361 delete this._overrideService; 362 return (this._overrideService = Cc[ 363 "@mozilla.org/security/certoverride;1" 364 ].getService(Ci.nsICertOverrideService)); 365 }, 366 get _identityIcon() { 367 delete this._identityIcon; 368 return (this._identityIcon = document.getElementById("identity-icon")); 369 }, 370 get _clearSiteDataFooter() { 371 delete this._clearSiteDataFooter; 372 return (this._clearSiteDataFooter = document.getElementById( 373 "identity-popup-clear-sitedata-footer" 374 )); 375 }, 376 get _insecureConnectionTextEnabled() { 377 delete this._insecureConnectionTextEnabled; 378 XPCOMUtils.defineLazyPreferenceGetter( 379 this, 380 "_insecureConnectionTextEnabled", 381 "security.insecure_connection_text.enabled" 382 ); 383 return this._insecureConnectionTextEnabled; 384 }, 385 get _insecureConnectionTextPBModeEnabled() { 386 delete this._insecureConnectionTextPBModeEnabled; 387 XPCOMUtils.defineLazyPreferenceGetter( 388 this, 389 "_insecureConnectionTextPBModeEnabled", 390 "security.insecure_connection_text.pbmode.enabled" 391 ); 392 return this._insecureConnectionTextPBModeEnabled; 393 }, 394 get _httpsOnlyModeEnabled() { 395 delete this._httpsOnlyModeEnabled; 396 XPCOMUtils.defineLazyPreferenceGetter( 397 this, 398 "_httpsOnlyModeEnabled", 399 "dom.security.https_only_mode" 400 ); 401 return this._httpsOnlyModeEnabled; 402 }, 403 get _httpsOnlyModeEnabledPBM() { 404 delete this._httpsOnlyModeEnabledPBM; 405 XPCOMUtils.defineLazyPreferenceGetter( 406 this, 407 "_httpsOnlyModeEnabledPBM", 408 "dom.security.https_only_mode_pbm" 409 ); 410 return this._httpsOnlyModeEnabledPBM; 411 }, 412 get _httpsFirstModeEnabled() { 413 delete this._httpsFirstModeEnabled; 414 XPCOMUtils.defineLazyPreferenceGetter( 415 this, 416 "_httpsFirstModeEnabled", 417 "dom.security.https_first" 418 ); 419 return this._httpsFirstModeEnabled; 420 }, 421 get _httpsFirstModeEnabledPBM() { 422 delete this._httpsFirstModeEnabledPBM; 423 XPCOMUtils.defineLazyPreferenceGetter( 424 this, 425 "_httpsFirstModeEnabledPBM", 426 "dom.security.https_first_pbm" 427 ); 428 return this._httpsFirstModeEnabledPBM; 429 }, 430 get _schemelessHttpsFirstModeEnabled() { 431 delete this._schemelessHttpsFirstModeEnabled; 432 XPCOMUtils.defineLazyPreferenceGetter( 433 this, 434 "_schemelessHttpsFirstModeEnabled", 435 "dom.security.https_first_schemeless" 436 ); 437 return this._schemelessHttpsFirstModeEnabled; 438 }, 439 440 _isHttpsOnlyModeActive(isWindowPrivate) { 441 return ( 442 this._httpsOnlyModeEnabled || 443 (isWindowPrivate && this._httpsOnlyModeEnabledPBM) 444 ); 445 }, 446 _isHttpsFirstModeActive(isWindowPrivate) { 447 return ( 448 !this._isHttpsOnlyModeActive(isWindowPrivate) && 449 (this._httpsFirstModeEnabled || 450 (isWindowPrivate && this._httpsFirstModeEnabledPBM)) 451 ); 452 }, 453 _isSchemelessHttpsFirstModeActive(isWindowPrivate) { 454 return ( 455 !this._isHttpsOnlyModeActive(isWindowPrivate) && 456 !this._isHttpsFirstModeActive(isWindowPrivate) && 457 this._schemelessHttpsFirstModeEnabled 458 ); 459 }, 460 461 /** 462 * Handles clicks on the "Clear Cookies and Site Data" button. 463 */ 464 async clearSiteData(event) { 465 if (!this._uriHasHost) { 466 return; 467 } 468 469 // Hide the popup before showing the removal prompt, to 470 // avoid a pretty ugly transition. Also hide it even 471 // if the update resulted in no site data, to keep the 472 // illusion that clicking the button had an effect. 473 let hidden = new Promise(c => { 474 this._identityPopup.addEventListener("popuphidden", c, { once: true }); 475 }); 476 PanelMultiView.hidePopup(this._identityPopup); 477 await hidden; 478 479 let baseDomain = SiteDataManager.getBaseDomainFromHost(this._uri.host); 480 if (SiteDataManager.promptSiteDataRemoval(window, [baseDomain])) { 481 SiteDataManager.remove(baseDomain); 482 } 483 484 event.stopPropagation(); 485 }, 486 487 /** 488 * Handler for mouseclicks on the "More Information" button in the 489 * "identity-popup" panel. 490 */ 491 handleMoreInfoClick(event) { 492 displaySecurityInfo(); 493 event.stopPropagation(); 494 PanelMultiView.hidePopup(this._identityPopup); 495 }, 496 497 showSecuritySubView() { 498 this._identityPopupMultiView.showSubView( 499 "identity-popup-securityView", 500 document.getElementById("identity-popup-security-button") 501 ); 502 503 // Elements of hidden views have -moz-user-focus:ignore but setting that 504 // per CSS selector doesn't blur a focused element in those hidden views. 505 Services.focus.clearFocus(window); 506 }, 507 508 removeCertException() { 509 if (!this._uriHasHost) { 510 console.error( 511 "Trying to revoke a cert exception on a URI without a host?" 512 ); 513 return; 514 } 515 let host = this._uri.host; 516 let port = this._uri.port > 0 ? this._uri.port : 443; 517 this._overrideService.clearValidityOverride( 518 host, 519 port, 520 gBrowser.contentPrincipal.originAttributes 521 ); 522 BrowserCommands.reloadSkipCache(); 523 if (this._popupInitialized) { 524 PanelMultiView.hidePopup(this._identityPopup); 525 } 526 }, 527 528 /** 529 * Gets the current HTTPS-Only mode permission for the current page. 530 * Values are the same as in #identity-popup-security-httpsonlymode-menulist, 531 * -1 indicates a incompatible scheme on the current URI. 532 */ 533 _getHttpsOnlyPermission() { 534 let uri = gBrowser.currentURI; 535 if (uri instanceof Ci.nsINestedURI) { 536 uri = uri.QueryInterface(Ci.nsINestedURI).innermostURI; 537 } 538 if (!uri.schemeIs("http") && !uri.schemeIs("https")) { 539 return -1; 540 } 541 uri = uri.mutate().setScheme("http").finalize(); 542 const principal = Services.scriptSecurityManager.createContentPrincipal( 543 uri, 544 gBrowser.contentPrincipal.originAttributes 545 ); 546 const { state } = SitePermissions.getForPrincipal( 547 principal, 548 "https-only-load-insecure" 549 ); 550 switch (state) { 551 case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION: 552 return 2; // Off temporarily 553 case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW: 554 return 1; // Off 555 default: 556 return 0; // On 557 } 558 }, 559 560 /** 561 * Sets/removes HTTPS-Only Mode exception and possibly reloads the page. 562 */ 563 changeHttpsOnlyPermission() { 564 // Get the new value from the menulist and the current value 565 // Note: value and permission association is laid out 566 // in _getHttpsOnlyPermission 567 const oldValue = this._getHttpsOnlyPermission(); 568 if (oldValue < 0) { 569 console.error( 570 "Did not update HTTPS-Only permission since scheme is incompatible" 571 ); 572 return; 573 } 574 575 let newValue = parseInt( 576 this._identityPopupHttpsOnlyModeMenuList.selectedItem.value, 577 10 578 ); 579 580 // If nothing changed, just return here 581 if (newValue === oldValue) { 582 return; 583 } 584 585 // We always want to set the exception for the HTTP version of the current URI, 586 // since when we check wether we should upgrade a request, we are checking permissons 587 // for the HTTP principal (Bug 1757297). 588 let newURI = gBrowser.currentURI; 589 if (newURI instanceof Ci.nsINestedURI) { 590 newURI = newURI.QueryInterface(Ci.nsINestedURI).innermostURI; 591 } 592 newURI = newURI.mutate().setScheme("http").finalize(); 593 const principal = Services.scriptSecurityManager.createContentPrincipal( 594 newURI, 595 gBrowser.contentPrincipal.originAttributes 596 ); 597 598 // Set or remove the permission 599 if (newValue === 0) { 600 SitePermissions.removeFromPrincipal( 601 principal, 602 "https-only-load-insecure" 603 ); 604 } else if (newValue === 1) { 605 SitePermissions.setForPrincipal( 606 principal, 607 "https-only-load-insecure", 608 Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW, 609 SitePermissions.SCOPE_PERSISTENT 610 ); 611 } else { 612 SitePermissions.setForPrincipal( 613 principal, 614 "https-only-load-insecure", 615 Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION, 616 SitePermissions.SCOPE_SESSION 617 ); 618 } 619 620 // If we're on the error-page, we have to redirect the user 621 // from HTTPS to HTTP. Otherwise we can just reload the page. 622 if (this._isAboutHttpsOnlyErrorPage) { 623 gBrowser.loadURI(newURI, { 624 triggeringPrincipal: 625 Services.scriptSecurityManager.getSystemPrincipal(), 626 loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, 627 }); 628 if (this._popupInitialized) { 629 PanelMultiView.hidePopup(this._identityPopup); 630 } 631 return; 632 } 633 // The page only needs to reload if we switch between allow and block 634 // Because "off" is 1 and "off temporarily" is 2, we can just check if the 635 // sum of newValue and oldValue is 3. 636 if (newValue + oldValue !== 3) { 637 BrowserCommands.reloadSkipCache(); 638 if (this._popupInitialized) { 639 PanelMultiView.hidePopup(this._identityPopup); 640 } 641 // Ensure the browser is focused again, otherwise we may not trigger the 642 // security delay on a potential error page following this reload. 643 gBrowser.selectedBrowser.focus(); 644 return; 645 } 646 // Otherwise we just refresh the interface 647 this.refreshIdentityPopup(); 648 }, 649 650 /** 651 * Helper to parse out the important parts of the given certificate for use 652 * in constructing identity UI strings. 653 */ 654 getIdentityData(cert = this._secInfo.serverCert) { 655 var result = {}; 656 657 // Human readable name of Subject 658 result.subjectOrg = cert.organization; 659 660 // SubjectName fields, broken up for individual access 661 if (cert.subjectName) { 662 result.subjectNameFields = {}; 663 cert.subjectName.split(",").forEach(function (v) { 664 var field = v.split("="); 665 this[field[0]] = field[1]; 666 }, result.subjectNameFields); 667 668 // Call out city, state, and country specifically 669 result.city = result.subjectNameFields.L; 670 result.state = result.subjectNameFields.ST; 671 result.country = result.subjectNameFields.C; 672 } 673 674 // Human readable name of Certificate Authority 675 result.caOrg = cert.issuerOrganization || cert.issuerCommonName; 676 result.cert = cert; 677 678 return result; 679 }, 680 681 _getIsSecureContext() { 682 if (gBrowser.contentPrincipal?.originNoSuffix != "resource://pdf.js") { 683 return gBrowser.securityUI.isSecureContext; 684 } 685 686 // For PDF viewer pages (pdf.js) we can't rely on the isSecureContext field. 687 // The backend will return isSecureContext = true, because the content 688 // principal has a resource:// URI. Instead use the URI of the selected 689 // browser to perform the isPotentiallyTrustWorthy check. 690 691 let principal; 692 try { 693 principal = Services.scriptSecurityManager.createContentPrincipal( 694 gBrowser.selectedBrowser.documentURI, 695 {} 696 ); 697 return principal.isOriginPotentiallyTrustworthy; 698 } catch (error) { 699 console.error( 700 "Error while computing isPotentiallyTrustWorthy for pdf viewer page: ", 701 error 702 ); 703 return false; 704 } 705 }, 706 707 /** 708 * Update the identity user interface for the page currently being displayed. 709 * 710 * This examines the SSL certificate metadata, if available, as well as the 711 * connection type and other security-related state information for the page. 712 * 713 * @param state 714 * Bitmask provided by nsIWebProgressListener.onSecurityChange. 715 * @param uri 716 * nsIURI for which the identity UI should be displayed, already 717 * processed by createExposableURI. 718 */ 719 updateIdentity(state, uri) { 720 let locationChanged = this._uri && this._uri.spec != uri.spec; 721 this._state = state; 722 723 // Firstly, populate the state properties required to display the UI. See 724 // the documentation of the individual properties for details. 725 this.setURI(uri); 726 this._secInfo = gBrowser.securityUI.secInfo; 727 this._isSecureContext = this._getIsSecureContext(); 728 if (locationChanged) { 729 this._qwac = null; 730 this._qwacStatusPromise = null; 731 } 732 // Then, update the user interface with the available data. 733 this.refreshIdentityBlock(); 734 // Handle a location change while the Control Center is focused 735 // by closing the popup (bug 1207542) 736 if (locationChanged) { 737 this.hidePopup(); 738 gPermissionPanel.hidePopup(); 739 } 740 741 // NOTE: We do NOT update the identity popup (the control center) when 742 // we receive a new security state on the existing page (i.e. from a 743 // subframe). If the user opened the popup and looks at the provided 744 // information we don't want to suddenly change the panel contents. 745 }, 746 747 /** 748 * Attempt to provide proper IDN treatment for host names 749 */ 750 getEffectiveHost() { 751 if (!this._IDNService) { 752 this._IDNService = Cc["@mozilla.org/network/idn-service;1"].getService( 753 Ci.nsIIDNService 754 ); 755 } 756 try { 757 return this._IDNService.convertToDisplayIDN(this._uri.host); 758 } catch (e) { 759 // If something goes wrong (e.g. host is an IP address) just fail back 760 // to the full domain. 761 return this._uri.host; 762 } 763 }, 764 765 getHostForDisplay() { 766 let host = ""; 767 768 try { 769 host = this.getEffectiveHost(); 770 } catch (e) { 771 // Some URIs might have no hosts. 772 } 773 774 if (this._uri.schemeIs("about")) { 775 // For example in about:certificate the original URL is 776 // about:certificate?cert=<large base64 encoded data>&cert=<large base64 encoded data>&cert=... 777 // So, instead of showing that large string in the identity panel header, we are just showing 778 // about:certificate now. For the other about pages we are just showing about:<page> 779 host = "about:" + this._uri.filePath; 780 } 781 782 if (this._uri.schemeIs("chrome")) { 783 host = this._uri.spec; 784 } 785 786 let readerStrippedURI = ReaderMode.getOriginalUrlObjectForDisplay( 787 this._uri.displaySpec 788 ); 789 if (readerStrippedURI) { 790 host = readerStrippedURI.host; 791 } 792 793 if (this._pageExtensionPolicy) { 794 host = this._pageExtensionPolicy.name; 795 } 796 797 // Fallback for special protocols. 798 if (!host) { 799 host = this._uri.specIgnoringRef; 800 } 801 802 // For tor browser we want to shorten onion addresses for the site identity 803 // panel (gIdentityHandler) to match the circuit display and the onion 804 // authorization panel. 805 // See tor-browser#42091 and tor-browser#41600. 806 // This will also shorten addresses for other consumers of this method, 807 // which includes the permissions panel (gPermissionPanel) and the 808 // protections panel (gProtectionsHandler), although the latter is hidden in 809 // tor browser. 810 return TorUIUtils.shortenOnionAddress(host); 811 }, 812 813 /** 814 * Return the CSS class name to set on the "fullscreen-warning" element to 815 * display information about connection security in the notification shown 816 * when a site enters the fullscreen mode. 817 */ 818 get pointerlockFsWarningClassName() { 819 // Note that the fullscreen warning does not handle _isSecureInternalUI. 820 if (this._uriHasHost && this._isSecureConnection) { 821 return this._uriIsOnionHost ? "onionVerifiedDomain" : "verifiedDomain"; 822 } 823 return this._uriIsOnionHost ? "onionUnknownIdentity" : "unknownIdentity"; 824 }, 825 826 /** 827 * Returns whether the issuer of the current certificate chain is 828 * built-in (returns false) or imported (returns true). 829 */ 830 _hasCustomRoot() { 831 // HTTP Onion Sites are considered secure, but will not not have _secInfo. 832 // FIXME: Self-signed HTTPS Onion Sites are also deemed secure, but this 833 // function will return true for them, creating a warning about an exception 834 // that cannot be actually removed. 835 return !!this._secInfo && !this._secInfo.isBuiltCertChainRootBuiltInRoot; 836 }, 837 838 /** 839 * Returns whether the current URI results in an "invalid" 840 * URL bar state, which effectively means hidden security 841 * indicators. 842 */ 843 _hasInvalidPageProxyState() { 844 return ( 845 !this._uriHasHost && 846 this._uri && 847 isBlankPageURL(this._uri.spec) && 848 !ExtensionUtils.isExtensionUrl(this._uri) 849 ); 850 }, 851 852 /** 853 * Updates the security identity in the identity block. 854 */ 855 _refreshIdentityIcons() { 856 let icon_label = ""; 857 let tooltip = ""; 858 859 let warnTextOnInsecure = 860 this._insecureConnectionTextEnabled || 861 (this._insecureConnectionTextPBModeEnabled && 862 PrivateBrowsingUtils.isWindowPrivate(window)); 863 864 if (this._isSecureInternalUI) { 865 // This is a secure internal Firefox page. 866 this._identityBox.className = "chromeUI"; 867 let brandBundle = document.getElementById("bundle_brand"); 868 icon_label = brandBundle.getString("brandShorterName"); 869 } else if (this._pageExtensionPolicy) { 870 // This is a WebExtension page. 871 this._identityBox.className = "extensionPage"; 872 let extensionName = this._pageExtensionPolicy.name; 873 icon_label = gNavigatorBundle.getFormattedString( 874 "identity.extension.label", 875 [extensionName] 876 ); 877 } else if (this._uriHasHost && this._isSecureConnection && this._secInfo) { 878 // This is a secure connection. 879 // _isSecureConnection implicitly includes onion services, which may not have an SSL certificate 880 const uriIsOnionHost = this._uriIsOnionHost; 881 this._identityBox.className = uriIsOnionHost 882 ? "onionVerifiedDomain" 883 : "verifiedDomain"; 884 if (this._isMixedActiveContentBlocked) { 885 this._identityBox.classList.add( 886 uriIsOnionHost ? "onionMixedActiveBlocked" : "mixedActiveBlocked" 887 ); 888 } 889 if (!this._isCertUserOverridden) { 890 // It's a normal cert, verifier is the CA Org. 891 tooltip = gNavigatorBundle.getFormattedString( 892 "identity.identified.verifier", 893 [this.getIdentityData().caOrg] 894 ); 895 } 896 } else if (this._isBrokenConnection) { 897 // This is a secure connection, but something is wrong. 898 const uriIsOnionHost = this._uriIsOnionHost; 899 this._identityBox.className = uriIsOnionHost 900 ? "onionUnknownIdentity" 901 : "unknownIdentity"; 902 903 if (this._isMixedActiveContentLoaded) { 904 this._identityBox.classList.add( 905 uriIsOnionHost ? "onionMixedActiveContent" : "mixedActiveContent" 906 ); 907 if ( 908 UrlbarPrefs.getScotchBonnetPref("trimHttps") && 909 warnTextOnInsecure 910 ) { 911 icon_label = gNavigatorBundle.getString("identity.notSecure.label"); 912 tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip"); 913 this._identityBox.classList.add("notSecureText"); 914 } 915 } else if (this._isMixedActiveContentBlocked) { 916 this._identityBox.classList.add( 917 uriIsOnionHost 918 ? "onionMixedDisplayContentLoadedActiveBlocked" 919 : "mixedDisplayContentLoadedActiveBlocked" 920 ); 921 } else if (this._isMixedPassiveContentLoaded) { 922 this._identityBox.classList.add( 923 uriIsOnionHost ? "onionMixedDisplayContent" : "mixedDisplayContent" 924 ); 925 } else { 926 // TODO: ignore weak https cipher for onionsites? 927 this._identityBox.classList.add("weakCipher"); 928 } 929 } else if (this._isCertErrorPage) { 930 // We show a warning lock icon for certificate errors, and 931 // show the "Not Secure" text. 932 this._identityBox.className = "certErrorPage notSecureText"; 933 icon_label = gNavigatorBundle.getString("identity.notSecure.label"); 934 tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip"); 935 } else if (this._isAboutHttpsOnlyErrorPage) { 936 // We show a not secure lock icon for 'about:httpsonlyerror' page. 937 this._identityBox.className = "httpsOnlyErrorPage"; 938 } else if ( 939 this._isAboutNetErrorPage || 940 this._isAboutBlockedPage || 941 this._isAssociatedIdentity 942 ) { 943 // Network errors, blocked pages, and pages associated 944 // with another page get a more neutral icon 945 this._identityBox.className = "unknownIdentity"; 946 } else if (this._uriIsOnionHost) { 947 this._identityBox.className = "onionUnknownIdentity"; 948 } else if (this._isPotentiallyTrustworthy) { 949 // This is a local resource (and shouldn't be marked insecure). 950 this._identityBox.className = "localResource"; 951 } else { 952 // This is an insecure connection. 953 let className = "notSecure"; 954 this._identityBox.className = className; 955 tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip"); 956 if (warnTextOnInsecure) { 957 icon_label = gNavigatorBundle.getString("identity.notSecure.label"); 958 this._identityBox.classList.add("notSecureText"); 959 } 960 } 961 962 if (this._isCertUserOverridden) { 963 const uriIsOnionHost = this._uriIsOnionHost; 964 this._identityBox.classList.add( 965 uriIsOnionHost ? "onionCertUserOverridden" : "certUserOverridden" 966 ); 967 // Cert is trusted because of a security exception, verifier is a special string. 968 tooltip = gNavigatorBundle.getString( 969 "identity.identified.verified_by_you" 970 ); 971 } 972 973 // Push the appropriate strings out to the UI 974 this._identityIcon.setAttribute("tooltiptext", tooltip); 975 976 if (this._pageExtensionPolicy) { 977 let extensionName = this._pageExtensionPolicy.name; 978 this._identityIcon.setAttribute( 979 "tooltiptext", 980 gNavigatorBundle.getFormattedString("identity.extension.tooltip", [ 981 extensionName, 982 ]) 983 ); 984 } 985 986 this._identityIconLabel.setAttribute("tooltiptext", tooltip); 987 this._identityIconLabel.setAttribute("value", icon_label); 988 this._identityIconLabel.collapsed = !icon_label; 989 }, 990 991 /** 992 * Updates the identity block user interface with the data from this object. 993 */ 994 refreshIdentityBlock() { 995 if (!this._identityBox) { 996 return; 997 } 998 999 this._refreshIdentityIcons(); 1000 1001 // If this condition is true, the URL bar will have an "invalid" 1002 // pageproxystate, so we should hide the permission icons. 1003 if (this._hasInvalidPageProxyState()) { 1004 gPermissionPanel.hidePermissionIcons(); 1005 } else { 1006 gPermissionPanel.refreshPermissionIcons(); 1007 } 1008 1009 // Hide the shield icon if it is a chrome page. 1010 gProtectionsHandler._trackingProtectionIconContainer.classList.toggle( 1011 "chromeUI", 1012 this._isSecureInternalUI 1013 ); 1014 }, 1015 1016 /** 1017 * Determines the string used to describe the connection security 1018 * information. 1019 */ 1020 getConnectionSecurityInformation() { 1021 if (this._isSecureInternalUI) { 1022 return "chrome"; 1023 } else if (this._pageExtensionPolicy) { 1024 return "extension"; 1025 } else if (this._isURILoadedFromFile) { 1026 return "file"; 1027 } else if (this._qwac) { 1028 return "secure-etsi"; 1029 } else if (this._isEV) { 1030 return "secure-ev"; 1031 } else if (this._isCertUserOverridden) { 1032 return "secure-cert-user-overridden"; 1033 } else if (this._isSecureConnection) { 1034 return "secure"; 1035 } else if (this._isCertErrorPage) { 1036 return "cert-error-page"; 1037 } else if (this._isAboutHttpsOnlyErrorPage) { 1038 return "https-only-error-page"; 1039 } else if (this._isAboutBlockedPage) { 1040 return "not-secure"; 1041 } else if (this._isSecurelyConnectedAboutNetErrorPage) { 1042 return "secure"; 1043 } else if (this._isAboutNetErrorPage) { 1044 return "net-error-page"; 1045 } else if (this._isAssociatedIdentity) { 1046 return "associated"; 1047 } else if (this._isPotentiallyTrustworthy) { 1048 return "file"; 1049 } 1050 return "not-secure"; 1051 }, 1052 1053 /** 1054 * Set up the title and content messages for the identity message popup, 1055 * based on the specified mode, and the details of the SSL cert, where 1056 * applicable 1057 */ 1058 refreshIdentityPopup() { 1059 // Update cookies and site data information and show the 1060 // "Clear Site Data" button if the site is storing local data, and 1061 // if the page is not controlled by a WebExtension. 1062 this._clearSiteDataFooter.hidden = true; 1063 let identityPopupPanelView = document.getElementById( 1064 "identity-popup-mainView" 1065 ); 1066 identityPopupPanelView.removeAttribute("footerVisible"); 1067 // Bug 1754172 - Only show the clear site data footer if we're not in private browsing. 1068 if ( 1069 !PrivateBrowsingUtils.isWindowPrivate(window) && 1070 this._uriHasHost && 1071 !this._pageExtensionPolicy 1072 ) { 1073 SiteDataManager.hasSiteData(this._uri.asciiHost).then(hasData => { 1074 this._clearSiteDataFooter.hidden = !hasData; 1075 identityPopupPanelView.setAttribute("footerVisible", hasData); 1076 }); 1077 } 1078 1079 let customRoot = false; 1080 1081 // Determine connection security information. 1082 let connection = this.getConnectionSecurityInformation(); 1083 if (this._isSecureConnection) { 1084 customRoot = this._hasCustomRoot(); 1085 } 1086 1087 let securityButtonNode = document.getElementById( 1088 "identity-popup-security-button" 1089 ); 1090 1091 let disableSecurityButton = ![ 1092 "not-secure", 1093 "secure", 1094 "secure-etsi", 1095 "secure-ev", 1096 "secure-cert-user-overridden", 1097 "cert-error-page", 1098 "net-error-page", 1099 "https-only-error-page", 1100 ].includes(connection); 1101 if (disableSecurityButton) { 1102 securityButtonNode.disabled = true; 1103 securityButtonNode.classList.remove("subviewbutton-nav"); 1104 } else { 1105 securityButtonNode.disabled = false; 1106 securityButtonNode.classList.add("subviewbutton-nav"); 1107 } 1108 1109 // Determine the mixed content state. 1110 let mixedcontent = []; 1111 if (this._isMixedPassiveContentLoaded) { 1112 mixedcontent.push("passive-loaded"); 1113 } 1114 if (this._isMixedActiveContentLoaded) { 1115 mixedcontent.push("active-loaded"); 1116 } else if (this._isMixedActiveContentBlocked) { 1117 mixedcontent.push("active-blocked"); 1118 } 1119 mixedcontent = mixedcontent.join(" "); 1120 1121 // We have no specific flags for weak ciphers (yet). If a connection is 1122 // broken and we can't detect any mixed content loaded then it's a weak 1123 // cipher. 1124 let ciphers = ""; 1125 if ( 1126 this._isBrokenConnection && 1127 !this._isMixedActiveContentLoaded && 1128 !this._isMixedPassiveContentLoaded 1129 ) { 1130 ciphers = "weak"; 1131 } 1132 1133 // If HTTPS-Only Mode is enabled, check the permission status 1134 const privateBrowsingWindow = PrivateBrowsingUtils.isWindowPrivate(window); 1135 const isHttpsOnlyModeActive = this._isHttpsOnlyModeActive( 1136 privateBrowsingWindow 1137 ); 1138 const isHttpsFirstModeActive = this._isHttpsFirstModeActive( 1139 privateBrowsingWindow 1140 ); 1141 const isSchemelessHttpsFirstModeActive = 1142 this._isSchemelessHttpsFirstModeActive(privateBrowsingWindow); 1143 let httpsOnlyStatus = ""; 1144 if ( 1145 isHttpsFirstModeActive || 1146 isHttpsOnlyModeActive || 1147 isSchemelessHttpsFirstModeActive 1148 ) { 1149 // Note: value and permission association is laid out 1150 // in _getHttpsOnlyPermission 1151 let value = this._getHttpsOnlyPermission(); 1152 1153 // We do not want to display the exception ui for schemeless 1154 // HTTPS-First, but we still want the "Upgraded to HTTPS" label. 1155 this._identityPopupHttpsOnlyMode.hidden = 1156 isSchemelessHttpsFirstModeActive; 1157 1158 this._identityPopupHttpsOnlyModeMenuListOffItem.hidden = 1159 privateBrowsingWindow && value != 1; 1160 1161 this._identityPopupHttpsOnlyModeMenuList.value = value; 1162 1163 if (value > 0) { 1164 httpsOnlyStatus = "exception"; 1165 } else if ( 1166 this._isAboutHttpsOnlyErrorPage || 1167 (isHttpsFirstModeActive && this._isContentHttpsOnlyModeUpgradeFailed) 1168 ) { 1169 httpsOnlyStatus = "failed-top"; 1170 } else if (this._isContentHttpsOnlyModeUpgradeFailed) { 1171 httpsOnlyStatus = "failed-sub"; 1172 } else if ( 1173 this._isContentHttpsOnlyModeUpgraded || 1174 this._isContentHttpsFirstModeUpgraded 1175 ) { 1176 httpsOnlyStatus = "upgraded"; 1177 } 1178 } 1179 1180 // Update all elements. 1181 let elementIDs = [ 1182 "identity-popup", 1183 "identity-popup-securityView-extended-info", 1184 ]; 1185 1186 for (let id of elementIDs) { 1187 let element = document.getElementById(id); 1188 this._updateAttribute(element, "connection", connection); 1189 this._updateAttribute(element, "ciphers", ciphers); 1190 this._updateAttribute(element, "mixedcontent", mixedcontent); 1191 this._updateAttribute(element, "isbroken", this._isBrokenConnection); 1192 this._updateAttribute(element, "customroot", customRoot); 1193 this._updateAttribute(element, "httpsonlystatus", httpsOnlyStatus); 1194 } 1195 1196 // Initialize the optional strings to empty values 1197 let supplemental = ""; 1198 let verifier = ""; 1199 let host = this.getHostForDisplay(); 1200 let owner = ""; 1201 1202 // Fill in the CA name if we have a valid TLS certificate. 1203 if (this._isSecureConnection || this._isCertUserOverridden) { 1204 verifier = this._identityIconLabel.tooltipText; 1205 } 1206 1207 // Fill in organization information if we have a valid EV certificate or 1208 // QWAC. 1209 if (this._isEV || this._qwac) { 1210 let iData = this.getIdentityData(this._qwac || this._secInfo.serverCert); 1211 owner = iData.subjectOrg; 1212 verifier = this._identityIconLabel.tooltipText; 1213 1214 // Build an appropriate supplemental block out of whatever location data we have 1215 if (iData.city) { 1216 supplemental += iData.city + "\n"; 1217 } 1218 if (iData.state && iData.country) { 1219 supplemental += gNavigatorBundle.getFormattedString( 1220 "identity.identified.state_and_country", 1221 [iData.state, iData.country] 1222 ); 1223 } else if (iData.state) { 1224 // State only 1225 supplemental += iData.state; 1226 } else if (iData.country) { 1227 // Country only 1228 supplemental += iData.country; 1229 } 1230 } 1231 1232 // Push the appropriate strings out to the UI. 1233 document.l10n.setAttributes( 1234 this._identityPopupMainViewHeaderLabel, 1235 "identity-site-information", 1236 { 1237 host, 1238 } 1239 ); 1240 1241 document.l10n.setAttributes( 1242 this._identityPopupSecurityView, 1243 "identity-header-security-with-host", 1244 { 1245 host, 1246 } 1247 ); 1248 1249 document.l10n.setAttributes( 1250 this._identityPopupMainViewHeaderLabel, 1251 "identity-site-information", 1252 { 1253 host, 1254 } 1255 ); 1256 1257 this._identityPopupSecurityEVContentOwner.textContent = 1258 gNavigatorBundle.getFormattedString("identity.ev.contentOwner2", [owner]); 1259 1260 this._identityPopupContentOwner.textContent = owner; 1261 this._identityPopupContentSupp.textContent = supplemental; 1262 this._identityPopupContentVerif.textContent = verifier; 1263 }, 1264 1265 setURI(uri) { 1266 // Unnest the URI, turning "view-source:https://example.com" into 1267 // "https://example.com" for example. "about:" URIs are a special exception 1268 // here, as some of them have a hidden moz-safe-about inner URI we do not 1269 // want to unnest. 1270 while (uri instanceof Ci.nsINestedURI && !uri.schemeIs("about")) { 1271 uri = uri.QueryInterface(Ci.nsINestedURI).innerURI; 1272 } 1273 this._uri = uri; 1274 1275 try { 1276 // Account for file: urls and catch when "" is the value 1277 this._uriHasHost = !!this._uri.host; 1278 } catch (ex) { 1279 this._uriHasHost = false; 1280 } 1281 1282 if (uri.schemeIs("about")) { 1283 let module = E10SUtils.getAboutModule(uri); 1284 if (module) { 1285 let flags = module.getURIFlags(uri); 1286 this._isSecureInternalUI = !!( 1287 flags & Ci.nsIAboutModule.IS_SECURE_CHROME_UI 1288 ); 1289 } 1290 } else { 1291 this._isSecureInternalUI = false; 1292 } 1293 this._pageExtensionPolicy = WebExtensionPolicy.getByURI(uri); 1294 this._isURILoadedFromFile = uri.schemeIs("file"); 1295 }, 1296 1297 /** 1298 * Click handler for the identity-box element in primary chrome. 1299 */ 1300 handleIdentityButtonEvent(event) { 1301 event.stopPropagation(); 1302 1303 if ( 1304 (event.type == "click" && event.button != 0) || 1305 (event.type == "keypress" && 1306 event.charCode != KeyEvent.DOM_VK_SPACE && 1307 event.keyCode != KeyEvent.DOM_VK_RETURN) 1308 ) { 1309 return; // Left click, space or enter only 1310 } 1311 1312 // Don't allow left click, space or enter if the location has been modified. 1313 if (gURLBar.getAttribute("pageproxystate") != "valid") { 1314 return; 1315 } 1316 1317 this._openPopup(event); 1318 }, 1319 1320 _openPopup(event) { 1321 // Make the popup available. 1322 this._initializePopup(); 1323 1324 // Kick off background determination of QWAC status. 1325 if (this._isSecureContext && !this._qwacStatusPromise) { 1326 let qwacStatusPromise = QWACs.determineQWACStatus( 1327 this._secInfo, 1328 this._uri, 1329 gBrowser.selectedBrowser.browsingContext 1330 ).then(result => { 1331 // Check that when this promise resolves, we're still on the same 1332 // document as when it was created. 1333 if (qwacStatusPromise == this._qwacStatusPromise && result) { 1334 this._qwac = result; 1335 this.refreshIdentityPopup(); 1336 } 1337 }); 1338 this._qwacStatusPromise = qwacStatusPromise; 1339 } 1340 1341 // Update the popup strings 1342 this.refreshIdentityPopup(); 1343 1344 // Check the panel state of other panels. Hide them if needed. 1345 let openPanels = Array.from(document.querySelectorAll("panel[openpanel]")); 1346 for (let panel of openPanels) { 1347 PanelMultiView.hidePopup(panel); 1348 } 1349 1350 // Now open the popup, anchored off the primary chrome element 1351 PanelMultiView.openPopup(this._identityPopup, this._identityIconBox, { 1352 position: "bottomleft topleft", 1353 triggerEvent: event, 1354 }).catch(console.error); 1355 }, 1356 1357 onPopupShown(event) { 1358 if (event.target == this._identityPopup) { 1359 PopupNotifications.suppressWhileOpen(this._identityPopup); 1360 window.addEventListener("focus", this, true); 1361 } 1362 }, 1363 1364 onPopupHidden(event) { 1365 if (event.target == this._identityPopup) { 1366 window.removeEventListener("focus", this, true); 1367 } 1368 }, 1369 1370 handleEvent() { 1371 let elem = document.activeElement; 1372 let position = elem.compareDocumentPosition(this._identityPopup); 1373 1374 if ( 1375 !( 1376 position & 1377 (Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_CONTAINED_BY) 1378 ) && 1379 !this._identityPopup.hasAttribute("noautohide") 1380 ) { 1381 // Hide the panel when focusing an element that is 1382 // neither an ancestor nor descendant unless the panel has 1383 // @noautohide (e.g. for a tour). 1384 PanelMultiView.hidePopup(this._identityPopup); 1385 } 1386 }, 1387 1388 observe(subject, topic) { 1389 switch (topic) { 1390 case "perm-changed": { 1391 // Exclude permissions which do not appear in the UI in order to avoid 1392 // doing extra work here. 1393 if (!subject) { 1394 return; 1395 } 1396 let { type } = subject.QueryInterface(Ci.nsIPermission); 1397 if (SitePermissions.isSitePermission(type)) { 1398 this.refreshIdentityBlock(); 1399 } 1400 break; 1401 } 1402 } 1403 }, 1404 1405 onDragStart(event) { 1406 const TEXT_SIZE = 14; 1407 const IMAGE_SIZE = 16; 1408 const SPACING = 5; 1409 1410 if (gURLBar.getAttribute("pageproxystate") != "valid") { 1411 return; 1412 } 1413 1414 let value = gBrowser.currentURI.displaySpec; 1415 let urlString = value + "\n" + gBrowser.contentTitle; 1416 let htmlString = '<a href="' + value + '">' + value + "</a>"; 1417 1418 let scale = window.devicePixelRatio; 1419 let canvas = document.createElementNS( 1420 "http://www.w3.org/1999/xhtml", 1421 "canvas" 1422 ); 1423 canvas.width = 550 * scale; 1424 let ctx = canvas.getContext("2d"); 1425 ctx.font = `${TEXT_SIZE * scale}px sans-serif`; 1426 let tabIcon = gBrowser.selectedTab.iconImage; 1427 let image = new Image(); 1428 image.src = tabIcon.src; 1429 let textWidth = ctx.measureText(value).width / scale; 1430 let textHeight = parseInt(ctx.font, 10) / scale; 1431 let imageHorizontalOffset, imageVerticalOffset; 1432 imageHorizontalOffset = imageVerticalOffset = SPACING; 1433 let textHorizontalOffset = image.width ? IMAGE_SIZE + SPACING * 2 : SPACING; 1434 let textVerticalOffset = textHeight + SPACING - 1; 1435 let backgroundColor = "white"; 1436 let textColor = "black"; 1437 let totalWidth = image.width 1438 ? textWidth + IMAGE_SIZE + 3 * SPACING 1439 : textWidth + 2 * SPACING; 1440 let totalHeight = image.width 1441 ? IMAGE_SIZE + 2 * SPACING 1442 : textHeight + 2 * SPACING; 1443 ctx.fillStyle = backgroundColor; 1444 ctx.fillRect(0, 0, totalWidth * scale, totalHeight * scale); 1445 ctx.fillStyle = textColor; 1446 ctx.fillText( 1447 `${value}`, 1448 textHorizontalOffset * scale, 1449 textVerticalOffset * scale 1450 ); 1451 try { 1452 ctx.drawImage( 1453 image, 1454 imageHorizontalOffset * scale, 1455 imageVerticalOffset * scale, 1456 IMAGE_SIZE * scale, 1457 IMAGE_SIZE * scale 1458 ); 1459 } catch (e) { 1460 // Sites might specify invalid data URIs favicons that 1461 // will result in errors when trying to draw, we can 1462 // just ignore this case and not paint any favicon. 1463 } 1464 1465 let dt = event.dataTransfer; 1466 dt.setData("text/x-moz-url", urlString); 1467 dt.setData("text/uri-list", value); 1468 dt.setData("text/plain", value); 1469 dt.setData("text/html", htmlString); 1470 dt.setDragImage(canvas, 16, 16); 1471 1472 // Don't cover potential drop targets on the toolbars or in content. 1473 gURLBar.view.close(); 1474 }, 1475 1476 _updateAttribute(elem, attr, value) { 1477 if (value) { 1478 elem.setAttribute(attr, value); 1479 } else { 1480 elem.removeAttribute(attr); 1481 } 1482 }, 1483 };