tor-browser

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

commit 870365b83c1953a2e3fdab88423b2826fb58e6b7
parent 861a4f1e9468f8a99ad7e4ff2c08cacbec3371e4
Author: kpatenio <kpatenio@mozilla.com>
Date:   Fri, 14 Nov 2025 19:21:40 +0000

Bug 1997411 — add a new site settings button in status card r=ip-protection-reviewers,fluent-reviewers,rking,bolsson

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

Diffstat:
Mbrowser/components/ipprotection/IPProtectionPanel.sys.mjs | 7+++++++
Mbrowser/components/ipprotection/content/ipprotection-content.mjs | 27+++++++++++++++++++++------
Abrowser/components/ipprotection/content/ipprotection-site-settings-control.css | 18++++++++++++++++++
Abrowser/components/ipprotection/content/ipprotection-site-settings-control.mjs | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/ipprotection/content/ipprotection-status-card.css | 4++++
Mbrowser/components/ipprotection/content/ipprotection-status-card.mjs | 30+++++++++++++++++++++++++++++-
Mbrowser/components/ipprotection/jar.mn | 2++
Mbrowser/components/ipprotection/tests/browser/browser.toml | 2++
Abrowser/components/ipprotection/tests/browser/browser_ipprotection_site_settings_control.js | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/locales-preview/ipProtection.ftl | 15+++++++++++++++
10 files changed, 372 insertions(+), 7 deletions(-)

