/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* import-globals-from browser-siteProtections.js */ ChromeUtils.defineESModuleGetters(this, { BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", ContentBlockingAllowList: "resource://gre/modules/ContentBlockingAllowList.sys.mjs", E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", PanelMultiView: "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", QWACs: "resource://gre/modules/psm/QWACs.sys.mjs", SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs", UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", }); XPCOMUtils.defineLazyPreferenceGetter( this, "insecureConnectionTextEnabled", "security.insecure_connection_text.enabled" ); XPCOMUtils.defineLazyPreferenceGetter( this, "insecureConnectionTextPBModeEnabled", "security.insecure_connection_text.pbmode.enabled" ); XPCOMUtils.defineLazyPreferenceGetter( this, "httpsOnlyModeEnabled", "dom.security.https_only_mode" ); XPCOMUtils.defineLazyPreferenceGetter( this, "httpsFirstModeEnabled", "dom.security.https_first" ); XPCOMUtils.defineLazyPreferenceGetter( this, "schemelessHttpsFirstModeEnabled", "dom.security.https_first_schemeless" ); XPCOMUtils.defineLazyPreferenceGetter( this, "httpsFirstModeEnabledPBM", "dom.security.https_first_pbm" ); XPCOMUtils.defineLazyPreferenceGetter( this, "httpsOnlyModeEnabledPBM", "dom.security.https_only_mode_pbm" ); XPCOMUtils.defineLazyPreferenceGetter( this, "popupClickjackDelay", "security.notification_enable_delay", 500 ); XPCOMUtils.defineLazyPreferenceGetter( this, "smartblockEmbedsEnabledPref", "extensions.webcompat.smartblockEmbeds.enabled", false ); const ETP_ENABLED_ASSETS = { label: "trustpanel-etp-label-enabled", description: "trustpanel-etp-description-enabled", header: "trustpanel-header-enabled", innerDescription: "trustpanel-description-enabled2", }; const ETP_DISABLED_ASSETS = { label: "trustpanel-etp-label-disabled", description: "trustpanel-etp-description-disabled", header: "trustpanel-header-disabled", innerDescription: "trustpanel-description-disabled", }; const SMARTBLOCK_EMBED_INFO = [ { matchPatterns: ["https://itisatracker.org/*"], shimId: "EmbedTestShim", displayName: "Test", }, { matchPatterns: [ "https://www.instagram.com/*", "https://platform.instagram.com/*", ], shimId: "InstagramEmbed", displayName: "Instagram", }, { matchPatterns: ["https://www.tiktok.com/*"], shimId: "TiktokEmbed", displayName: "Tiktok", }, { matchPatterns: ["https://platform.twitter.com/*"], shimId: "TwitterEmbed", displayName: "X", }, { matchPatterns: ["https://*.disqus.com/*"], shimId: "DisqusEmbed", displayName: "Disqus", }, ]; class TrustPanel { #state = null; #secInfo = null; /** * If the document is using a qualified website authentication certificate * (QWAC), this may eventually be an nsIX509Cert corresponding to it. */ #qwac = null; /** * Promise that will resolve when determining if the document is using a QWAC * has resolved. */ #qwacStatusPromise = null; #host = null; #uri = null; #uriHasHost = null; #pageExtensionPolicy = null; #lastEvent = null; #popupToggleDelayTimer = null; #openingReason = null; #blockers = { SocialTracking, ThirdPartyCookies, TrackingProtection, Fingerprinting, Cryptomining, }; init() { for (let blocker of Object.values(this.#blockers)) { if (blocker.init) { blocker.init(); } } // Add an observer to listen to requests to open the protections panel Services.obs.addObserver(this, "smartblock:open-protections-panel"); } uninit() { for (let blocker of Object.values(this.#blockers)) { if (blocker.uninit) { blocker.uninit(); } } Services.obs.removeObserver(this, "smartblock:open-protections-panel"); } get #popup() { return document.getElementById("trustpanel-popup"); } get #enabled() { return UrlbarPrefs.get("trustPanel.featureGate"); } handleProtectionsButtonEvent(event) { event.stopPropagation(); if ( (event.type == "click" && event.button != 0) || (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE && event.keyCode != KeyEvent.DOM_VK_RETURN) ) { return; // Left click, space or enter only } this.showPopup({ event, openingReason: "shieldButtonClicked" }); } async onContentBlockingEvent( event, _webProgress, _isSimulated, _previousState ) { // Only accept contentblocking events for uris that we initialised `updateIdentity` // with, this can go wrong if trustpanel is enabled mid page load. if (!this.#enabled || !this.#uri) { return; } // First update all our internal state based on the allowlist and the // different blockers: this.anyDetected = false; this.#lastEvent = event; // Check whether the user has added an exception for this site. this.hasException = ContentBlockingAllowList.canHandle(window.gBrowser.selectedBrowser) && ContentBlockingAllowList.includes(window.gBrowser.selectedBrowser); // Update blocker state and find if they detected or blocked anything. for (let blocker of Object.values(this.#blockers)) { // Store data on whether the blocker is activated for reporting it // using the "report breakage" dialog. Under normal circumstances this // dialog should only be able to open in the currently selected tab // and onSecurityChange runs on tab switch, so we can avoid associating // the data with the document directly. blocker.activated = blocker.isBlocking(event); this.anyDetected = this.anyDetected || blocker.isDetected(event); } if (this.#popup) { await this.#updatePopup(); } } #initializePopup() { if (!this.#popup) { let wrapper = document.getElementById("template-trustpanel-popup"); wrapper.replaceWith(wrapper.content); document .getElementById("trustpanel-popup-connection") .addEventListener("click", event => this.#openSecurityInformationSubview(event) ); document .getElementById("trustpanel-blocker-see-all") .addEventListener("click", event => this.#openBlockerSubview(event)); document .getElementById("trustpanel-privacy-link") .addEventListener("click", () => { this.#hidePopup(); window.openTrustedLinkIn("about:preferences#privacy", "tab"); }); document .getElementById("trustpanel-clear-cookies-button") .addEventListener("command", event => this.#showClearCookiesSubview(event) ); document .getElementById("trustpanel-siteinformation-morelink") .addEventListener("click", () => this.#showSecurityPopup()); document .getElementById("trustpanel-clear-cookie-cancel") .addEventListener("click", () => this.#hidePopup()); document .getElementById("trustpanel-clear-cookie-clear") .addEventListener("click", () => this.#clearSiteData()); document .getElementById("trustpanel-toggle") .addEventListener("click", () => this.#toggleTrackingProtection()); document .getElementById("identity-popup-remove-cert-exception") .addEventListener("click", () => this.#removeCertException()); document .getElementById("trustpanel-popup-security-httpsonlymode-menulist") .addEventListener("command", () => this.#changeHttpsOnlyPermission()); this.#popup.addEventListener("popupshown", this); } } async showPopup(opts = {}) { this.#initializePopup(); // Kick off background determination of QWAC status. if (this.#isSecureContext && !this.#qwacStatusPromise) { let qwacStatusPromise = QWACs.determineQWACStatus( this.#secInfo, this.#uri, gBrowser.selectedBrowser.browsingContext ).then(result => { // Check that when this promise resolves, we're still on the same // document as when it was created. if (qwacStatusPromise == this.#qwacStatusPromise && result) { this.#qwac = result; this.#updateSecurityInformationSubview(); } }); this.#qwacStatusPromise = qwacStatusPromise; } await this.#updatePopup(); this.#openingReason = opts.reason; PanelMultiView.openPopup(this.#popup, this.#anchor(), { position: "bottomleft topleft", }); } async #hidePopup() { let hidden = new Promise(c => { this.#popup.addEventListener("popuphidden", c, { once: true }); }); PanelMultiView.hidePopup(this.#popup); await hidden; } updateIdentity(state, uri) { if (!this.#enabled) { return; } try { // Account for file: urls and catch when "" is the value this.#uriHasHost = !!uri.host; } catch (ex) { this.#uriHasHost = false; } this.#state = state; this.#uri = uri; this.#secInfo = gBrowser.securityUI.secInfo; // Clear any previously-determined QWAC information. this.#qwac = null; this.#qwacStatusPromise = null; this.#pageExtensionPolicy = WebExtensionPolicy.getByURI(uri); this.#updateUrlbarIcon(); } /** * The trust icon may be hidden, in that case the identity box * should be shown so use that as anchor. * * @returns {DOMElement} */ #anchor() { let anchors = [ document.getElementById("trust-icon-container"), document.getElementById("identity-icon-box"), ]; return anchors.find(element => element.checkVisibility()); } #updateUrlbarIcon() { let icon = document.getElementById("trust-icon-container"); icon.className = this.#isSecurePage() ? "secure" : "insecure"; if (this.#isURILoadedFromFile) { icon.classList.add("file"); } if (!this.#trackingProtectionEnabled) { icon.classList.add("inactive"); } icon.setAttribute("tooltiptext", this.#tooltipText()); icon.classList.toggle("chickletShown", this.#isInternalSecurePage); } async #updatePopup() { this.#host = BrowserUtils.formatURIForDisplay(this.#uri, { onlyBaseDomain: true, }); this.#popup.setAttribute("connection", this.#connectionState()); this.#popup.setAttribute( "tracking-protection", this.#trackingProtectionStatus() ); await this.#updateMainView(); } async #updateMainView() { let assets = this.#trackingProtectionEnabled ? ETP_ENABLED_ASSETS : ETP_DISABLED_ASSETS; if (this.#uri) { let favicon = await PlacesUtils.favicons.getFaviconForPage(this.#uri); document.getElementById("trustpanel-popup-icon").src = favicon?.uri.spec ?? ""; } let toggle = document.getElementById("trustpanel-toggle"); toggle.toggleAttribute("pressed", this.#trackingProtectionEnabled); document.l10n.setAttributes( toggle, this.#trackingProtectionEnabled ? "trustpanel-etp-toggle-on" : "trustpanel-etp-toggle-off", { host: this.#host } ); let hostElement = document.getElementById("trustpanel-popup-host"); hostElement.setAttribute("value", this.#host); hostElement.setAttribute("tooltiptext", this.#host); document.l10n.setAttributes( document.getElementById("trustpanel-etp-label"), assets.label ); document.l10n.setAttributes( document.getElementById("trustpanel-etp-description"), assets.description ); document.l10n.setAttributes( document.getElementById("trustpanel-header"), assets.header ); document.l10n.setAttributes( document.getElementById("trustpanel-description"), assets.innerDescription ); document.l10n.setAttributes( document.getElementById("trustpanel-connection-label"), this.#connectionLabel() ); this.#updateAttribute( document.getElementById("trustpanel-blocker-section"), "hidden", !this.anyDetected ); this.#updateAttribute( document.getElementById("trustpanel-toggle-section"), "disabled", !ContentBlockingAllowList.canHandle(window.gBrowser.selectedBrowser) ); let hasSiteData = false; try { let baseDomain = SiteDataManager.getBaseDomainFromHost(this.#uri.host); hasSiteData = await SiteDataManager.hasSiteData(baseDomain); } catch (e) {} this.#updateAttribute( document.getElementById("trustpanel-clear-cookies-button"), "disabled", !hasSiteData ); this.#updateAttribute( document.getElementById("trustpanel-toggle"), "disabled", !ContentBlockingAllowList.canHandle(window.gBrowser.selectedBrowser) ); await this.#updateBlockerView(); } async #updateBlockerView() { let count = this.#fetchSmartBlocked().length; let blocked = []; let detected = []; for (let blocker of Object.values(this.#blockers)) { if (blocker.isBlocking(this.#lastEvent)) { blocked.push(blocker); count += await blocker.getBlockerCount(); } else if (blocker.isDetected(this.#lastEvent)) { detected.push(blocker); } } this.#addButtons("trustpanel-blocked", blocked, true); this.#addButtons("trustpanel-detected", detected, false); document .getElementById("trustpanel-smartblock-section") .toggleAttribute("hidden", !this.#addSmartblockEmbedToggles()); // This element is in the main view but updated in case // any content blocking events were missed. document.l10n.setArgs( document.getElementById("trustpanel-blocker-section-header"), { count } ); } async #showSecurityPopup() { await this.#hidePopup(); window.BrowserCommands.pageInfo(null, "securityTab"); } #removeCertException() { let overrideService = Cc["@mozilla.org/security/certoverride;1"].getService( Ci.nsICertOverrideService ); overrideService.clearValidityOverride( this.#uri.host, this.#uri.port > 0 ? this.#uri.port : 443, gBrowser.contentPrincipal.originAttributes ); BrowserCommands.reloadSkipCache(); PanelMultiView.hidePopup(this.#popup); } #trackingProtectionStatus() { if (!this.#isSecurePage()) { return "warning"; } return this.#trackingProtectionEnabled ? "enabled" : "disabled"; } #updateSecurityInformationSubview() { document.l10n.setAttributes( document.getElementById("trustpanel-securityInformationView"), "trustpanel-site-information-header", { host: this.#host } ); let customRoot = this.#isSecureConnection ? this.#hasCustomRoot() : false; let connection = this.#connectionState(); let mixedcontent = this.#mixedContentState(); let ciphers = this.#ciphersState(); let httpsOnlyStatus = this.#httpsOnlyState(); // Update all elements. let elementIDs = [ "trustpanel-popup", "identity-popup-securityView-extended-info", ]; for (let id of elementIDs) { let element = document.getElementById(id); this.#updateAttribute(element, "connection", connection); this.#updateAttribute(element, "ciphers", ciphers); this.#updateAttribute(element, "mixedcontent", mixedcontent); this.#updateAttribute(element, "isbroken", this.#isBrokenConnection); this.#updateAttribute(element, "customroot", customRoot); this.#updateAttribute(element, "httpsonlystatus", httpsOnlyStatus); } let { supplemental, owner, verifier } = this.#supplementalText(); document.getElementById("identity-popup-content-supplemental").textContent = supplemental; document.getElementById("identity-popup-content-verifier").textContent = verifier; document.getElementById("identity-popup-content-owner").textContent = owner; } #openSecurityInformationSubview(event) { this.#updateSecurityInformationSubview(); document .getElementById("trustpanel-popup-multiView") .showSubView("trustpanel-securityInformationView", event.target); } async #openBlockerSubview(event) { document.l10n.setAttributes( document.getElementById("trustpanel-blockerView"), "trustpanel-blocker-header", { host: this.#host } ); await this.#updateBlockerView(); document .getElementById("trustpanel-popup-multiView") .showSubView("trustpanel-blockerView", event.target); } async #openBlockerDetailsSubview(event, blocker, blocking) { let count = await blocker.getBlockerCount(); let blockingKey = blocking ? "blocking" : "not-blocking"; document.l10n.setAttributes( document.getElementById("trustpanel-blockerDetailsView"), blocker.l10nKeys.title[blockingKey] ); document.l10n.setAttributes( document.getElementById("trustpanel-blocker-details-header"), `trustpanel-${blocker.l10nKeys.general}-${blockingKey}-tab-header`, { count } ); document.l10n.setAttributes( document.getElementById("trustpanel-blocker-details-content"), `protections-panel-${blocker.l10nKeys.content}` ); let listHeaderId; if (blocker.l10nKeys.general == "fingerprinter") { listHeaderId = "trustpanel-fingerprinter-list-header"; } else if (blocker.l10nKeys.general == "cryptominer") { listHeaderId = "trustpanel-cryptominer-tab-list-header"; } else { listHeaderId = "trustpanel-tracking-content-tab-list-header"; } document.l10n.setAttributes( document.getElementById("trustpanel-blocker-details-list-header"), listHeaderId ); let { items } = await blocker._generateSubViewListItems(); document.getElementById("trustpanel-blocker-items").replaceChildren(items); document .getElementById("trustpanel-popup-multiView") .showSubView("trustpanel-blockerDetailsView", event.target); } async #showClearCookiesSubview(event) { document.l10n.setAttributes( document.getElementById("trustpanel-clearcookiesView"), "trustpanel-clear-cookies-header", { host: this.#host } ); document .getElementById("trustpanel-popup-multiView") .showSubView("trustpanel-clearcookiesView", event.target); } async #addButtons(section, blockers, blocking) { let sectionElement = document.getElementById(section); if (!blockers.length) { sectionElement.hidden = true; return; } let children = blockers.map(async blocker => { let button = document.createElement("moz-button"); button.classList.add("moz-button-subviewbutton-nav"); button.setAttribute("iconsrc", blocker.iconSrc); button.setAttribute("type", "ghost icon"); document.l10n.setAttributes( button, `trustpanel-list-label-${blocker.l10nKeys.general}`, { count: await blocker.getBlockerCount() } ); button.addEventListener("click", event => this.#openBlockerDetailsSubview(event, blocker, blocking) ); return button; }); sectionElement.hidden = false; sectionElement .querySelector(".trustpanel-blocker-buttons") .replaceChildren(...(await Promise.all(children))); } get #trackingProtectionEnabled() { return ( !ContentBlockingAllowList.canHandle(window.gBrowser.selectedBrowser) || !ContentBlockingAllowList.includes(window.gBrowser.selectedBrowser) ); } #isSecurePage() { if (this.#isInternalSecurePage) { return true; } if (this.#isCertErrorPage || this.#isCertUserOverridden) { return false; } if (this.#isSecureConnection) { return true; } if (this.#isBrokenConnection) { return false; } if (this.#isPotentiallyTrustworthy) { return true; } return false; } get #isInternalSecurePage() { if (this.#uri?.schemeIs("about")) { let module = E10SUtils.getAboutModule(this.#uri); if (module) { let flags = module.getURIFlags(this.#uri); if (flags & Ci.nsIAboutModule.IS_SECURE_CHROME_UI) { return true; } } } return false; } #clearSiteData() { let baseDomain = SiteDataManager.getBaseDomainFromHost(this.#uri.host); SiteDataManager.remove(baseDomain); this.#hidePopup(); } #toggleTrackingProtection() { if (this.#trackingProtectionEnabled) { ContentBlockingAllowList.add(window.gBrowser.selectedBrowser); } else { ContentBlockingAllowList.remove(window.gBrowser.selectedBrowser); } PanelMultiView.hidePopup(this.#popup); window.BrowserCommands.reload(); } #isHttpsOnlyModeActive(isWindowPrivate) { return httpsOnlyModeEnabled || (isWindowPrivate && httpsOnlyModeEnabledPBM); } #isHttpsFirstModeActive(isWindowPrivate) { return ( !this.#isHttpsOnlyModeActive(isWindowPrivate) && (httpsFirstModeEnabled || (isWindowPrivate && httpsFirstModeEnabledPBM)) ); } #isSchemelessHttpsFirstModeActive(isWindowPrivate) { return ( !this.#isHttpsOnlyModeActive(isWindowPrivate) && !this.#isHttpsFirstModeActive(isWindowPrivate) && schemelessHttpsFirstModeEnabled ); } /** * Helper to parse out the important parts of _secInfo (of the SSL cert in * particular) for use in constructing identity UI strings */ #getIdentityData(cert = this.#secInfo.serverCert) { var result = {}; // Human readable name of Subject result.subjectOrg = cert.organization; // SubjectName fields, broken up for individual access if (cert.subjectName) { result.subjectNameFields = {}; cert.subjectName.split(",").forEach(function (v) { var field = v.split("="); this[field[0]] = field[1]; }, result.subjectNameFields); // Call out city, state, and country specifically result.city = result.subjectNameFields.L; result.state = result.subjectNameFields.ST; result.country = result.subjectNameFields.C; } // Human readable name of Certificate Authority result.caOrg = cert.issuerOrganization || cert.issuerCommonName; result.cert = cert; return result; } get #isSecureContext() { if (gBrowser.contentPrincipal?.originNoSuffix != "resource://pdf.js") { return gBrowser.securityUI.isSecureContext; } // For PDF viewer pages (pdf.js) we can't rely on the isSecureContext field. // The backend will return isSecureContext = true, because the content // principal has a resource:// URI. Instead use the URI of the selected // browser to perform the isPotentiallyTrustWorthy check. let principal; try { principal = Services.scriptSecurityManager.createContentPrincipal( gBrowser.selectedBrowser.documentURI, {} ); return principal.isOriginPotentiallyTrustworthy; } catch (error) { console.error( "Error while computing isPotentiallyTrustWorthy for pdf viewer page: ", error ); return false; } } /** * Returns whether the issuer of the current certificate chain is * built-in (returns false) or imported (returns true). */ #hasCustomRoot() { return !this.#secInfo.isBuiltCertChainRootBuiltInRoot; } /** * Whether the established HTTPS connection is considered "broken". * This could have several reasons, such as mixed content or weak * cryptography. If this is true, _isSecureConnection is false. */ get #isBrokenConnection() { return this.#state & Ci.nsIWebProgressListener.STATE_IS_BROKEN; } /** * Whether the connection to the current site was done via secure * transport. Note that this attribute is not true in all cases that * the site was accessed via HTTPS, i.e. _isSecureConnection will * be false when _isBrokenConnection is true, even though the page * was loaded over HTTPS. */ get #isSecureConnection() { // If a is included within a chrome document, then this._state // will refer to the security state for the and not the top level // document. In this case, don't upgrade the security state in the UI // with the secure state of the embedded . return ( !this.#isURILoadedFromFile && this.#state & Ci.nsIWebProgressListener.STATE_IS_SECURE ); } get #isEV() { // If a is included within a chrome document, then this._state // will refer to the security state for the and not the top level // document. In this case, don't upgrade the security state in the UI // with the EV state of the embedded . return ( !this.#isURILoadedFromFile && this.#state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL ); } get #isAssociatedIdentity() { return this.#state & Ci.nsIWebProgressListener.STATE_IDENTITY_ASSOCIATED; } get #isMixedActiveContentLoaded() { return ( this.#state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT ); } get #isMixedActiveContentBlocked() { return ( this.#state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT ); } get #isMixedPassiveContentLoaded() { return ( this.#state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT ); } get #isContentHttpsOnlyModeUpgraded() { return ( this.#state & Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADED ); } get #isContentHttpsOnlyModeUpgradeFailed() { return ( this.#state & Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED ); } get #isContentHttpsFirstModeUpgraded() { return ( this.#state & Ci.nsIWebProgressListener.STATE_HTTPS_ONLY_MODE_UPGRADED_FIRST ); } get #isCertUserOverridden() { return this.#state & Ci.nsIWebProgressListener.STATE_CERT_USER_OVERRIDDEN; } get #isCertErrorPage() { let { documentURI } = gBrowser.selectedBrowser; if (documentURI?.scheme != "about") { return false; } return ( documentURI.filePath == "certerror" || (documentURI.filePath == "neterror" && new URLSearchParams(documentURI.query).get("e") == "nssFailure2") ); } get #isSecurelyConnectedAboutNetErrorPage() { let { documentURI } = gBrowser.selectedBrowser; if (documentURI?.scheme != "about" || documentURI.filePath != "neterror") { return false; } let error = new URLSearchParams(documentURI.query).get("e"); // Bug 1944993 - A list of neterrors without connection issues return error === "httpErrorPage" || error === "serverError"; } get #isAboutNetErrorPage() { let { documentURI } = gBrowser.selectedBrowser; return documentURI?.scheme == "about" && documentURI.filePath == "neterror"; } get #isAboutHttpsOnlyErrorPage() { let { documentURI } = gBrowser.selectedBrowser; return ( documentURI?.scheme == "about" && documentURI.filePath == "httpsonlyerror" ); } get #isPotentiallyTrustworthy() { return ( !this.#isBrokenConnection && (this.#isSecureContext || gBrowser.selectedBrowser.documentURI?.scheme == "chrome") ); } get #isAboutBlockedPage() { let { documentURI } = gBrowser.selectedBrowser; return documentURI?.scheme == "about" && documentURI.filePath == "blocked"; } get #isURILoadedFromFile() { return this.#uri.schemeIs("file"); } /** * Returns a promise that will resolve when determining QWAC status has * finished. Primarily intended for tests. */ get qwacStatusPromise() { return this.#qwacStatusPromise; } #supplementalText() { let supplemental = ""; let verifier = ""; let owner = ""; // Fill in the CA name if we have a valid TLS certificate. if (this.#isSecureConnection || this.#isCertUserOverridden) { verifier = this.#tooltipText(); } // Fill in organization information if we have a valid EV certificate or // QWAC. if (this.#isEV || this.#qwac) { let iData = this.#getIdentityData(this.#qwac || this.#secInfo.serverCert); owner = iData.subjectOrg; verifier = this.#tooltipText(); // Build an appropriate supplemental block out of whatever location data we have if (iData.city) { supplemental += iData.city + "\n"; } if (iData.state && iData.country) { supplemental += gNavigatorBundle.getFormattedString( "identity.identified.state_and_country", [iData.state, iData.country] ); } else if (iData.state) { // State only supplemental += iData.state; } else if (iData.country) { // Country only supplemental += iData.country; } } return { supplemental, verifier, owner }; } #tooltipText() { let tooltip = ""; let warnTextOnInsecure = insecureConnectionTextEnabled || (insecureConnectionTextPBModeEnabled && PrivateBrowsingUtils.isWindowPrivate(window)); if (this.#uriHasHost && this.#isSecureConnection) { // This is a secure connection. if (!this.#isCertUserOverridden) { // It's a normal cert, verifier is the CA Org. tooltip = gNavigatorBundle.getFormattedString( "identity.identified.verifier", [this.#getIdentityData().caOrg] ); } } else if (this.#isBrokenConnection) { if (this.#isMixedActiveContentLoaded) { if ( UrlbarPrefs.getScotchBonnetPref("trimHttps") && warnTextOnInsecure ) { tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip"); } } } else if (!this.#isPotentiallyTrustworthy) { tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip"); } if (this.#isCertUserOverridden) { // Cert is trusted because of a security exception, verifier is a special string. tooltip = gNavigatorBundle.getString( "identity.identified.verified_by_you" ); } return tooltip; } #connectionState() { // Determine connection security information. let connection = "not-secure"; if (this.#isInternalSecurePage) { connection = "chrome"; } else if (this.#pageExtensionPolicy) { connection = "extension"; } else if (this.#isURILoadedFromFile) { connection = "file"; } else if (this.#qwac) { connection = "secure-etsi"; } else if (this.#isEV) { connection = "secure-ev"; } else if (this.#isCertUserOverridden) { connection = "secure-cert-user-overridden"; } else if (this.#isSecureConnection) { connection = "secure"; } else if (this.#isCertErrorPage) { connection = "cert-error-page"; } else if (this.#isAboutHttpsOnlyErrorPage) { connection = "https-only-error-page"; } else if (this.#isAboutBlockedPage) { connection = "not-secure"; } else if (this.#isSecurelyConnectedAboutNetErrorPage) { connection = "secure"; } else if (this.#isAboutNetErrorPage) { connection = "net-error-page"; } else if (this.#isAssociatedIdentity) { connection = "associated"; } else if (this.#isPotentiallyTrustworthy) { connection = "file"; } return connection; } #connectionLabel() { if (this.#isAboutNetErrorPage) { return "identity-connection-failure"; } if (this.#isSecurePage()) { return "trustpanel-connection-label-secure"; } return "trustpanel-connection-label-insecure"; } #mixedContentState() { let mixedcontent = []; if (this.#isMixedPassiveContentLoaded) { mixedcontent.push("passive-loaded"); } if (this.#isMixedActiveContentLoaded) { mixedcontent.push("active-loaded"); } else if (this.#isMixedActiveContentBlocked) { mixedcontent.push("active-blocked"); } return mixedcontent; } #ciphersState() { // We have no specific flags for weak ciphers (yet). If a connection is // broken and we can't detect any mixed content loaded then it's a weak // cipher. if ( this.#isBrokenConnection && !this.#isMixedActiveContentLoaded && !this.#isMixedPassiveContentLoaded ) { return "weak"; } return ""; } #httpsOnlyState() { // If HTTPS-Only Mode is enabled, check the permission status const privateBrowsingWindow = PrivateBrowsingUtils.isWindowPrivate(window); const isHttpsOnlyModeActive = this.#isHttpsOnlyModeActive( privateBrowsingWindow ); const isHttpsFirstModeActive = this.#isHttpsFirstModeActive( privateBrowsingWindow ); const isSchemelessHttpsFirstModeActive = this.#isSchemelessHttpsFirstModeActive(privateBrowsingWindow); let httpsOnlyStatus = ""; if ( isHttpsFirstModeActive || isHttpsOnlyModeActive || isSchemelessHttpsFirstModeActive ) { // Note: value and permission association is laid out // in _getHttpsOnlyPermission let value = this.#getHttpsOnlyPermission(); // We do not want to display the exception ui for schemeless // HTTPS-First, but we still want the "Upgraded to HTTPS" label. document.getElementById( "trustpanel-popup-security-httpsonlymode" ).hidden = isSchemelessHttpsFirstModeActive; document.getElementById( "trustpanel-popup-security-menulist-off-item" ).hidden = privateBrowsingWindow && value != 1; document.getElementById( "trustpanel-popup-security-httpsonlymode-menulist" ).value = value; if (value > 0) { httpsOnlyStatus = "exception"; } else if ( this.#isAboutHttpsOnlyErrorPage || (isHttpsFirstModeActive && this.#isContentHttpsOnlyModeUpgradeFailed) ) { httpsOnlyStatus = "failed-top"; } else if (this.#isContentHttpsOnlyModeUpgradeFailed) { httpsOnlyStatus = "failed-sub"; } else if ( this.#isContentHttpsOnlyModeUpgraded || this.#isContentHttpsFirstModeUpgraded ) { httpsOnlyStatus = "upgraded"; } } return httpsOnlyStatus; } /** * Gets the current HTTPS-Only mode permission for the current page. * Values are the same as in #identity-popup-security-httpsonlymode-menulist, * -1 indicates a incompatible scheme on the current URI. */ #getHttpsOnlyPermission() { let uri = gBrowser.currentURI; if (uri instanceof Ci.nsINestedURI) { uri = uri.QueryInterface(Ci.nsINestedURI).innermostURI; } if (!uri.schemeIs("http") && !uri.schemeIs("https")) { return -1; } uri = uri.mutate().setScheme("http").finalize(); const principal = Services.scriptSecurityManager.createContentPrincipal( uri, gBrowser.contentPrincipal.originAttributes ); const { state } = SitePermissions.getForPrincipal( principal, "https-only-load-insecure" ); switch (state) { case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION: return 2; // Off temporarily case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW: return 1; // Off default: return 0; // On } } /** * Sets/removes HTTPS-Only Mode exception and possibly reloads the page. */ #changeHttpsOnlyPermission() { // Get the new value from the menulist and the current value // Note: value and permission association is laid out // in _getHttpsOnlyPermission const oldValue = this.#getHttpsOnlyPermission(); if (oldValue < 0) { console.error( "Did not update HTTPS-Only permission since scheme is incompatible" ); return; } let menulist = document.getElementById( "trustpanel-popup-security-httpsonlymode-menulist" ); let newValue = parseInt(menulist.selectedItem.value, 10); // If nothing changed, just return here if (newValue === oldValue) { return; } // We always want to set the exception for the HTTP version of the current URI, // since when we check wether we should upgrade a request, we are checking permissons // for the HTTP principal (Bug 1757297). let newURI = gBrowser.currentURI; if (newURI instanceof Ci.nsINestedURI) { newURI = newURI.QueryInterface(Ci.nsINestedURI).innermostURI; } newURI = newURI.mutate().setScheme("http").finalize(); const principal = Services.scriptSecurityManager.createContentPrincipal( newURI, gBrowser.contentPrincipal.originAttributes ); // Set or remove the permission if (newValue === 0) { SitePermissions.removeFromPrincipal( principal, "https-only-load-insecure" ); } else if (newValue === 1) { SitePermissions.setForPrincipal( principal, "https-only-load-insecure", Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW, SitePermissions.SCOPE_PERSISTENT ); } else { SitePermissions.setForPrincipal( principal, "https-only-load-insecure", Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION, SitePermissions.SCOPE_SESSION ); } // If we're on the error-page, we have to redirect the user // from HTTPS to HTTP. Otherwise we can just reload the page. if (this.#isAboutHttpsOnlyErrorPage) { gBrowser.loadURI(newURI, { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, }); PanelMultiView.hidePopup(this.#popup); return; } // The page only needs to reload if we switch between allow and block // Because "off" is 1 and "off temporarily" is 2, we can just check if the // sum of newValue and oldValue is 3. if (newValue + oldValue !== 3) { BrowserCommands.reloadSkipCache(); PanelMultiView.hidePopup(this.#popup); // Ensure the browser is focused again, otherwise we may not trigger the // security delay on a potential error page following this reload. gBrowser.selectedBrowser.focus(); } } /** * Adds the toggles into the smartblock toggle container. Clears existing toggles first, then * searches through the contentBlockingLog for smartblock-compatible content. * * @returns {boolean} true if a smartblock compatible resource is blocked or shimmed, false otherwise */ #addSmartblockEmbedToggles() { if (!smartblockEmbedsEnabledPref) { // Do not insert toggles if feature is disabled. return false; } let container = document.getElementById( "trustpanel-smartblock-toggle-container" ); container.replaceChildren(); // check that there is an allowed or replaced flag present let contentBlockingEvents = gBrowser.selectedBrowser.getContentBlockingEvents(); // In the future, we should add a flag specifically for smartblock embeds so that // these checks do not trigger when a non-embed-related shim is shimming // a smartblock compatible site, see Bug 1926461 let somethingAllowedOrReplaced = contentBlockingEvents & Ci.nsIWebProgressListener.STATE_ALLOWED_TRACKING_CONTENT || contentBlockingEvents & Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT; if (!somethingAllowedOrReplaced) { // return early if there is no content that is allowed or replaced return false; } let blocked = this.#fetchSmartBlocked(); if (!blocked.length) { return false; } // search through content log for compatible blocked origins for (let { shimAllowed, shimInfo } of blocked) { const { shimId, displayName } = shimInfo; // check that a toggle doesn't already exist let existingToggle = document.getElementById( `trustpanel-smartblock-${shimId.toLowerCase()}-toggle` ); if (existingToggle) { // make sure toggle state is allowed if ANY of the sites are allowed if (shimAllowed) { existingToggle.setAttribute("pressed", true); } // skip adding a new toggle continue; } // create the toggle element let toggle = document.createElement("moz-toggle"); toggle.setAttribute( "id", `trustpanel-smartblock-${shimId.toLowerCase()}-toggle` ); toggle.setAttribute("data-l10n-attrs", "label"); document.l10n.setAttributes( toggle, "protections-panel-smartblock-blocking-toggle", { trackername: displayName, } ); // set toggle to correct position toggle.toggleAttribute("pressed", !!shimAllowed); // add functionality to toggle toggle.addEventListener("toggle", event => { if (event.target.pressed) { this.#sendUnblockMessageToSmartblock(shimId); } else { this.#sendReblockMessageToSmartblock(shimId); } PanelMultiView.hidePopup(this.#popup); }); container.insertAdjacentElement("beforeend", toggle); } return true; } #fetchSmartBlocked() { let blocked = []; let contentBlockingLog = JSON.parse( gBrowser.selectedBrowser.getContentBlockingLog() ); // search through content log for compatible blocked origins for (let [origin, actions] of Object.entries(contentBlockingLog)) { let shimAllowed = actions.some( ([flag]) => (flag & Ci.nsIWebProgressListener.STATE_ALLOWED_TRACKING_CONTENT) != 0 ); let shimDetected = actions.some( ([flag]) => (flag & Ci.nsIWebProgressListener.STATE_REPLACED_TRACKING_CONTENT) != 0 ); if (!shimAllowed && !shimDetected) { // origin is not being shimmed or allowed continue; } let shimInfo = SMARTBLOCK_EMBED_INFO.find(element => { let matchPatternSet = new MatchPatternSet(element.matchPatterns); return matchPatternSet.matches(origin); }); if (!shimInfo) { // origin not relevant to smartblock continue; } blocked.push({ shimAllowed, shimInfo }); } return blocked; } async observe(subject, topic) { if (!this.#enabled) { return; } switch (topic) { case "smartblock:open-protections-panel": { if (gBrowser.selectedBrowser.browserId !== subject.browserId) { break; } this.#initializePopup(); let multiview = document.getElementById("trustpanel-popup-multiView"); // TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=1999928 // This currently opens as a standalone panel, we would like to open // the panel with a back button and title the same way as if it // were accessed via the urlbar icon. let initialMainViewId = multiview.getAttribute("mainViewId"); this.#popup.addEventListener( "popuphidden", () => { multiview.setAttribute("mainViewId", initialMainViewId); }, { once: true } ); multiview.setAttribute("mainViewId", "trustpanel-blockerView"); this.showPopup({ reason: "embedPlaceholderButton" }); break; } } } // We handle focus here when the panel is shown. handleEvent(event) { switch (event.type) { case "popupshown": this.onPopupShown(event); break; } } onPopupShown() { // Disable the toggles for a short time after opening via SmartBlock placeholder button // to prevent clickjacking. if (this.#openingReason == "embedPlaceholderButton") { this.#disablePopupToggles(); this.#popupToggleDelayTimer = setTimeout(() => { this.#enablePopupToggles(); }, popupClickjackDelay); } } /** * Sends a message to webcompat extension to unblock content and remove placeholders * * @param {string} shimId - the id of the shim blocking the content */ #sendUnblockMessageToSmartblock(shimId) { Services.obs.notifyObservers( gBrowser.selectedTab, "smartblock:unblock-embed", shimId ); } /** * Sends a message to webcompat extension to reblock content * * @param {string} shimId - the id of the shim blocking the content */ #sendReblockMessageToSmartblock(shimId) { Services.obs.notifyObservers( gBrowser.selectedTab, "smartblock:reblock-embed", shimId ); } #resetToggleSecDelay() { clearTimeout(this.#popupToggleDelayTimer); this.#popupToggleDelayTimer = setTimeout(() => { this.#enablePopupToggles(); }, popupClickjackDelay); } #disablePopupToggles() { // Disables all toggles in the protections panel this.#popup.querySelectorAll("moz-toggle").forEach(toggle => { toggle.setAttribute("disabled", true); toggle.addEventListener("pointerdown", this.#resetToggleReference); }); } #resetToggleReference = this.#resetToggleSecDelay.bind(this); #enablePopupToggles() { // Enables all toggles in the protections panel this.#popup.querySelectorAll("moz-toggle").forEach(toggle => { if ( toggle.id != "trustpanel-toggle" || ContentBlockingAllowList.canHandle(window.gBrowser.selectedBrowser) ) { toggle.removeAttribute("disabled"); } toggle.removeEventListener("pointerdown", this.#resetToggleReference); }); } #updateAttribute(elem, attr, value) { if (value) { elem.setAttribute(attr, value); } else { elem.removeAttribute(attr); } } } var gTrustPanelHandler = new TrustPanel();