tor-browser

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

commit 86937429cf28205d79998d147380193138a6b7c3
parent c06c2d185f7043763f3025d700a1dc412acba95c
Author: kpatenio <kpatenio@mozilla.com>
Date:   Tue, 21 Oct 2025 17:04:27 +0000

Bug 1993322 — add basic site exceptions controls to IP Protection settings section. r=ip-protection-reviewers,fluent-reviewers,mstriemer,bolsson,desktop-theme-reviewers,fchasen

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

Diffstat:
Mbrowser/components/preferences/main.js | 33++++++++++++++++++++++++++++++---
Mbrowser/components/preferences/privacy.js | 27++++++++++++++++++++++++++-
Mbrowser/components/preferences/tests/browser_privacy_ipprotection.js | 189++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mbrowser/locales-preview/ipProtection.ftl | 17+++++++++++++++++
Mbrowser/themes/shared/preferences/preferences.css | 1+
5 files changed, 257 insertions(+), 10 deletions(-)

diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js @@ -1486,11 +1486,38 @@ let SETTINGS_CONFIG = { supportPage: "ip-protection", items: [ { - id: "ipProtectionPlaceholderMessage", - control: "moz-message-bar", + id: "ipProtectionExceptionsMode", + l10nId: "ip-protection-site-exceptions", + control: "moz-radio-group", controlAttrs: { - message: "This is a placeholder for the IP Protection section", + ".headingLevel": 3, }, + options: [ + { + id: "ipProtectionExceptionRadioAll", + value: "all", + l10nId: "ip-protection-site-exceptions-all-sites-radio", + items: [ + { + id: "ipProtectionExceptionAllListButton", + l10nId: "ip-protection-site-exceptions-all-sites-button", + control: "moz-box-button", + }, + ], + }, + { + id: "ipProtectionExceptionRadioSelect", + value: "select", + l10nId: "ip-protection-site-exceptions-select-sites-radio", + items: [ + { + id: "ipProtectionExceptionSelectListButton", + l10nId: "ip-protection-site-exceptions-select-sites-button", + control: "moz-box-button", + }, + ], + }, + ], }, ], }, diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js @@ -212,6 +212,7 @@ Preferences.addAll([ // Firefox VPN { id: "browser.ipProtection.variant", type: "string" }, + { id: "browser.ipProtection.exceptionsMode", type: "string" }, // Media { id: "media.autoplay.default", type: "int" }, @@ -1260,11 +1261,35 @@ Preferences.addSetting({ pref: "browser.ipProtection.variant", get: prefVal => prefVal == "beta", }); +// This setting also affects the radio group for site exceptions Preferences.addSetting({ - id: "ipProtectionPlaceholderMessage", + id: "ipProtectionExceptionsMode", + pref: "browser.ipProtection.exceptionsMode", deps: ["ipProtectionVisible"], visible: ({ ipProtectionVisible }) => ipProtectionVisible.value, }); +Preferences.addSetting({ + id: "ipProtectionExceptionAllListButton", + deps: ["ipProtectionVisible", "ipProtectionExceptionsMode"], + visible: ({ ipProtectionVisible, ipProtectionExceptionsMode }) => + ipProtectionVisible.value && ipProtectionExceptionsMode.value == "all", + onUserClick() { + // TODO: show UI based on current exception mode selected (Bug 1993334) + // We can read the target id to verify the button type and open a dialog + // with gSubDialog.open + }, +}); +Preferences.addSetting({ + id: "ipProtectionExceptionSelectListButton", + deps: ["ipProtectionVisible", "ipProtectionExceptionsMode"], + visible: ({ ipProtectionVisible, ipProtectionExceptionsMode }) => + ipProtectionVisible.value && ipProtectionExceptionsMode.value == "select", + onUserClick() { + // TODO: show UI based on current exception mode selected (Bug 1993334) + // We can read the target id to verify the button type and open a dialog + // with gSubDialog.open + }, +}); // Study opt out if (AppConstants.MOZ_DATA_REPORTING) { diff --git a/browser/components/preferences/tests/browser_privacy_ipprotection.js b/browser/components/preferences/tests/browser_privacy_ipprotection.js @@ -6,15 +6,23 @@ "use strict"; const FEATURE_PREF = "browser.ipProtection.variant"; +const MODE_PREF = "browser.ipProtection.exceptionsMode"; const SECTION_ID = "dataIPProtectionGroup"; +async function setupVpnPrefs({ feature, mode = "all" }) { + return SpecialPowers.pushPrefEnv({ + set: [ + [FEATURE_PREF, feature], + [MODE_PREF, mode], + ], + }); +} + // Test the section is hidden on page load if the variant pref is set to an ineligible experiment. add_task( async function test_section_removed_when_set_to_ineligible_experiment_pref() { - await SpecialPowers.pushPrefEnv({ - set: [[FEATURE_PREF, "alpha"]], - }); + await setupVpnPrefs({ feature: "alpha" }); await BrowserTestUtils.withNewTab( { gBrowser, url: "about:preferences#privacy" }, @@ -31,9 +39,7 @@ add_task( // Test the section is shown on page load if the variant pref is set to an eligible experiment add_task( async function test_section_shown_when_set_to_eligible_experiment_pref() { - await SpecialPowers.pushPrefEnv({ - set: [[FEATURE_PREF, "beta"]], - }); + await setupVpnPrefs({ feature: "beta" }); await BrowserTestUtils.withNewTab( { gBrowser, url: "about:preferences#privacy" }, @@ -46,3 +52,174 @@ add_task( await SpecialPowers.popPrefEnv(); } ); + +// Test the site exceptions controls load correctly with mode set to "all" +add_task(async function test_exceptions_load_with_all_mode() { + await setupVpnPrefs({ feature: "beta" }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let section = browser.contentDocument.getElementById(SECTION_ID); + let settingGroup = section.querySelector( + `setting-group[groupid="ipprotection"]` + ); + is_element_visible(section, "#dataIPProtectionGroup is shown"); + is_element_visible(settingGroup, "ipprotection setting group is shown"); + + let siteExceptionsRadioGroup = settingGroup?.querySelector( + "#ipProtectionExceptionsMode" + ); + is_element_visible( + siteExceptionsRadioGroup, + "Site exceptions radio group is shown" + ); + + let exceptionAllRadioButton = siteExceptionsRadioGroup?.querySelector( + "#ipProtectionExceptionRadioAll" + ); + let exceptionSelectRadioButton = siteExceptionsRadioGroup?.querySelector( + "#ipProtectionExceptionRadioSelect" + ); + Assert.ok( + exceptionAllRadioButton?.checked, + "The 'all' radio button should be checked" + ); + Assert.ok( + !exceptionSelectRadioButton?.checked, + "The 'select' radio button should not be checked" + ); + + let exceptionAllListButton = siteExceptionsRadioGroup?.querySelector( + "#ipProtectionExceptionAllListButton" + ); + let exceptionSelectListButton = siteExceptionsRadioGroup?.querySelector( + "#ipProtectionExceptionSelectListButton" + ); + is_element_visible( + exceptionAllListButton, + "Button for list of exclusions is shown" + ); + is_element_hidden( + exceptionSelectListButton, + "Button for list of inclusions is hidden" + ); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +// Test the site exceptions controls load correctly with mode set to "select" +add_task(async function test_exceptions_with_select_mode() { + await setupVpnPrefs({ feature: "beta", mode: "select" }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let section = browser.contentDocument.getElementById(SECTION_ID); + let settingGroup = section.querySelector( + `setting-group[groupid="ipprotection"]` + ); + is_element_visible(section, "#dataIPProtectionGroup is shown"); + is_element_visible(settingGroup, "ipprotection setting group is shown"); + + let siteExceptionsRadioGroup = settingGroup?.querySelector( + "#ipProtectionExceptionsMode" + ); + is_element_visible( + siteExceptionsRadioGroup, + "Site exceptions radio group is shown" + ); + + let exceptionAllRadioButton = siteExceptionsRadioGroup?.querySelector( + "#ipProtectionExceptionRadioAll" + ); + let exceptionSelectRadioButton = siteExceptionsRadioGroup?.querySelector( + "#ipProtectionExceptionRadioSelect" + ); + Assert.ok( + !exceptionAllRadioButton?.checked, + "The 'all' radio button should not be checked" + ); + Assert.ok( + exceptionSelectRadioButton?.checked, + "The 'select' radio button should be checked" + ); + + let exceptionAllListButton = siteExceptionsRadioGroup?.querySelector( + "#ipProtectionExceptionAllListButton" + ); + let exceptionSelectListButton = siteExceptionsRadioGroup?.querySelector( + "#ipProtectionExceptionSelectListButton" + ); + is_element_hidden( + exceptionAllListButton, + "Button for list of exclusions is hidden" + ); + is_element_visible( + exceptionSelectListButton, + "Button for list of inclusions is shown" + ); + } + ); + + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); +}); + +// Test the site exceptions controls and pref update correctly after selecting another mode option. +add_task(async function test_exceptions_change_mode_and_buttons() { + await setupVpnPrefs({ feature: "beta" }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:preferences#privacy" }, + async function (browser) { + let section = browser.contentDocument.getElementById(SECTION_ID); + let settingGroup = section.querySelector( + `setting-group[groupid="ipprotection"]` + ); + is_element_visible(section, "#dataIPProtectionGroup is shown"); + is_element_visible(settingGroup, "ipprotection setting group is shown"); + + let siteExceptionsRadioGroup = settingGroup?.querySelector( + "#ipProtectionExceptionsMode" + ); + is_element_visible( + siteExceptionsRadioGroup, + "Site exceptions radio group is shown" + ); + + let exceptionAllRadioButton = siteExceptionsRadioGroup?.querySelector( + "#ipProtectionExceptionRadioAll" + ); + let exceptionSelectRadioButton = siteExceptionsRadioGroup?.querySelector( + "#ipProtectionExceptionRadioSelect" + ); + + // Change mode by clicking "select" button + exceptionSelectRadioButton.click(); + + Assert.ok( + !exceptionAllRadioButton?.checked, + "The 'all' radio button should not be checked" + ); + Assert.ok( + exceptionSelectRadioButton?.checked, + "The 'select' radio button should be checked" + ); + + let mode = Services.prefs.getStringPref(MODE_PREF); + Assert.equal( + mode, + "select", + `Mode should now be "select" instead of "all"` + ); + + Services.prefs.clearUserPref(MODE_PREF); + } + ); + + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/locales-preview/ipProtection.ftl b/browser/locales-preview/ipProtection.ftl @@ -81,4 +81,21 @@ ip-protection-description = .description = Hides your location and adds extra encryption to your browsing in { -brand-short-name }. ip-protection-learn-more = Learn more +ip-protection-site-exceptions = + .label = Where to use VPN + .description = Use VPN for all websites except ones you choose, or for select specific ones. +ip-protection-site-exceptions-all-sites-radio = + .label = All websites (default) +ip-protection-site-exceptions-all-sites-button = + .label = { -firefox-vpn-brand-name } is off for these websites + .description = No websites added yet + +# "Select" is an adjective here to describe a setting that allows running the VPN on certain sites only. +# Not to be confused with the action of selecting a site, which is not at all applicable to this setting. +ip-protection-site-exceptions-select-sites-radio = + .label = Select websites +ip-protection-site-exceptions-select-sites-button = + .label = { -firefox-vpn-brand-name } is on for these websites + .description = No websites added yet + ## diff --git a/browser/themes/shared/preferences/preferences.css b/browser/themes/shared/preferences/preferences.css @@ -13,6 +13,7 @@ */ --heading-font-size-xlarge: var(--font-size-xlarge); --heading-font-size-large: var(--font-size-large); + --heading-font-size-medium: var(--font-size-medium); user-select: text; }