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:
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;