diff --git a/browser/components/ipprotection/IPProtectionPanel.sys.mjs b/browser/components/ipprotection/IPProtectionPanel.sys.mjs @@ -321,6 +321,7 @@ export class IPProtectionPanel { doc.addEventListener("IPProtection:UserDisable", this.handleEvent); doc.addEventListener("IPProtection:ShowHelpPage", this.handleEvent); doc.addEventListener("IPProtection:SignIn", this.handleEvent); + doc.addEventListener("IPProtection:UserShowSiteSettings", this.handleEvent); } #removePanelListeners(doc) { @@ -331,6 +332,10 @@ export class IPProtectionPanel { doc.removeEventListener("IPProtection:UserDisable", this.handleEvent); doc.removeEventListener("IPProtection:ShowHelpPage", this.handleEvent); doc.removeEventListener("IPProtection:SignIn", this.handleEvent); + doc.removeEventListener( + "IPProtection:UserShowSiteSettings", + this.handleEvent + ); } #addProxyListeners() { @@ -399,6 +404,8 @@ export class IPProtectionPanel { hasUpgraded: lazy.IPPEnrollAndEntitleManager.hasUpgraded, error: hasError ? ERRORS.GENERIC : "", }); + } else if (event.type == "IPProtection:UserShowSiteSettings") { + // TODO: show subview for site settings (Bug 1997413) } } } diff --git a/browser/components/ipprotection/content/ipprotection-content.mjs b/browser/components/ipprotection/content/ipprotection-content.mjs @@ -47,7 +47,7 @@ export default class IPProtectionContentElement extends MozLitElement { this.keyListener = this.#keyListener.bind(this); this.messageBarListener = this.#messageBarListener.bind(this); - this.toggleListener = this.#toggleEventListener.bind(this); + this.statusCardListener = this.#statusCardListener.bind(this); this._showMessageBar = false; this._messageDismissed = false; } @@ -58,11 +58,15 @@ export default class IPProtectionContentElement extends MozLitElement { this.addEventListener("keydown", this.keyListener, { capture: true }); this.addEventListener( "ipprotection-status-card:user-toggled-on", - this.#toggleEventListener + this.#statusCardListener ); this.addEventListener( "ipprotection-status-card:user-toggled-off", - this.#toggleEventListener + this.#statusCardListener + ); + this.addEventListener( + "ipprotection-site-settings-control:click", + this.#statusCardListener ); this.addEventListener( "ipprotection-message-bar:user-dismissed", @@ -76,11 +80,15 @@ export default class IPProtectionContentElement extends MozLitElement { this.removeEventListener("keydown", this.keyListener, { capture: true }); this.removeEventListener( "ipprotection-status-card:user-toggled-on", - this.#toggleEventListener + this.#statusCardListener ); this.removeEventListener( "ipprotection-status-card:user-toggled-off", - this.#toggleEventListener + this.#statusCardListener + ); + this.removeEventListener( + "ipprotection-site-settings-control:click", + this.#statusCardListener ); this.removeEventListener( "ipprotection-message-bar:user-dismissed", @@ -160,7 +168,7 @@ export default class IPProtectionContentElement extends MozLitElement { } } - #toggleEventListener(event) { + #statusCardListener(event) { if (event.type === "ipprotection-status-card:user-toggled-on") { this.dispatchEvent( new CustomEvent("IPProtection:UserEnable", { bubbles: true }) @@ -169,6 +177,10 @@ export default class IPProtectionContentElement extends MozLitElement { this.dispatchEvent( new CustomEvent("IPProtection:UserDisable", { bubbles: true }) ); + } else if (event.type === "ipprotection-site-settings-control:click") { + this.dispatchEvent( + new CustomEvent("IPProtection:UserShowSiteSettings", { bubbles: true }) + ); } } @@ -225,12 +237,15 @@ export default class IPProtectionContentElement extends MozLitElement { } statusCardTemplate() { + // TODO: Pass site information to status-card to conditionally + // render the site settings control. (Bug 1997412) return html` <ipprotection-status-card .protectionEnabled=${this.canEnableConnection} .canShowTime=${this.canShowConnectionTime} .enabledSince=${this.state.protectionEnabledSince} .location=${this.state.location} + .siteData=${ifDefined(this.state.siteData)} ></ipprotection-status-card> `; } diff --git a/browser/components/ipprotection/content/ipprotection-site-settings-control.css b/browser/components/ipprotection/content/ipprotection-site-settings-control.css @@ -0,0 +1,18 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +@import "chrome://global/skin/global.css"; + +#site-settings-label { + --box-border-radius-start: initial; +} + +.site-settings { + --box-group-border: none; + background-color: var(--background-color-box); +} + +.exception-enabled { + --box-icon-fill: var(--icon-color-success); +} diff --git a/browser/components/ipprotection/content/ipprotection-site-settings-control.mjs b/browser/components/ipprotection/content/ipprotection-site-settings-control.mjs @@ -0,0 +1,90 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import { html, classMap } from "chrome://global/content/vendor/lit.all.mjs"; + +/** + * Custom element that implements a button for site settings in the panel. + */ +export default class IPPSiteSettingsControl extends MozLitElement { + CLICK_EVENT = "ipprotection-site-settings-control:click"; + + static properties = { + site: { type: String }, + exceptionEnabled: { type: Boolean }, + }; + + constructor() { + super(); + + this.site = null; + this.exceptionEnabled = false; + } + + get iconsrc() { + if (!this.exceptionEnabled) { + return "chrome://global/skin/icons/close-fill.svg"; + } + return "chrome://global/skin/icons/check-filled.svg"; + } + + get descriptionL10n() { + if (!this.exceptionEnabled) { + return "ipprotection-site-settings-button-vpn-off"; + } + return "ipprotection-site-settings-button-vpn-on"; + } + + handleClickSettings(event) { + event.preventDefault(); + + this.dispatchEvent( + new CustomEvent(this.CLICK_EVENT, { + bubbles: true, + composed: true, + }) + ); + } + + render() { + if (!this.site) { + return null; + } + + let icon = this.iconsrc; + let descriptionL10n = this.descriptionL10n; + let l10nArgs = JSON.stringify({ sitename: this.site }); + + return html` + <link + rel="stylesheet" + href="chrome://browser/content/ipprotection/ipprotection-site-settings-control.css" + /> + <moz-box-group> + <moz-box-item + slot="header" + id="site-settings-label" + class="site-settings" + data-l10n-id="ipprotection-site-settings-control" + ></moz-box-item> + <moz-box-button + @click=${this.handleClickSettings} + class=${classMap({ + "site-settings": true, + "exception-enabled": this.exceptionEnabled, + })} + data-l10n-id=${descriptionL10n} + data-l10n-args=${l10nArgs} + iconsrc=${icon} + ></moz-box-button> + </moz-box-group> + `; + } +} + +customElements.define( + "ipprotection-site-settings-control", + IPPSiteSettingsControl +); diff --git a/browser/components/ipprotection/content/ipprotection-status-card.css b/browser/components/ipprotection/content/ipprotection-status-card.css @@ -34,3 +34,7 @@ #connection-toggle { margin-inline-end: var(--space-small); } + +#site-settings { + --box-group-border: none; +} diff --git a/browser/components/ipprotection/content/ipprotection-status-card.mjs b/browser/components/ipprotection/content/ipprotection-status-card.mjs @@ -17,6 +17,8 @@ import { import "chrome://browser/content/ipprotection/ipprotection-header.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://global/content/elements/moz-toggle.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/ipprotection/ipprotection-site-settings-control.mjs"; /** * Custom element that implements a status card for IP protection. @@ -29,6 +31,7 @@ export default class IPProtectionStatusCard extends MozLitElement { statusGroupEl: "#status-card", connectionToggleEl: "#connection-toggle", locationEl: "#location-wrapper", + siteSettingsEl: "ipprotection-site-settings-control", }; static shadowRootOptions = { @@ -41,6 +44,7 @@ export default class IPProtectionStatusCard extends MozLitElement { canShowTime: { type: Boolean }, enabledSince: { type: Object }, location: { type: Object }, + siteData: { type: Object }, // Track toggle state separately so that we can tell when the toggle // is enabled because of the existing protection state or because of user action. _toggleEnabled: { type: Boolean, state: true }, @@ -138,7 +142,9 @@ export default class IPProtectionStatusCard extends MozLitElement { ? "ipprotection-toggle-active" : "ipprotection-toggle-inactive"; - // TODO: add site settings button as a slotted element (class="slotted") in a moz-boz-item (Bug 1997411) + const siteSettingsTemplate = this.protectionEnabled + ? this.siteSettingsTemplate() + : null; return html` <link rel="stylesheet" @@ -162,9 +168,31 @@ export default class IPProtectionStatusCard extends MozLitElement { slot="actions" ></moz-toggle> </moz-box-item> + ${siteSettingsTemplate} </moz-box-group>`; } + siteSettingsTemplate() { + // TODO: Once we're able to detect the current site and its exception status, show + // ipprotection-site-settings-control (Bug 1997412). + if (!this.siteData?.siteName) { + return null; + } + + return html` <moz-box-item + id="site-settings" + class=${classMap({ + "is-enabled": this.protectionEnabled, + })} + > + <ipprotection-site-settings-control + .site=${this.siteData.siteName} + .exceptionEnabled=${this.siteData.isException} + class="slotted" + ></ipprotection-site-settings-control> + </moz-box-item>`; + } + cardDescriptionTemplate() { // The template consists of location name and connection time. let time = this.canShowTime diff --git a/browser/components/ipprotection/jar.mn b/browser/components/ipprotection/jar.mn @@ -13,6 +13,8 @@ browser.jar: content/browser/ipprotection/ipprotection-header.mjs (content/ipprotection-header.mjs) content/browser/ipprotection/ipprotection-message-bar.mjs (content/ipprotection-message-bar.mjs) content/browser/ipprotection/ipprotection-signedout.mjs (content/ipprotection-signedout.mjs) + content/browser/ipprotection/ipprotection-site-settings-control.css (content/ipprotection-site-settings-control.css) + content/browser/ipprotection/ipprotection-site-settings-control.mjs (content/ipprotection-site-settings-control.mjs) content/browser/ipprotection/ipprotection-status-card.css (content/ipprotection-status-card.css) content/browser/ipprotection/ipprotection-status-card.mjs (content/ipprotection-status-card.mjs) content/browser/ipprotection/ipprotection-timer.mjs (content/ipprotection-timer.mjs) diff --git a/browser/components/ipprotection/tests/browser/browser.toml b/browser/components/ipprotection/tests/browser/browser.toml @@ -34,6 +34,8 @@ prefs = [ ["browser_ipprotection_proxy_errors.js"] +["browser_ipprotection_site_settings_control.js"] + ["browser_ipprotection_status_card.js"] ["browser_ipprotection_telemetry.js"] diff --git a/browser/components/ipprotection/tests/browser/browser_ipprotection_site_settings_control.js b/browser/components/ipprotection/tests/browser/browser_ipprotection_site_settings_control.js @@ -0,0 +1,184 @@ +/* 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"; + +const MOCK_SITE_NAME = "example.com"; + +/** + * Tests that we can show ipprotection-site-settings-control when VPN is on. + */ +add_task( + async function test_site_settings_control_visible_with_VPN_on_and_site_data() { + let content = await openPanel({ + isSignedOut: false, + isProtectionEnabled: false, + siteData: { + siteName: MOCK_SITE_NAME, + isException: false, + }, + }); + + Assert.ok( + BrowserTestUtils.isVisible(content), + "ipprotection content component should be present" + ); + + let statusCard = content.statusCardEl; + Assert.ok( + content.statusCardEl, + "ipprotection-status-card should be present" + ); + + // VPN is off, no site settings + Assert.ok( + !statusCard.siteSettingsEl, + "ipprotection-site-settings-control should not be present with VPN off" + ); + + let siteSettingsVisiblePromise = BrowserTestUtils.waitForMutationCondition( + statusCard.shadowRoot, + { childList: true, subtree: true }, + () => statusCard.siteSettingsEl + ); + + // Turn VPN on + await setPanelState({ + isSignedOut: false, + isProtectionEnabled: true, + siteData: { + siteName: MOCK_SITE_NAME, + isException: false, + }, + }); + + await Promise.all([statusCard.updateComplete, siteSettingsVisiblePromise]); + + Assert.ok( + statusCard.siteSettingsEl, + "Now ipprotection-site-settings-control should be present with VPN on" + ); + + await closePanel(); + } +); + +/** + * Tests that we don't show ipprotection-site-settings-control when there's no siteData. + */ +add_task( + async function test_site_settings_control_hidden_with_VPN_on_and_no_site_data() { + let content = await openPanel({ + isSignedOut: false, + isProtectionEnabled: true, + siteData: undefined, + }); + + Assert.ok( + BrowserTestUtils.isVisible(content), + "ipprotection content component should be present" + ); + + let statusCard = content.statusCardEl; + Assert.ok( + content.statusCardEl, + "ipprotection-status-card should be present" + ); + Assert.ok( + !statusCard.siteSettingsEl, + "ipprotection-site-settings-control should not be present because there's no site data" + ); + + await closePanel(); + } +); + +/** + * Tests that we don't show ipprotection-site-settings-control when an error occurs. + */ +add_task(async function test_site_settings_control_hidden_with_VPN_error() { + let content = await openPanel({ + isSignedOut: false, + isProtectionEnabled: true, + siteData: { + siteName: MOCK_SITE_NAME, + isException: false, + }, + }); + + Assert.ok( + BrowserTestUtils.isVisible(content), + "ipprotection content component should be present" + ); + + let statusCard = content.statusCardEl; + Assert.ok(content.statusCardEl, "ipprotection-status-card should be present"); + Assert.ok( + statusCard.siteSettingsEl, + "ipprotection-site-settings-control should be present" + ); + + let siteSettingsNotVisiblePromise = BrowserTestUtils.waitForMutationCondition( + statusCard.shadowRoot, + { childList: true, subtree: true }, + () => !statusCard.siteSettingsEl + ); + + // Turn VPN on + await setPanelState({ + isSignedOut: false, + isProtectionEnabled: true, + error: "generic-error", + }); + + await Promise.all([statusCard.updateComplete, siteSettingsNotVisiblePromise]); + + Assert.ok( + !statusCard.siteSettingsEl, + "Now ipprotection-site-settings-control should not be present due to an error" + ); + + await closePanel(); +}); + +/** + * Tests that we dispatch expected events when we select the button + * in ipprotection-site-settings-control. + */ +add_task(async function test_site_settings_control_click_event() { + let content = await openPanel({ + isSignedOut: false, + isProtectionEnabled: true, + siteData: { + siteName: MOCK_SITE_NAME, + isException: false, + }, + }); + + Assert.ok( + BrowserTestUtils.isVisible(content), + "ipprotection content component should be present" + ); + + let statusCard = content.statusCardEl; + Assert.ok(content.statusCardEl, "ipprotection-status-card should be present"); + Assert.ok( + statusCard.siteSettingsEl, + "ipprotection-site-settings-control should be present" + ); + + let button = + statusCard.siteSettingsEl.shadowRoot.querySelector("moz-box-button"); + let eventPromise = BrowserTestUtils.waitForEvent( + window, + "IPProtection:UserShowSiteSettings" + ); + + button.click(); + + await eventPromise; + Assert.ok("Event was dispatched after clicking site settings button"); + + await closePanel(); +}); diff --git a/browser/locales-preview/ipProtection.ftl b/browser/locales-preview/ipProtection.ftl @@ -62,6 +62,21 @@ ipprotection-connection-time = { $time } ipprotection-location-title = .title = Location selected based on fastest server +ipprotection-site-settings-control = + .label = Website settings + +# Variables: +# $sitename (String) - The name of the site that we're currently on (eg. example.com) +ipprotection-site-settings-button-vpn-off = + .label = { $sitename } + .description = VPN is off + +# Variables: +# $sitename (String) - The name of the site that we're currently on (eg. example.com) +ipprotection-site-settings-button-vpn-on = + .label = { $sitename } + .description = VPN is on + # When VPN is toggled on ipprotection-toggle-active = .aria-label = Turn VPN off