browser-trustPanel.js (43409B)
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 /* import-globals-from browser-siteProtections.js */ 6 7 ChromeUtils.defineESModuleGetters(this, { 8 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 9 ContentBlockingAllowList: 10 "resource://gre/modules/ContentBlockingAllowList.sys.mjs", 11 E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", 12 PanelMultiView: 13 "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs", 14 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 15 SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs", 16 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 17 }); 18 19 XPCOMUtils.defineLazyPreferenceGetter( 20 this, 21 "insecureConnectionTextEnabled", 22 "security.insecure_connection_text.enabled" 23 ); 24 XPCOMUtils.defineLazyPreferenceGetter( 25 this, 26 "insecureConnectionTextPBModeEnabled", 27 "security.insecure_connection_text.pbmode.enabled" 28 ); 29 XPCOMUtils.defineLazyPreferenceGetter( 30 this, 31 "httpsOnlyModeEnabled", 32 "dom.security.https_only_mode" 33 ); 34 XPCOMUtils.defineLazyPreferenceGetter( 35 this, 36 "httpsFirstModeEnabled", 37 "dom.security.https_first" 38 ); 39 XPCOMUtils.defineLazyPreferenceGetter( 40 this, 41 "schemelessHttpsFirstModeEnabled", 42 "dom.security.https_first_schemeless" 43 ); 44 XPCOMUtils.defineLazyPreferenceGetter( 45 this, 46 "httpsFirstModeEnabledPBM", 47 "dom.security.https_first_pbm" 48 ); 49 XPCOMUtils.defineLazyPreferenceGetter( 50 this, 51 "httpsOnlyModeEnabledPBM", 52 "dom.security.https_only_mode_pbm" 53 ); 54 XPCOMUtils.defineLazyPreferenceGetter( 55 this, 56 "popupClickjackDelay", 57 "security.notification_enable_delay", 58 500 59 ); 60 XPCOMUtils.defineLazyPreferenceGetter( 61 this, 62 "smartblockEmbedsEnabledPref", 63 "extensions.webcompat.smartblockEmbeds.enabled", 64 false 65 ); 66 67 const ETP_ENABLED_ASSETS = { 68 label: "trustpanel-etp-label-enabled", 69 description: "trustpanel-etp-description-enabled", 70 header: "trustpanel-header-enabled", 71 innerDescription: "trustpanel-description-enabled2", 72 }; 73 74 const ETP_DISABLED_ASSETS = { 75 label: "trustpanel-etp-label-disabled", 76 description: "trustpanel-etp-description-disabled", 77 header: "trustpanel-header-disabled", 78 innerDescription: "trustpanel-description-disabled", 79 }; 80 81 const SMARTBLOCK_EMBED_INFO = [ 82 { 83 matchPatterns: ["https://itisatracker.org/*"], 84 shimId: "EmbedTestShim", 85 displayName: "Test", 86 }, 87 { 88 matchPatterns: [ 89 "https://www.instagram.com/*", 90 "https://platform.instagram.com/*", 91 ], 92 shimId: "InstagramEmbed", 93 displayName: "Instagram", 94 }, 95 { 96 matchPatterns: ["https://www.tiktok.com/*"], 97 shimId: "TiktokEmbed", 98 displayName: "Tiktok", 99 }, 100 { 101 matchPatterns: ["https://platform.twitter.com/*"], 102 shimId: "TwitterEmbed", 103 displayName: "X", 104 }, 105 { 106 matchPatterns: ["https://*.disqus.com/*"], 107 shimId: "DisqusEmbed", 108 displayName: "Disqus", 109 }, 110 ]; 111 112 class TrustPanel { 113 #state = null; 114 #secInfo = null; 115 #host = null; 116 #uri = null; 117 #uriHasHost = null; 118 #pageExtensionPolicy = null; 119 #isSecureContext = null; 120 #isSecureInternalUI = null; 121 122 #lastEvent = null; 123 124 #popupToggleDelayTimer = null; 125 #openingReason = null; 126 127 #blockers = { 128 SocialTracking, 129 ThirdPartyCookies, 130 TrackingProtection, 131 Fingerprinting, 132 Cryptomining, 133 }; 134 135 init() { 136 for (let blocker of Object.values(this.#blockers)) { 137 if (blocker.init) { 138 blocker.init(); 139 } 140 } 141 142 // Add an observer to listen to requests to open the protections panel 143 Services.obs.addObserver(this, "smartblock:open-protections-panel"); 144 } 145 146 uninit() { 147 for (let blocker of Object.values(this.#blockers)) { 148 if (blocker.uninit) { 149 blocker.uninit(); 150 } 151 } 152 153 Services.obs.removeObserver(this, "smartblock:open-protections-panel"); 154 } 155 156 get #popup() { 157 return document.getElementById("trustpanel-popup"); 158 } 159 160 get #enabled() { 161 return UrlbarPrefs.get("trustPanel.featureGate"); 162 } 163 164 handleProtectionsButtonEvent(event) { 165 event.stopPropagation(); 166 if ( 167 (event.type == "click" && event.button != 0) || 168 (event.type == "keypress" && 169 event.charCode != KeyEvent.DOM_VK_SPACE && 170 event.keyCode != KeyEvent.DOM_VK_RETURN) 171 ) { 172 return; // Left click, space or enter only 173 } 174 175 this.showPopup({ event, openingReason: "shieldButtonClicked" }); 176 } 177 178 async onContentBlockingEvent( 179 event, 180 _webProgress, 181 _isSimulated, 182 _previousState 183 ) { 184 if (!this.#enabled) { 185 return; 186 } 187 // First update all our internal state based on the allowlist and the 188 // different blockers: 189 this.anyDetected = false; 190 this.anyBlocking = false; 191 this.#lastEvent = event; 192 193 // Check whether the user has added an exception for this site. 194 this.hasException = 195 ContentBlockingAllowList.canHandle(window.gBrowser.selectedBrowser) && 196 ContentBlockingAllowList.includes(window.gBrowser.selectedBrowser); 197 198 // Update blocker state and find if they detected or blocked anything. 199 for (let blocker of Object.values(this.#blockers)) { 200 // Store data on whether the blocker is activated for reporting it 201 // using the "report breakage" dialog. Under normal circumstances this 202 // dialog should only be able to open in the currently selected tab 203 // and onSecurityChange runs on tab switch, so we can avoid associating 204 // the data with the document directly. 205 blocker.activated = blocker.isBlocking(event); 206 this.anyDetected = this.anyDetected || blocker.isDetected(event); 207 this.anyBlocking = this.anyBlocking || blocker.activated; 208 } 209 210 if (this.#popup) { 211 await this.#updatePopup(); 212 } 213 } 214 215 #initializePopup() { 216 if (!this.#popup) { 217 let wrapper = document.getElementById("template-trustpanel-popup"); 218 wrapper.replaceWith(wrapper.content); 219 220 document 221 .getElementById("trustpanel-popup-connection") 222 .addEventListener("click", event => 223 this.#openSecurityInformationSubview(event) 224 ); 225 document 226 .getElementById("trustpanel-blocker-see-all") 227 .addEventListener("click", event => this.#openBlockerSubview(event)); 228 document 229 .getElementById("trustpanel-privacy-link") 230 .addEventListener("click", () => 231 window.openTrustedLinkIn("about:preferences#privacy", "tab") 232 ); 233 document 234 .getElementById("trustpanel-clear-cookies-button") 235 .addEventListener("click", event => 236 this.#showClearCookiesSubview(event) 237 ); 238 document 239 .getElementById("trustpanel-siteinformation-morelink") 240 .addEventListener("click", () => this.#showSecurityPopup()); 241 document 242 .getElementById("trustpanel-clear-cookie-cancel") 243 .addEventListener("click", () => this.#hidePopup()); 244 document 245 .getElementById("trustpanel-clear-cookie-clear") 246 .addEventListener("click", () => this.#clearSiteData()); 247 document 248 .getElementById("trustpanel-toggle") 249 .addEventListener("click", () => this.#toggleTrackingProtection()); 250 document 251 .getElementById("identity-popup-remove-cert-exception") 252 .addEventListener("click", () => this.#removeCertException()); 253 document 254 .getElementById("trustpanel-popup-security-httpsonlymode-menulist") 255 .addEventListener("command", () => this.#changeHttpsOnlyPermission()); 256 257 this.#popup.addEventListener("popupshown", this); 258 } 259 } 260 261 async showPopup(opts = {}) { 262 this.#initializePopup(); 263 await this.#updatePopup(); 264 265 this.#openingReason = opts.reason; 266 267 let anchor = document.getElementById("trust-icon-container"); 268 PanelMultiView.openPopup(this.#popup, anchor, { 269 position: "bottomleft topleft", 270 }); 271 } 272 273 async #hidePopup() { 274 let hidden = new Promise(c => { 275 this.#popup.addEventListener("popuphidden", c, { once: true }); 276 }); 277 PanelMultiView.hidePopup(this.#popup); 278 await hidden; 279 } 280 281 updateIdentity(state, uri) { 282 if (!this.#enabled) { 283 return; 284 } 285 try { 286 // Account for file: urls and catch when "" is the value 287 this.#uriHasHost = !!uri.host; 288 } catch (ex) { 289 this.#uriHasHost = false; 290 } 291 this.#state = state; 292 this.#uri = uri; 293 294 this.#secInfo = gBrowser.securityUI.secInfo; 295 this.#pageExtensionPolicy = WebExtensionPolicy.getByURI(uri); 296 this.#isSecureContext = this.#getIsSecureContext(); 297 298 this.#isSecureInternalUI = false; 299 if (this.#uri.schemeIs("about")) { 300 let module = E10SUtils.getAboutModule(this.#uri); 301 if (module) { 302 let flags = module.getURIFlags(this.#uri); 303 this.#isSecureInternalUI = !!( 304 flags & Ci.nsIAboutModule.IS_SECURE_CHROME_UI 305 ); 306 } 307 } 308 309 this.#updateUrlbarIcon(); 310 } 311 312 #updateUrlbarIcon() { 313 let icon = document.getElementById("trust-icon-container"); 314 icon.className = this.#isSecurePage() ? "secure" : "insecure"; 315 316 if (this.#isURILoadedFromFile) { 317 icon.classList.add("file"); 318 } 319 320 if (!this.#trackingProtectionEnabled) { 321 icon.classList.add("inactive"); 322 } 323 324 icon.setAttribute("tooltiptext", this.#tooltipText()); 325 icon.classList.toggle("chickletShown", this.#isSecureInternalUI); 326 } 327 328 async #updatePopup() { 329 if (this.#uri) { 330 this.#host = BrowserUtils.formatURIForDisplay(this.#uri, { 331 onlyBaseDomain: true, 332 }); 333 } else { 334 this.#host = ""; 335 } 336 this.#popup.setAttribute("connection", this.#connectionState()); 337 this.#popup.setAttribute( 338 "tracking-protection", 339 this.#trackingProtectionStatus() 340 ); 341 342 await this.#updateMainView(); 343 } 344 345 async #updateMainView() { 346 let secureConnection = this.#isSecurePage(); 347 let assets = this.#trackingProtectionEnabled 348 ? ETP_ENABLED_ASSETS 349 : ETP_DISABLED_ASSETS; 350 351 if (this.#uri) { 352 let favicon = await PlacesUtils.favicons.getFaviconForPage(this.#uri); 353 document.getElementById("trustpanel-popup-icon").src = 354 favicon?.uri.spec ?? ""; 355 } 356 357 let toggle = document.getElementById("trustpanel-toggle"); 358 toggle.toggleAttribute("pressed", this.#trackingProtectionEnabled); 359 document.l10n.setAttributes( 360 toggle, 361 this.#trackingProtectionEnabled 362 ? "trustpanel-etp-toggle-on" 363 : "trustpanel-etp-toggle-off", 364 { host: this.#host } 365 ); 366 367 let hostElement = document.getElementById("trustpanel-popup-host"); 368 hostElement.setAttribute("value", this.#host); 369 hostElement.setAttribute("tooltiptext", this.#host); 370 371 document.l10n.setAttributes( 372 document.getElementById("trustpanel-etp-label"), 373 assets.label 374 ); 375 document.l10n.setAttributes( 376 document.getElementById("trustpanel-etp-description"), 377 assets.description 378 ); 379 document.l10n.setAttributes( 380 document.getElementById("trustpanel-header"), 381 assets.header 382 ); 383 document.l10n.setAttributes( 384 document.getElementById("trustpanel-description"), 385 assets.innerDescription 386 ); 387 document.l10n.setAttributes( 388 document.getElementById("trustpanel-connection-label"), 389 secureConnection 390 ? "trustpanel-connection-label-secure" 391 : "trustpanel-connection-label-insecure" 392 ); 393 394 this.#updateAttribute( 395 document.getElementById("trustpanel-blocker-section"), 396 "hidden", 397 !this.anyDetected 398 ); 399 await this.#updateBlockerView(); 400 } 401 402 async #updateBlockerView() { 403 let count = this.#fetchSmartBlocked().length; 404 let blocked = []; 405 let detected = []; 406 407 for (let blocker of Object.values(this.#blockers)) { 408 if (blocker.isBlocking(this.#lastEvent)) { 409 blocked.push(blocker); 410 count += await blocker.getBlockerCount(); 411 } else if (blocker.isDetected(this.#lastEvent)) { 412 detected.push(blocker); 413 } 414 } 415 416 this.#addButtons("trustpanel-blocked", blocked, true); 417 this.#addButtons("trustpanel-detected", detected, false); 418 419 document 420 .getElementById("trustpanel-smartblock-section") 421 .toggleAttribute("hidden", !this.#addSmartblockEmbedToggles()); 422 423 // This element is in the main view but updated in case 424 // any content blocking events were missed. 425 document.l10n.setArgs( 426 document.getElementById("trustpanel-blocker-section-header"), 427 { count } 428 ); 429 } 430 431 async #showSecurityPopup() { 432 await this.#hidePopup(); 433 window.BrowserCommands.pageInfo(null, "securityTab"); 434 } 435 436 #removeCertException() { 437 let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService( 438 Ci.nsICertOverrideService 439 ); 440 overrideService.clearValidityOverride( 441 this.#uri.host, 442 this.#uri.port > 0 ? this.#uri.port : 443, 443 gBrowser.contentPrincipal.originAttributes 444 ); 445 BrowserCommands.reloadSkipCache(); 446 PanelMultiView.hidePopup(this.#popup); 447 } 448 449 #trackingProtectionStatus() { 450 if (!this.#isSecurePage()) { 451 return "warning"; 452 } 453 return this.#trackingProtectionEnabled ? "enabled" : "disabled"; 454 } 455 456 #openSecurityInformationSubview(event) { 457 document.l10n.setAttributes( 458 document.getElementById("trustpanel-securityInformationView"), 459 "trustpanel-site-information-header", 460 { host: this.#host } 461 ); 462 463 let customRoot = this.#isSecureConnection ? this.#hasCustomRoot() : false; 464 let connection = this.#connectionState(); 465 let mixedcontent = this.#mixedContentState(); 466 let ciphers = this.#ciphersState(); 467 let httpsOnlyStatus = this.#httpsOnlyState(); 468 469 // Update all elements. 470 let elementIDs = [ 471 "trustpanel-popup", 472 "identity-popup-securityView-extended-info", 473 ]; 474 475 for (let id of elementIDs) { 476 let element = document.getElementById(id); 477 this.#updateAttribute(element, "connection", connection); 478 this.#updateAttribute(element, "ciphers", ciphers); 479 this.#updateAttribute(element, "mixedcontent", mixedcontent); 480 this.#updateAttribute(element, "isbroken", this.#isBrokenConnection); 481 this.#updateAttribute(element, "customroot", customRoot); 482 this.#updateAttribute(element, "httpsonlystatus", httpsOnlyStatus); 483 } 484 485 let { supplemental, owner, verifier } = this.#supplementalText(); 486 document.getElementById("identity-popup-content-supplemental").textContent = 487 supplemental; 488 document.getElementById("identity-popup-content-verifier").textContent = 489 verifier; 490 document.getElementById("identity-popup-content-owner").textContent = owner; 491 492 document 493 .getElementById("trustpanel-popup-multiView") 494 .showSubView("trustpanel-securityInformationView", event.target); 495 } 496 497 async #openBlockerSubview(event) { 498 document.l10n.setAttributes( 499 document.getElementById("trustpanel-blockerView"), 500 "trustpanel-blocker-header", 501 { host: this.#host } 502 ); 503 await this.#updateBlockerView(); 504 document 505 .getElementById("trustpanel-popup-multiView") 506 .showSubView("trustpanel-blockerView", event.target); 507 } 508 509 async #openBlockerDetailsSubview(event, blocker, blocking) { 510 let count = await blocker.getBlockerCount(); 511 let blockingKey = blocking ? "blocking" : "not-blocking"; 512 document.l10n.setAttributes( 513 document.getElementById("trustpanel-blockerDetailsView"), 514 blocker.l10nKeys.title[blockingKey] 515 ); 516 document.l10n.setAttributes( 517 document.getElementById("trustpanel-blocker-details-header"), 518 `trustpanel-${blocker.l10nKeys.general}-${blockingKey}-tab-header`, 519 { count } 520 ); 521 document.l10n.setAttributes( 522 document.getElementById("trustpanel-blocker-details-content"), 523 `protections-panel-${blocker.l10nKeys.content}` 524 ); 525 526 let listHeaderId; 527 if (blocker.l10nKeys.general == "fingerprinter") { 528 listHeaderId = "trustpanel-fingerprinter-list-header"; 529 } else if (blocker.l10nKeys.general == "cryptominer") { 530 listHeaderId = "trustpanel-cryptominer-tab-list-header"; 531 } else { 532 listHeaderId = "trustpanel-tracking-content-tab-list-header"; 533 } 534 535 document.l10n.setAttributes( 536 document.getElementById("trustpanel-blocker-details-list-header"), 537 listHeaderId 538 ); 539 540 let { items } = await blocker._generateSubViewListItems(); 541 document.getElementById("trustpanel-blocker-items").replaceChildren(items); 542 document 543 .getElementById("trustpanel-popup-multiView") 544 .showSubView("trustpanel-blockerDetailsView", event.target); 545 } 546 547 async #showClearCookiesSubview(event) { 548 document.l10n.setAttributes( 549 document.getElementById("trustpanel-clearcookiesView"), 550 "trustpanel-clear-cookies-header", 551 { host: window.gIdentityHandler.getHostForDisplay() } 552 ); 553 document 554 .getElementById("trustpanel-popup-multiView") 555 .showSubView("trustpanel-clearcookiesView", event.target); 556 } 557 558 async #addButtons(section, blockers, blocking) { 559 let sectionElement = document.getElementById(section); 560 561 if (!blockers.length) { 562 sectionElement.hidden = true; 563 return; 564 } 565 566 let children = blockers.map(async blocker => { 567 let button = document.createElement("moz-button"); 568 button.classList.add("moz-button-subviewbutton-nav"); 569 button.setAttribute("iconsrc", blocker.iconSrc); 570 button.setAttribute("type", "ghost icon"); 571 document.l10n.setAttributes( 572 button, 573 `trustpanel-list-label-${blocker.l10nKeys.general}`, 574 { count: await blocker.getBlockerCount() } 575 ); 576 button.addEventListener("click", event => 577 this.#openBlockerDetailsSubview(event, blocker, blocking) 578 ); 579 return button; 580 }); 581 582 sectionElement.hidden = false; 583 sectionElement 584 .querySelector(".trustpanel-blocker-buttons") 585 .replaceChildren(...(await Promise.all(children))); 586 } 587 588 get #trackingProtectionEnabled() { 589 return ( 590 !ContentBlockingAllowList.canHandle(window.gBrowser.selectedBrowser) || 591 !ContentBlockingAllowList.includes(window.gBrowser.selectedBrowser) 592 ); 593 } 594 595 #isSecurePage() { 596 return ( 597 this.#state & Ci.nsIWebProgressListener.STATE_IS_SECURE || 598 this.#isInternalSecurePage(this.#uri) || 599 this.#isPotentiallyTrustworthy 600 ); 601 } 602 603 #isInternalSecurePage(uri) { 604 if (uri && uri.schemeIs("about")) { 605 let module = E10SUtils.getAboutModule(uri); 606 if (module) { 607 let flags = module.getURIFlags(uri); 608 if (flags & Ci.nsIAboutModule.IS_SECURE_CHROME_UI) { 609 return true; 610 } 611 } 612 } 613 return false; 614 } 615 616 #clearSiteData() { 617 let baseDomain = SiteDataManager.getBaseDomainFromHost(this.#uri.host); 618 SiteDataManager.remove(baseDomain); 619 this.#hidePopup(); 620 } 621 622 #toggleTrackingProtection() { 623 if (this.#trackingProtectionEnabled) { 624 ContentBlockingAllowList.add(window.gBrowser.selectedBrowser); 625 } else { 626 ContentBlockingAllowList.remove(window.gBrowser.selectedBrowser); 627 } 628 629 PanelMultiView.hidePopup(this.#popup); 630 window.BrowserCommands.reload(); 631 } 632 633 #isHttpsOnlyModeActive(isWindowPrivate) { 634 return httpsOnlyModeEnabled || (isWindowPrivate && httpsOnlyModeEnabledPBM); 635 } 636 637 #isHttpsFirstModeActive(isWindowPrivate) { 638 return ( 639 !this.#isHttpsOnlyModeActive(isWindowPrivate) && 640 (httpsFirstModeEnabled || (isWindowPrivate && httpsFirstModeEnabledPBM)) 641 ); 642 } 643 #isSchemelessHttpsFirstModeActive(isWindowPrivate) { 644 return ( 645 !this.#isHttpsOnlyModeActive(isWindowPrivate) && 646 !this.#isHttpsFirstModeActive(isWindowPrivate) && 647 schemelessHttpsFirstModeEnabled 648 ); 649 } 650 /** 651 * Helper to parse out the important parts of _secInfo (of the SSL cert in 652 * particular) for use in constructing identity UI strings 653 */ 654 #getIdentityData() { 655 var result = {}; 656 var cert = this.#secInfo.serverCert; 657 658 // Human readable name of Subject 659 result.subjectOrg = cert.organization; 660 661 // SubjectName fields, broken up for individual access 662 if (cert.subjectName) { 663 result.subjectNameFields = {}; 664 cert.subjectName.split(",").forEach(function (v) { 665 var field = v.split("="); 666 this[field[0]] = field[1]; 667 }, result.subjectNameFields); 668 669 // Call out city, state, and country specifically 670 result.city = result.subjectNameFields.L; 671 result.state = result.subjectNameFields.ST; 672 result.country = result.subjectNameFields.C; 673 } 674 675 // Human readable name of Certificate Authority 676 result.caOrg = cert.issuerOrganization || cert.issuerCommonName; 677 result.cert = cert; 678 679 return result; 680 } 681 682 #getIsSecureContext() { 683 if (gBrowser.contentPrincipal?.originNoSuffix != "resource://pdf.js") { 684 return gBrowser.securityUI.isSecureContext; 685 } 686 687 // For PDF viewer pages (pdf.js) we can't rely on the isSecureContext field. 688 // The backend will return isSecureContext = true, because the content 689 // principal has a resource:// URI. Instead use the URI of the selected 690 // browser to perform the isPotentiallyTrustWorthy check. 691 692 let principal; 693 try { 694 principal = Services.scriptSecurityManager.createContentPrincipal( 695 gBrowser.selectedBrowser.documentURI, 696 {} 697 ); 698 return principal.isOriginPotentiallyTrustworthy; 699 } catch (error) { 700 console.error( 701 "Error while computing isPotentiallyTrustWorthy for pdf viewer page: ", 702 error 703 ); 704 return false; 705 } 706 } 707 708 /** 709 * Returns whether the issuer of the current certificate chain is 710 * built-in (returns false) or imported (returns true). 711 */ 712 #hasCustomRoot() { 713 return !this.#secInfo.isBuiltCertChainRootBuiltInRoot; 714 } 715 716 /** 717 * Whether the established HTTPS connection is considered "broken". 718 * This could have several reasons, such as mixed content or weak 719 * cryptography. If this is true, _isSecureConnection is false. 720 */ 721 get #isBrokenConnection() { 722 return this.#state & Ci.nsIWebProgressListener.STATE_IS_BROKEN; 723 } 724 725 /** 726 * Whether the connection to the current site was done via secure 727 * transport. Note that this attribute is not true in all cases that 728 * the site was accessed via HTTPS, i.e. _isSecureConnection will 729 * be false when _isBrokenConnection is true, even though the page 730 * was loaded over HTTPS. 731 */ 732 get #isSecureConnection() { 733 // If a <browser> is included within a chrome document, then this._state 734 // will refer to the security state for the <browser> and not the top level 735 // document. In this case, don't upgrade the security state in the UI 736 // with the secure state of the embedded <browser>. 737 return ( 738 !this.#isURILoadedFromFile && 739 this.#state & Ci.nsIWebProgressListener.STATE_IS_SECURE 740 ); 741 } 742 743 get #isEV() { 744 // If a <browser> is included within a chrome document, then this._state 745 // will refer to the security state for the <browser> and not the top level 746 // document. In this case, don't upgrade the security state in the UI 747 // with the EV state of the embedded <browser>. 748 return ( 749 !this.#isURILoadedFromFile && 750 this.#state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL 751 ); 752 } 753 754 get #isAssociatedIdentity() { 755 return this.#state & Ci.nsIWebProgressListener.STATE_IDENTITY_ASSOCIATED; 756 } 757 758 get #isMixedActiveContentLoaded() { 759 return ( 760 this.#state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT 761 ); 762 } 763 764 get #isMixedActiveContentBlocked() { 765 return ( 766 this.#state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT 767 ); 768 } 769 770 get #isMixedPassiveContentLoaded() { 771 return ( 772 this.#state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT 773 ); 774 } 775 776 get #isContentHttpsOnlyModeUpgraded() { 777 return ( 778 this.#state & Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADED 779 ); 780 } 781 782 get #isContentHttpsOnlyModeUpgradeFailed() { 783 return ( 784 this.#state & 785 Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED 786 ); 787 } 788 789 get #isContentHttpsFirstModeUpgraded() { 790 return ( 791 this.#state & 792 Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADED_FIRST 793 ); 794 } 795 796 get #isCertUserOverridden() { 797 return this.#state & Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN; 798 } 799 800 get #isCertErrorPage() { 801 let { documentURI } = gBrowser.selectedBrowser; 802 if (documentURI?.scheme != "about") { 803 return false; 804 } 805 806 return ( 807 documentURI.filePath == "certerror" || 808 (documentURI.filePath == "neterror" && 809 new URLSearchParams(documentURI.query).get("e") == "nssFailure2") 810 ); 811 } 812 813 get #isSecurelyConnectedAboutNetErrorPage() { 814 let { documentURI } = gBrowser.selectedBrowser; 815 if (documentURI?.scheme != "about" || documentURI.filePath != "neterror") { 816 return false; 817 } 818 819 let error = new URLSearchParams(documentURI.query).get("e"); 820 821 // Bug 1944993 - A list of neterrors without connection issues 822 return error === "httpErrorPage" || error === "serverError"; 823 } 824 825 get #isAboutNetErrorPage() { 826 let { documentURI } = gBrowser.selectedBrowser; 827 return documentURI?.scheme == "about" && documentURI.filePath == "neterror"; 828 } 829 830 get #isAboutHttpsOnlyErrorPage() { 831 let { documentURI } = gBrowser.selectedBrowser; 832 return ( 833 documentURI?.scheme == "about" && documentURI.filePath == "httpsonlyerror" 834 ); 835 } 836 837 get #isPotentiallyTrustworthy() { 838 return ( 839 !this.#isBrokenConnection && 840 (this.#isSecureContext || 841 gBrowser.selectedBrowser.documentURI?.scheme == "chrome") 842 ); 843 } 844 845 get #isAboutBlockedPage() { 846 let { documentURI } = gBrowser.selectedBrowser; 847 return documentURI?.scheme == "about" && documentURI.filePath == "blocked"; 848 } 849 850 get #isURILoadedFromFile() { 851 return this.#uri.schemeIs("file"); 852 } 853 854 #supplementalText() { 855 let supplemental = ""; 856 let verifier = ""; 857 let owner = ""; 858 859 // Fill in the CA name if we have a valid TLS certificate. 860 if (this.#isSecureConnection || this.#isCertUserOverridden) { 861 verifier = this.#tooltipText(); 862 } 863 864 // Fill in organization information if we have a valid EV certificate. 865 if (this.#isEV) { 866 let iData = this.#getIdentityData(); 867 owner = iData.subjectOrg; 868 verifier = this.#tooltipText(); 869 870 // Build an appropriate supplemental block out of whatever location data we have 871 if (iData.city) { 872 supplemental += iData.city + "\n"; 873 } 874 if (iData.state && iData.country) { 875 supplemental += gNavigatorBundle.getFormattedString( 876 "identity.identified.state_and_country", 877 [iData.state, iData.country] 878 ); 879 } else if (iData.state) { 880 // State only 881 supplemental += iData.state; 882 } else if (iData.country) { 883 // Country only 884 supplemental += iData.country; 885 } 886 } 887 return { supplemental, verifier, owner }; 888 } 889 890 #tooltipText() { 891 let tooltip = ""; 892 let warnTextOnInsecure = 893 insecureConnectionTextEnabled || 894 (insecureConnectionTextPBModeEnabled && 895 PrivateBrowsingUtils.isWindowPrivate(window)); 896 897 if (this.#uriHasHost && this.#isSecureConnection) { 898 // This is a secure connection. 899 if (!this._isCertUserOverridden) { 900 // It's a normal cert, verifier is the CA Org. 901 tooltip = gNavigatorBundle.getFormattedString( 902 "identity.identified.verifier", 903 [this.#getIdentityData().caOrg] 904 ); 905 } 906 } else if (this.#isBrokenConnection) { 907 if (this.#isMixedActiveContentLoaded) { 908 this._identityBox.classList.add("mixedActiveContent"); 909 if ( 910 UrlbarPrefs.getScotchBonnetPref("trimHttps") && 911 warnTextOnInsecure 912 ) { 913 tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip"); 914 } 915 } 916 } else if (!this.#isPotentiallyTrustworthy) { 917 tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip"); 918 } 919 920 if (this._isCertUserOverridden) { 921 // Cert is trusted because of a security exception, verifier is a special string. 922 tooltip = gNavigatorBundle.getString( 923 "identity.identified.verified_by_you" 924 ); 925 } 926 return tooltip; 927 } 928 929 #connectionState() { 930 // Determine connection security information. 931 let connection = "not-secure"; 932 if (this.#isSecureInternalUI) { 933 connection = "chrome"; 934 } else if (this.#pageExtensionPolicy) { 935 connection = "extension"; 936 } else if (this.#isURILoadedFromFile) { 937 connection = "file"; 938 } else if (this.#isEV) { 939 connection = "secure-ev"; 940 } else if (this.#isCertUserOverridden) { 941 connection = "secure-cert-user-overridden"; 942 } else if (this.#isSecureConnection) { 943 connection = "secure"; 944 } else if (this.#isCertErrorPage) { 945 connection = "cert-error-page"; 946 } else if (this.#isAboutHttpsOnlyErrorPage) { 947 connection = "https-only-error-page"; 948 } else if (this.#isAboutBlockedPage) { 949 connection = "not-secure"; 950 } else if (this.#isSecurelyConnectedAboutNetErrorPage) { 951 connection = "secure"; 952 } else if (this.#isAboutNetErrorPage) { 953 connection = "net-error-page"; 954 } else if (this.#isAssociatedIdentity) { 955 connection = "associated"; 956 } else if (this.#isPotentiallyTrustworthy) { 957 connection = "file"; 958 } 959 return connection; 960 } 961 962 #mixedContentState() { 963 let mixedcontent = []; 964 if (this.#isMixedPassiveContentLoaded) { 965 mixedcontent.push("passive-loaded"); 966 } 967 if (this.#isMixedActiveContentLoaded) { 968 mixedcontent.push("active-loaded"); 969 } else if (this.#isMixedActiveContentBlocked) { 970 mixedcontent.push("active-blocked"); 971 } 972 return mixedcontent; 973 } 974 975 #ciphersState() { 976 // We have no specific flags for weak ciphers (yet). If a connection is 977 // broken and we can't detect any mixed content loaded then it's a weak 978 // cipher. 979 if ( 980 this.#isBrokenConnection && 981 !this.#isMixedActiveContentLoaded && 982 !this.#isMixedPassiveContentLoaded 983 ) { 984 return "weak"; 985 } 986 return ""; 987 } 988 989 #httpsOnlyState() { 990 // If HTTPS-Only Mode is enabled, check the permission status 991 const privateBrowsingWindow = PrivateBrowsingUtils.isWindowPrivate(window); 992 const isHttpsOnlyModeActive = this.#isHttpsOnlyModeActive( 993 privateBrowsingWindow 994 ); 995 const isHttpsFirstModeActive = this.#isHttpsFirstModeActive( 996 privateBrowsingWindow 997 ); 998 const isSchemelessHttpsFirstModeActive = 999 this.#isSchemelessHttpsFirstModeActive(privateBrowsingWindow); 1000 1001 let httpsOnlyStatus = ""; 1002 1003 if ( 1004 isHttpsFirstModeActive || 1005 isHttpsOnlyModeActive || 1006 isSchemelessHttpsFirstModeActive 1007 ) { 1008 // Note: value and permission association is laid out 1009 // in _getHttpsOnlyPermission 1010 let value = this.#getHttpsOnlyPermission(); 1011 1012 // We do not want to display the exception ui for schemeless 1013 // HTTPS-First, but we still want the "Upgraded to HTTPS" label. 1014 document.getElementById( 1015 "trustpanel-popup-security-httpsonlymode" 1016 ).hidden = isSchemelessHttpsFirstModeActive; 1017 1018 document.getElementById( 1019 "trustpanel-popup-security-menulist-off-item" 1020 ).hidden = privateBrowsingWindow && value != 1; 1021 document.getElementById( 1022 "trustpanel-popup-security-httpsonlymode-menulist" 1023 ).value = value; 1024 1025 if (value > 0) { 1026 httpsOnlyStatus = "exception"; 1027 } else if ( 1028 this.#isAboutHttpsOnlyErrorPage || 1029 (isHttpsFirstModeActive && this.#isContentHttpsOnlyModeUpgradeFailed) 1030 ) { 1031 httpsOnlyStatus = "failed-top"; 1032 } else if (this.#isContentHttpsOnlyModeUpgradeFailed) { 1033 httpsOnlyStatus = "failed-sub"; 1034 } else if ( 1035 this.#isContentHttpsOnlyModeUpgraded || 1036 this.#isContentHttpsFirstModeUpgraded 1037 ) { 1038 httpsOnlyStatus = "upgraded"; 1039 } 1040 } 1041 return httpsOnlyStatus; 1042 } 1043 1044 /** 1045 * Gets the current HTTPS-Only mode permission for the current page. 1046 * Values are the same as in #identity-popup-security-httpsonlymode-menulist, 1047 * -1 indicates a incompatible scheme on the current URI. 1048 */ 1049 #getHttpsOnlyPermission() { 1050 let uri = gBrowser.currentURI; 1051 if (uri instanceof Ci.nsINestedURI) { 1052 uri = uri.QueryInterface(Ci.nsINestedURI).innermostURI; 1053 } 1054 if (!uri.schemeIs("http") && !uri.schemeIs("https")) { 1055 return -1; 1056 } 1057 uri = uri.mutate().setScheme("http").finalize(); 1058 const principal = Services.scriptSecurityManager.createContentPrincipal( 1059 uri, 1060 gBrowser.contentPrincipal.originAttributes 1061 ); 1062 const { state } = SitePermissions.getForPrincipal( 1063 principal, 1064 "https-only-load-insecure" 1065 ); 1066 switch (state) { 1067 case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION: 1068 return 2; // Off temporarily 1069 case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW: 1070 return 1; // Off 1071 default: 1072 return 0; // On 1073 } 1074 } 1075 1076 /** 1077 * Sets/removes HTTPS-Only Mode exception and possibly reloads the page. 1078 */ 1079 #changeHttpsOnlyPermission() { 1080 // Get the new value from the menulist and the current value 1081 // Note: value and permission association is laid out 1082 // in _getHttpsOnlyPermission 1083 const oldValue = this.#getHttpsOnlyPermission(); 1084 if (oldValue < 0) { 1085 console.error( 1086 "Did not update HTTPS-Only permission since scheme is incompatible" 1087 ); 1088 return; 1089 } 1090 1091 let menulist = document.getElementById( 1092 "trustpanel-popup-security-httpsonlymode-menulist" 1093 ); 1094 let newValue = parseInt(menulist.selectedItem.value, 10); 1095 1096 // If nothing changed, just return here 1097 if (newValue === oldValue) { 1098 return; 1099 } 1100 1101 // We always want to set the exception for the HTTP version of the current URI, 1102 // since when we check wether we should upgrade a request, we are checking permissons 1103 // for the HTTP principal (Bug 1757297). 1104 let newURI = gBrowser.currentURI; 1105 if (newURI instanceof Ci.nsINestedURI) { 1106 newURI = newURI.QueryInterface(Ci.nsINestedURI).innermostURI; 1107 } 1108 newURI = newURI.mutate().setScheme("http").finalize(); 1109 const principal = Services.scriptSecurityManager.createContentPrincipal( 1110 newURI, 1111 gBrowser.contentPrincipal.originAttributes 1112 ); 1113 1114 // Set or remove the permission 1115 if (newValue === 0) { 1116 SitePermissions.removeFromPrincipal( 1117 principal, 1118 "https-only-load-insecure" 1119 ); 1120 } else if (newValue === 1) { 1121 SitePermissions.setForPrincipal( 1122 principal, 1123 "https-only-load-insecure", 1124 Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW, 1125 SitePermissions.SCOPE_PERSISTENT 1126 ); 1127 } else { 1128 SitePermissions.setForPrincipal( 1129 principal, 1130 "https-only-load-insecure", 1131 Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION, 1132 SitePermissions.SCOPE_SESSION 1133 ); 1134 } 1135 1136 // If we're on the error-page, we have to redirect the user 1137 // from HTTPS to HTTP. Otherwise we can just reload the page. 1138 if (this.#isAboutHttpsOnlyErrorPage) { 1139 gBrowser.loadURI(newURI, { 1140 triggeringPrincipal: 1141 Services.scriptSecurityManager.getSystemPrincipal(), 1142 loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, 1143 }); 1144 PanelMultiView.hidePopup(this.#popup); 1145 return; 1146 } 1147 // The page only needs to reload if we switch between allow and block 1148 // Because "off" is 1 and "off temporarily" is 2, we can just check if the 1149 // sum of newValue and oldValue is 3. 1150 if (newValue + oldValue !== 3) { 1151 BrowserCommands.reloadSkipCache(); 1152 PanelMultiView.hidePopup(this.#popup); 1153 // Ensure the browser is focused again, otherwise we may not trigger the 1154 // security delay on a potential error page following this reload. 1155 gBrowser.selectedBrowser.focus(); 1156 } 1157 } 1158 1159 /** 1160 * Adds the toggles into the smartblock toggle container. Clears existing toggles first, then 1161 * searches through the contentBlockingLog for smartblock-compatible content. 1162 * 1163 * @returns {boolean} true if a smartblock compatible resource is blocked or shimmed, false otherwise 1164 */ 1165 #addSmartblockEmbedToggles() { 1166 if (!smartblockEmbedsEnabledPref) { 1167 // Do not insert toggles if feature is disabled. 1168 return false; 1169 } 1170 1171 let container = document.getElementById( 1172 "trustpanel-smartblock-toggle-container" 1173 ); 1174 container.replaceChildren(); 1175 1176 // check that there is an allowed or replaced flag present 1177 let contentBlockingEvents = 1178 gBrowser.selectedBrowser.getContentBlockingEvents(); 1179 1180 // In the future, we should add a flag specifically for smartblock embeds so that 1181 // these checks do not trigger when a non-embed-related shim is shimming 1182 // a smartblock compatible site, see Bug 1926461 1183 let somethingAllowedOrReplaced = 1184 contentBlockingEvents & 1185 Ci.nsIWebProgressListener.STATE_ALLOWED_TRACKING_CONTENT || 1186 contentBlockingEvents & 1187 Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT; 1188 1189 if (!somethingAllowedOrReplaced) { 1190 // return early if there is no content that is allowed or replaced 1191 return false; 1192 } 1193 1194 let blocked = this.#fetchSmartBlocked(); 1195 if (!blocked.length) { 1196 return false; 1197 } 1198 1199 // search through content log for compatible blocked origins 1200 for (let { shimAllowed, shimInfo } of blocked) { 1201 const { shimId, displayName } = shimInfo; 1202 1203 // check that a toggle doesn't already exist 1204 let existingToggle = document.getElementById( 1205 `trustpanel-smartblock-${shimId.toLowerCase()}-toggle` 1206 ); 1207 if (existingToggle) { 1208 // make sure toggle state is allowed if ANY of the sites are allowed 1209 if (shimAllowed) { 1210 existingToggle.setAttribute("pressed", true); 1211 } 1212 // skip adding a new toggle 1213 continue; 1214 } 1215 1216 // create the toggle element 1217 let toggle = document.createElement("moz-toggle"); 1218 toggle.setAttribute( 1219 "id", 1220 `trustpanel-smartblock-${shimId.toLowerCase()}-toggle` 1221 ); 1222 toggle.setAttribute("data-l10n-attrs", "label"); 1223 document.l10n.setAttributes( 1224 toggle, 1225 "protections-panel-smartblock-blocking-toggle", 1226 { 1227 trackername: displayName, 1228 } 1229 ); 1230 1231 // set toggle to correct position 1232 toggle.toggleAttribute("pressed", !!shimAllowed); 1233 1234 // add functionality to toggle 1235 toggle.addEventListener("toggle", event => { 1236 if (event.target.pressed) { 1237 this.#sendUnblockMessageToSmartblock(shimId); 1238 } else { 1239 this.#sendReblockMessageToSmartblock(shimId); 1240 } 1241 PanelMultiView.hidePopup(this.#popup); 1242 }); 1243 1244 container.insertAdjacentElement("beforeend", toggle); 1245 } 1246 return true; 1247 } 1248 1249 #fetchSmartBlocked() { 1250 let blocked = []; 1251 let contentBlockingLog = JSON.parse( 1252 gBrowser.selectedBrowser.getContentBlockingLog() 1253 ); 1254 // search through content log for compatible blocked origins 1255 for (let [origin, actions] of Object.entries(contentBlockingLog)) { 1256 let shimAllowed = actions.some( 1257 ([flag]) => 1258 (flag & Ci.nsIWebProgressListener.STATE_ALLOWED_TRACKING_CONTENT) != 0 1259 ); 1260 1261 let shimDetected = actions.some( 1262 ([flag]) => 1263 (flag & Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT) != 1264 0 1265 ); 1266 1267 if (!shimAllowed && !shimDetected) { 1268 // origin is not being shimmed or allowed 1269 continue; 1270 } 1271 1272 let shimInfo = SMARTBLOCK_EMBED_INFO.find(element => { 1273 let matchPatternSet = new MatchPatternSet(element.matchPatterns); 1274 return matchPatternSet.matches(origin); 1275 }); 1276 if (!shimInfo) { 1277 // origin not relevant to smartblock 1278 continue; 1279 } 1280 1281 blocked.push({ shimAllowed, shimInfo }); 1282 } 1283 return blocked; 1284 } 1285 1286 async observe(subject, topic) { 1287 switch (topic) { 1288 case "smartblock:open-protections-panel": { 1289 if (gBrowser.selectedBrowser.browserId !== subject.browserId) { 1290 break; 1291 } 1292 this.#initializePopup(); 1293 let multiview = document.getElementById("trustpanel-popup-multiView"); 1294 // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1999928 1295 // This currently opens as a standalone panel, we would like to open 1296 // the panel with a back button and title the same way as if it 1297 // were accessed via the urlbar icon. 1298 let initialMainViewId = multiview.getAttribute("mainViewId"); 1299 this.#popup.addEventListener( 1300 "popuphidden", 1301 () => { 1302 multiview.setAttribute("mainViewId", initialMainViewId); 1303 }, 1304 { once: true } 1305 ); 1306 multiview.setAttribute("mainViewId", "trustpanel-blockerView"); 1307 this.showPopup({ reason: "embedPlaceholderButton" }); 1308 break; 1309 } 1310 } 1311 } 1312 1313 // We handle focus here when the panel is shown. 1314 handleEvent(event) { 1315 switch (event.type) { 1316 case "popupshown": 1317 this.onPopupShown(event); 1318 break; 1319 } 1320 } 1321 1322 onPopupShown() { 1323 // Disable the toggles for a short time after opening via SmartBlock placeholder button 1324 // to prevent clickjacking. 1325 if (this.#openingReason == "embedPlaceholderButton") { 1326 this.#disablePopupToggles(); 1327 this.#popupToggleDelayTimer = setTimeout(() => { 1328 this.#enablePopupToggles(); 1329 }, popupClickjackDelay); 1330 } 1331 } 1332 1333 /** 1334 * Sends a message to webcompat extension to unblock content and remove placeholders 1335 * 1336 * @param {string} shimId - the id of the shim blocking the content 1337 */ 1338 #sendUnblockMessageToSmartblock(shimId) { 1339 Services.obs.notifyObservers( 1340 gBrowser.selectedTab, 1341 "smartblock:unblock-embed", 1342 shimId 1343 ); 1344 } 1345 1346 /** 1347 * Sends a message to webcompat extension to reblock content 1348 * 1349 * @param {string} shimId - the id of the shim blocking the content 1350 */ 1351 #sendReblockMessageToSmartblock(shimId) { 1352 Services.obs.notifyObservers( 1353 gBrowser.selectedTab, 1354 "smartblock:reblock-embed", 1355 shimId 1356 ); 1357 } 1358 1359 #resetToggleSecDelay() { 1360 clearTimeout(this.#popupToggleDelayTimer); 1361 this.#popupToggleDelayTimer = setTimeout(() => { 1362 this.#enablePopupToggles(); 1363 }, popupClickjackDelay); 1364 } 1365 1366 #disablePopupToggles() { 1367 // Disables all toggles in the protections panel 1368 this.#popup.querySelectorAll("moz-toggle").forEach(toggle => { 1369 toggle.setAttribute("disabled", true); 1370 toggle.addEventListener("pointerdown", this.#resetToggleReference); 1371 }); 1372 } 1373 1374 #resetToggleReference = this.#resetToggleSecDelay.bind(this); 1375 #enablePopupToggles() { 1376 // Enables all toggles in the protections panel 1377 this.#popup.querySelectorAll("moz-toggle").forEach(toggle => { 1378 toggle.removeAttribute("disabled"); 1379 toggle.removeEventListener("pointerdown", this.#resetToggleReference); 1380 }); 1381 } 1382 1383 #updateAttribute(elem, attr, value) { 1384 if (value) { 1385 elem.setAttribute(attr, value); 1386 } else { 1387 elem.removeAttribute(attr); 1388 } 1389 } 1390 } 1391 1392 var gTrustPanelHandler = new TrustPanel();