tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit e2ad70d36fcf5fd43380c5b9dae60bcf9631bdd9
parent 378d73ab60203ce30714d1cb05b50c707eebe835
Author: Dale Harvey <dale@arandomurl.com>
Date:   Wed, 19 Nov 2025 20:08:41 +0000

Bug 1986497 - Implement smartblock controls in trustpanel. r=emz,desktop-theme-reviewers,dao

Differential Revision: https://phabricator.services.mozilla.com/D271417

Diffstat:
Mbrowser/base/content/browser-siteProtections.js | 10++++++++++
Mbrowser/base/content/browser-trustPanel.js | 302++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mbrowser/components/controlcenter/content/trustPanel.inc.xhtml | 8++++++++
Mbrowser/extensions/webcompat/tests/browser/browser.toml | 8++++++++
Abrowser/extensions/webcompat/tests/browser/browser_smartblockembeds_trustpanel.js | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/extensions/webcompat/tests/browser/head.js | 18++++++++++++++----
Mbrowser/themes/shared/controlcenter/panel.css | 9+++++++++
7 files changed, 481 insertions(+), 12 deletions(-)

diff --git a/browser/base/content/browser-siteProtections.js b/browser/base/content/browser-siteProtections.js @@ -1811,6 +1811,13 @@ var gProtectionsHandler = { false ); + XPCOMUtils.defineLazyPreferenceGetter( + this, + "trustPanelEnabledPref", + "browser.urlbar.trustPanel.featureGate", + false + ); + for (let blocker of Object.values(this.blockers)) { if (blocker.init) { blocker.init(); @@ -2862,6 +2869,9 @@ var gProtectionsHandler = { * telemetry purposes. */ showProtectionsPopup(options = {}) { + if (this.trustPanelEnabledPref) { + return; + } const { event, toast, openingReason } = options; this._initializePopup(); diff --git a/browser/base/content/browser-trustPanel.js b/browser/base/content/browser-trustPanel.js @@ -50,6 +50,18 @@ XPCOMUtils.defineLazyPreferenceGetter( "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", @@ -65,6 +77,37 @@ const ETP_DISABLED_ASSETS = { 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; @@ -76,6 +119,9 @@ class TrustPanel { #lastEvent = null; + #popupToggleDelayTimer = null; + #openingReason = null; + #blockers = { SocialTracking, ThirdPartyCookies, @@ -90,6 +136,9 @@ class TrustPanel { blocker.init(); } } + + // Add an observer to listen to requests to open the protections panel + Services.obs.addObserver(this, "smartblock:open-protections-panel"); } uninit() { @@ -98,6 +147,8 @@ class TrustPanel { blocker.uninit(); } } + + Services.obs.removeObserver(this, "smartblock:open-protections-panel"); } get #popup() { @@ -191,16 +242,20 @@ class TrustPanel { document .getElementById("trustpanel-popup-security-httpsonlymode-menulist") .addEventListener("command", () => this.#changeHttpsOnlyPermission()); + + this.#popup.addEventListener("popupshown", this); } } - showPopup() { + showPopup(opts = {}) { this.#initializePopup(); this.#updatePopup(); + this.#openingReason = opts.reason; let anchor = document.getElementById("trust-icon-container"); - let opts = { position: "bottomleft topleft" }; - PanelMultiView.openPopup(this.#popup, anchor, opts); + PanelMultiView.openPopup(this.#popup, anchor, { + position: "bottomleft topleft", + }); } async #hidePopup() { @@ -278,9 +333,11 @@ class TrustPanel { let host = window.gIdentityHandler.getHostForDisplay(); this.host = host; - let favicon = await PlacesUtils.favicons.getFaviconForPage(this.#uri); - document.getElementById("trustpanel-popup-icon").src = - favicon?.uri.spec ?? ""; + 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); @@ -332,7 +389,7 @@ class TrustPanel { if (!this.anyDetected) { document.getElementById("trustpanel-blocker-section").hidden = true; } else { - let count = 0; + let count = this.#fetchSmartBlocked().length; let blocked = []; let detected = []; @@ -344,6 +401,7 @@ class TrustPanel { detected.push(blocker); } } + document.l10n.setArgs( document.getElementById("trustpanel-blocker-section-header"), { count } @@ -354,6 +412,10 @@ class TrustPanel { document .getElementById("trustpanel-blocker-section") .removeAttribute("hidden"); + + document + .getElementById("trustpanel-smartblock-section") + .toggleAttribute("hidden", !this.#addSmartblockEmbedToggles()); } } @@ -529,7 +591,7 @@ class TrustPanel { } #isInternalSecurePage(uri) { - if (uri.schemeIs("about")) { + if (uri && uri.schemeIs("about")) { let module = E10SUtils.getAboutModule(uri); if (module) { let flags = module.getURIFlags(uri); @@ -1084,6 +1146,230 @@ class TrustPanel { } } + /** + * 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) { + 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 => { + toggle.removeAttribute("disabled"); + toggle.removeEventListener("pointerdown", this.#resetToggleReference); + }); + } + #updateAttribute(elem, attr, value) { if (value) { elem.setAttribute(attr, value); diff --git a/browser/components/controlcenter/content/trustPanel.inc.xhtml b/browser/components/controlcenter/content/trustPanel.inc.xhtml @@ -87,6 +87,14 @@ class="PanelUI-subView" role="document"> <toolbarseparator></toolbarseparator> + <html:moz-message-bar id="trustpanel-smartblock-section"> + <div slot="message"> + <vbox> + <description data-l10n-id="protections-panel-smartblock-desc-label"></description> + <vbox id="trustpanel-smartblock-toggle-container" class="trustpanel-smartblock-toggle-box"></vbox> + </vbox> + </div> + </html:moz-message-bar> <vbox class="trustpanel-blocker-section" id="trustpanel-blocked"> <label class="trustpanel-header" id="trustpanel-blocked-header" data-l10n-id="trustpanel-blocked-header"></label> <vbox class="trustpanel-blocker-buttons"></vbox> diff --git a/browser/extensions/webcompat/tests/browser/browser.toml b/browser/extensions/webcompat/tests/browser/browser.toml @@ -47,4 +47,12 @@ skip-if = [ "os == 'linux' && os_version == '24.04' && processor == 'x86_64' && display == 'x11' && opt && socketprocess_networking", # Bug 1960066 ] # Bug 1858919, 1960066 +["browser_smartblockembeds_trustpanel.js"] +skip-if = [ + "os == 'linux' && os_version == '18.04' && processor == 'x86_64'", # Bug 1945222 + "os == 'mac' && os_version == '10.15' && processor == 'x86_64'", # Bug 1945222 + "os == 'mac' && os_version == '15.30' && arch == 'aarch64' && opt", # Bug 1945222 + "os == 'win' && os_version == '11.26100' && processor == 'x86_64' && opt", # Bug 1945222 +] + ["browser_ua_helpers.js"] diff --git a/browser/extensions/webcompat/tests/browser/browser_smartblockembeds_trustpanel.js b/browser/extensions/webcompat/tests/browser/browser_smartblockembeds_trustpanel.js @@ -0,0 +1,138 @@ +/* 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/. */ +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["test.wait300msAfterTabSwitch", true], + ["browser.urlbar.trustPanel.featureGate", true], + // Extend clickjacking delay for test because timer expiry can happen before we + // check the toggle is disabled (especially in chaos mode). + [SEC_DELAY_PREF, 1000], + [TRACKING_PREF, true], + [SMARTBLOCK_EMBEDS_ENABLED_PREF, true], + ], + }); + + await UrlClassifierTestUtils.addTestTrackers(); + await generateTestShims(); + + registerCleanupFunction(() => { + UrlClassifierTestUtils.cleanupTestTrackers(); + }); + + Services.fog.testResetFOG(); +}); + +add_task(async function test_smartblock_embed_replaced() { + // Open a site with a test "embed" + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + waitForLoad: true, + }); + + await loadSmartblockPageOnTab(tab); + await clickOnPagePlaceholder(tab); + + // Check smartblock section is unhidden + ok( + BrowserTestUtils.isVisible( + tab.ownerDocument.getElementById("trustpanel-smartblock-toggle-container") + ), + "Smartblock section is visible" + ); + + // Check toggle is present and off + let blockedEmbedToggle = tab.ownerDocument.querySelector( + "#trustpanel-smartblock-toggle-container moz-toggle" + ); + ok(blockedEmbedToggle, "Toggle exists in container"); + ok(BrowserTestUtils.isVisible(blockedEmbedToggle), "Toggle is visible"); + ok(!blockedEmbedToggle.pressed, "Unblock toggle should be off"); + + // Check toggle disabled by clickjacking protections + ok(blockedEmbedToggle.disabled, "Unblock toggle should be disabled"); + + // Wait for clickjacking protections to timeout + let delayTime = Services.prefs.getIntPref(SEC_DELAY_PREF); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, delayTime + 100)); + + // Setup promise on custom event to wait for placeholders to finish replacing + let embedScriptFinished = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "testEmbedScriptFinished", + false, + null, + true + ); + + // Click to toggle to unblock embed and wait for script to finish + await EventUtils.synthesizeMouseAtCenter(blockedEmbedToggle.buttonEl, {}); + + await embedScriptFinished; + + ok(blockedEmbedToggle.pressed, "Unblock toggle should be on"); + + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + let unloadedEmbed = content.document.querySelector(".broken-embed-content"); + ok(!unloadedEmbed, "Unloaded embeds should not be on the page"); + + // Check embed was put back on the page + let loadedEmbed = content.document.querySelector(".loaded-embed-content"); + ok(loadedEmbed, "Embed should now be on the page"); + }); + + await openProtectionsPanel(window); + + await EventUtils.synthesizeMouseAtCenter( + document.getElementById("trustpanel-blocker-see-all"), + {} + ); + + // Check if smartblock section is still there after unblock + await BrowserTestUtils.waitForCondition(() => { + return BrowserTestUtils.isVisible( + tab.ownerDocument.getElementById("trustpanel-smartblock-toggle-container") + ); + }); + + // Check toggle is still there and is on now + blockedEmbedToggle = tab.ownerDocument.querySelector( + "#trustpanel-smartblock-toggle-container moz-toggle" + ); + ok(blockedEmbedToggle, "Toggle exists in container"); + ok(BrowserTestUtils.isVisible(blockedEmbedToggle), "Toggle is visible"); + ok(blockedEmbedToggle.pressed, "Unblock toggle should be on"); + + // Setup promise on custom event to wait for placeholders to finish replacing + let smartblockScriptFinished = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "smartblockEmbedScriptFinished", + false, + null, + true + ); + + // click toggle to reblock (this will trigger a reload) + // Note: clickjacking delay should not happen because panel not opened via embed button + blockedEmbedToggle.click(); + + // Wait for smartblock embed script to finish + await smartblockScriptFinished; + + ok(!blockedEmbedToggle.pressed, "Unblock toggle should be off"); + + await SpecialPowers.spawn(tab.linkedBrowser, [], () => { + // Check that the "embed" was replaced with a placeholder + let placeholder = content.document.querySelector( + ".shimmed-embedded-content" + ); + + ok(placeholder, "Embed replaced with a placeholder after reblock"); + }); + + await BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/extensions/webcompat/tests/browser/head.js b/browser/extensions/webcompat/tests/browser/head.js @@ -293,8 +293,14 @@ async function testShimDoesNotRun( await BrowserTestUtils.removeTab(tab); } +function panelId() { + return Services.prefs.getBoolPref("browser.urlbar.trustPanel.featureGate") + ? "trustpanel-popup" + : "protections-popup"; +} + async function closeProtectionsPanel(win = window) { - let protectionsPopup = win.document.getElementById("protections-popup"); + let protectionsPopup = win.document.getElementById(panelId()); if (!protectionsPopup) { return; } @@ -312,10 +318,14 @@ async function openProtectionsPanel(win = window) { win, "popupshown", true, - e => e.target.id == "protections-popup" + e => e.target.id == panelId() ); - win.gProtectionsHandler.showProtectionsPopup(); + if (Services.prefs.getBoolPref("browser.urlbar.trustPanel.featureGate")) { + win.gTrustPanelHandler.showPopup(); + } else { + win.gProtectionsHandler.showProtectionsPopup(); + } await popupShownPromise; } @@ -343,7 +353,7 @@ async function clickOnPagePlaceholder(tab) { window, "popupshown", true, - e => e.target.id == "protections-popup" + e => e.target.id == panelId() ); await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { diff --git a/browser/themes/shared/controlcenter/panel.css b/browser/themes/shared/controlcenter/panel.css @@ -91,6 +91,7 @@ #identity-popup-mainView, #permission-popup-mainView, #trustpanel-popup-mainView, +#trustpanel-blockerView, #protections-popup-mainView { min-width: var(--popup-width); max-width: var(--popup-width); @@ -968,6 +969,14 @@ } } +#trustpanel-smartblock-section { + margin: var(--space-large); +} + +#trustpanel-smartblock-toggle-container { + margin-block-start: var(--space-large); +} + .moz-button-subviewbutton-nav { --button-font-weight: normal; --button-alignment: inline-start;