tor-browser

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

commit 3759f80defe47904aaa8e2b602b28acf02415b6d
parent fb0dc04b190c79aa6b9e7376e6308394f7cc8e03
Author: kpatenio <kpatenio@mozilla.com>
Date:   Thu, 30 Oct 2025 20:38:11 +0000

Bug 1993334 — exceptions dialogs with permissions infrastructure and capability filter r=ip-protection-reviewers,fluent-reviewers,fchasen,mstriemer,flod

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

Diffstat:
Mbrowser/components/ipprotection/IPPExceptionsManager.sys.mjs | 312++++++++++++++-----------------------------------------------------------------
Mbrowser/components/ipprotection/tests/browser/browser.toml | 2++
Abrowser/components/ipprotection/tests/browser/browser_exceptions_dialog.js | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager.js | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dbrowser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_exclusions.js | 192-------------------------------------------------------------------------------
Dbrowser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_inclusions.js | 182-------------------------------------------------------------------------------
Mbrowser/components/ipprotection/tests/xpcshell/xpcshell.toml | 4+---
Mbrowser/components/preferences/dialogs/permissions.js | 35++++++++++++++++++++++++++++++++++-
Mbrowser/components/preferences/dialogs/permissions.xhtml | 1+
Mbrowser/components/preferences/privacy.js | 32++++++++++++++++++++++++++------
Mbrowser/locales-preview/ipProtection.ftl | 7+++++++
11 files changed, 540 insertions(+), 641 deletions(-)

diff --git a/browser/components/ipprotection/IPPExceptionsManager.sys.mjs b/browser/components/ipprotection/IPPExceptionsManager.sys.mjs @@ -4,68 +4,35 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; -const lazy = {}; - const MODE_PREF = "browser.ipProtection.exceptionsMode"; -const EXCLUSIONS_PREF = "browser.ipProtection.domainExclusions"; -const INCLUSIONS_PREF = "browser.ipProtection.domainInclusions"; -const LOG_PREF = "browser.ipProtection.log"; const MODE = { ALL: "all", SELECT: "select", }; -ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { - return console.createInstance({ - prefix: "IPPExceptionsManager", - maxLogLevel: Services.prefs.getBoolPref(LOG_PREF, false) ? "Debug" : "Warn", - }); -}); +const PERM_NAME = "ipp-vpn"; /** * Manages site inclusions and exclusions for IP Protection. + * It communicates with Services.perms to update the ipp-vpn permission type. + * Site exclusions are marked as permissions with DENY capabilities, whereas + * site inclusions are marked as permissions with ALLOW capabilities. + * + * While permissions related UI (eg. panels and dialogs) already handle changes to ipp-vpn, + * the intention of this class is to abstract methods for updating ipp-vpn as needed + * from other non-permissions related UI. */ class ExceptionsManager { #inited = false; - #exclusions = null; - #inclusions = null; #mode = MODE.ALL; /** - * The set of domains to exclude from the VPN. - * - * @returns {Set<string>} - * A set of domain names as strings. - * - * @example - * Set { "https://www.example.com", "https://www.bbc.co.uk" } - */ - get exclusions() { - if (!this.#exclusions || !(this.#exclusions instanceof Set)) { - this.#exclusions = new Set(); - } - return this.#exclusions; - } - - /** - * The set of domains to include for VPN protection. - * - * @returns {Set<string>} - * A set of domain names as strings. - * - * @example - * Set { "https://www.example.com", "https://www.bbc.co.uk" } - */ - get inclusions() { - if (!this.#inclusions || !(this.#inclusions instanceof Set)) { - this.#inclusions = new Set(); - } - return this.#inclusions; - } - - /** * The type of site exceptions for VPN. + * Valid types are "all" and "select". + * + * @returns {"all" | "select"} + * The site exception type. * * @see MODE */ @@ -78,13 +45,7 @@ class ExceptionsManager { return; } - this.#mode = this.#getModePref(); - - this.#exclusions = new Set(); - this.#inclusions = new Set(); - this.#loadExceptionsForPref(EXCLUSIONS_PREF); - this.#loadExceptionsForPref(INCLUSIONS_PREF); - + this.#mode = this.exceptionsMode; this.#inited = true; } @@ -93,224 +54,74 @@ class ExceptionsManager { return; } - this.#unloadExceptions(); this.#inited = false; } - #getModePref() { - let modePrefVal; - - try { - modePrefVal = this.exceptionsMode; - } catch (e) { - lazy.logConsole.error( - `Unable to read pref ${MODE_PREF}. Falling back to default.` - ); - return MODE.ALL; - } - - if (typeof modePrefVal !== "string") { - lazy.logConsole.error( - `Mode ${modePrefVal} is not a string. Falling back to default.` - ); - return MODE.ALL; - } - - return modePrefVal; + onModeUpdate() { + this.#mode = this.exceptionsMode; } /** - * Changes the protection exceptions mode. + * If mode is MODE.ALL, adds a new principal to ipp-vpn with DENY capability. + * If mode is MODE.SELECT, adds a new principal to ipp-vpn with ALLOW capability. * - * @param {"all"|"select"} newMode - * The type of exceptions + * @param {nsIPrincipal} principal + * The principal that we want to add as a site exception. * * @see MODE */ - changeMode(newMode) { - if (!Object.values(MODE).includes(newMode)) { - lazy.logConsole.error( - `Invalid mode ${newMode} found. Falling back to default.` - ); - newMode = MODE.ALL; - } - - this.#mode = newMode; - this.#updateModePref(); - } - - /** - * Updates the value of browser.ipProtection.exceptionsMode - * according to the current mode property. - */ - #updateModePref() { - Services.prefs.setStringPref(MODE_PREF, this.#mode); - } - - #getExceptionPref(pref) { - let prefString; - - if (pref === EXCLUSIONS_PREF) { - prefString = this.domainExclusions; - } else if (pref === INCLUSIONS_PREF) { - prefString = this.domainInclusions; - } else { - lazy.logConsole.error(`Invalid pref ${pref} found.`); - return ""; - } - - if (typeof prefString !== "string") { - lazy.logConsole.error(`${prefString} is not a string`); - return ""; - } - - return prefString; - } - - /** - * Initializes the exclusions set with domains from - * browser.ipProtection.domainExclusions, or - * initializes the inclusions set with domains from - * browser.ipProtection.domainInclusions. - * - * @param {string} pref - * The exclusions pref (browser.ipProtection.domainExclusions) - * or inclusions pref (browser.ipProtection.domainInclusions). - * @see exclusions - * @see inclusions - */ - #loadExceptionsForPref(pref) { - let prefString = this.#getExceptionPref(pref); - - if (!prefString) { - return; - } - - let domains = prefString.trim().split(","); - - for (let domain of domains) { - if (!this.#canCreateURI(domain)) { - continue; - } - - let uri = Services.io.newURI(domain); - - if (pref === EXCLUSIONS_PREF) { - this.#exclusions.add(uri.prePath); - } else if (pref === INCLUSIONS_PREF) { - this.#inclusions.add(uri.prePath); - } - } - } - - /** - * Checks if we can add a domain as an exclusion or inclusion for VPN usage. - * - * @param {string} domain - * The domain name. - * @returns {boolean} - * True if a URI can be created from the domain string. - * Else false. - */ - #canCreateURI(domain) { - try { - return !!Services.io.newURI(domain); - } catch (e) { - lazy.logConsole.error(e); - return false; - } - } - - /** - * If mode is MODE.ALL, adds a new domain the exclusions set if the domain is valid. - * - * @param {string} domain - * The domain to add to the exclusions or inclusions set. - * - * @see MODE - * @see exclusions - */ - addException(domain) { - // TODO: to be called by IPProtectionPanel or other classes (Bug 1990975, Bug 1990972) + addException(principal) { if (this.#mode === MODE.ALL) { - this.#addExclusion(domain); + this.#addExclusionFromPrincipal(principal); } else if (this.#mode === MODE.SELECT) { - this.#addInclusion(domain); + this.#addInclusionFromPrincipal(principal); } } - #addExclusion(domain) { - if (!this.#canCreateURI(domain)) { - return; - } - - this.#exclusions.add(domain); - this.#updateExclusionPref(); + #addExclusionFromPrincipal(principal) { + Services.perms.addFromPrincipal( + principal, + PERM_NAME, + Ci.nsIPermissionManager.DENY_ACTION + ); } - #addInclusion(domain) { - if (!this.#canCreateURI(domain)) { - return; - } - - this.#inclusions.add(domain); - this.#updateInclusionPref(); + #addInclusionFromPrincipal(principal) { + Services.perms.addFromPrincipal( + principal, + PERM_NAME, + Ci.nsIPermissionManager.ALLOW_ACTION + ); } /** - * If mode is MODE.ALL, removes a domain from the exclusions set. + * Removes an existing principal from ipp-vpn. * - * @param {string} domain - * The domain to remove from the exclusions or inclusions set. + * @param {nsIPrincipal} principal + * The principal that we want to remove as a site exception. * * @see MODE - * @see exclusions */ - removeException(domain) { - // TODO: to be called by IPProtectionPanel or other classes (Bug 1990975, Bug 1990972) - if (this.#mode === MODE.ALL) { - this.#removeExclusion(domain); - } else if (this.#mode === MODE.SELECT) { - this.#removeInclusion(domain); - } - } - - #removeExclusion(domain) { - if (this.#exclusions.delete(domain)) { - this.#updateExclusionPref(); - } - } - - #removeInclusion(domain) { - if (this.#inclusions.delete(domain)) { - this.#updateInclusionPref(); - } - } - - /** - * Updates the value of browser.ipProtection.domainExclusions - * according to the latest version of the exclusions set. - */ - #updateExclusionPref() { - let newPrefString = [...this.#exclusions].join(","); - Services.prefs.setStringPref(EXCLUSIONS_PREF, newPrefString); + removeException(principal) { + Services.perms.removeFromPrincipal(principal, PERM_NAME); } /** - * Updates the value of browser.ipProtection.domainInclusions - * according to the latest version of the inclusions set. - */ - #updateInclusionPref() { - let newPrefString = [...this.#inclusions].join(","); - Services.prefs.setStringPref(INCLUSIONS_PREF, newPrefString); - } - - /** - * Clear the exclusions set. + * Get the permission object for a site exception if it is in ipp-vpn. + * + * @param {nsIPrincipal} principal + * The principal that we want to check is saved in ipp-vpn. + * + * @returns {nsIPermission} + * The permission object for a site exception, or null if unavailable. */ - #unloadExceptions() { - this.#exclusions = null; - this.#inclusions = null; + getExceptionPermissionObject(principal) { + let permission = Services.perms.getPermissionObject( + principal, + PERM_NAME, + true /* exactHost */ + ); + return permission; } } @@ -318,23 +129,10 @@ const IPPExceptionsManager = new ExceptionsManager(); XPCOMUtils.defineLazyPreferenceGetter( IPPExceptionsManager, - "domainExclusions", - EXCLUSIONS_PREF, - "" -); - -XPCOMUtils.defineLazyPreferenceGetter( - IPPExceptionsManager, - "domainInclusions", - INCLUSIONS_PREF, - "" -); - -XPCOMUtils.defineLazyPreferenceGetter( - IPPExceptionsManager, "exceptionsMode", MODE_PREF, - MODE.ALL + MODE.ALL, + () => IPPExceptionsManager.onModeUpdate() ); export { IPPExceptionsManager }; diff --git a/browser/components/ipprotection/tests/browser/browser.toml b/browser/components/ipprotection/tests/browser/browser.toml @@ -14,6 +14,8 @@ prefs = [ ["browser_IPProtectionService.js"] +["browser_exceptions_dialog.js"] + ["browser_guardian_client.js"] ["browser_ipprotection_content.js"] diff --git a/browser/components/ipprotection/tests/browser/browser_exceptions_dialog.js b/browser/components/ipprotection/tests/browser/browser_exceptions_dialog.js @@ -0,0 +1,203 @@ +/* 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/. */ + +"use strict"; + +const { IPPExceptionsManager } = ChromeUtils.importESModule( + "resource:///modules/ipprotection/IPPExceptionsManager.sys.mjs" +); + +const MODE_PREF = "browser.ipProtection.exceptionsMode"; +const ALL_MODE = "all"; +const SELECT_MODE = "select"; + +const PERM_NAME = "ipp-vpn"; + +/** + * Opens the ipp-vpn permission dialog with a set capabilityFilter. + * + * @param {Browser} browser + * The current browser instance. + * @param {1 | 2} capabilityFilter + * Ci.nsIPermissionManager.DENY_ACTION or Ci.nsIPermissionManager.ALLOW_ACTION + * @returns {Window} The dialog window. + */ +async function openDialog(browser, capabilityFilter) { + let params = { + hideStatusColumn: true, + prefilledHost: "", + permissionType: PERM_NAME, + capabilityFilter, + }; + + let dialogLoaded = TestUtils.topicObserved("subdialog-loaded"); + + browser.contentWindow.gSubDialog.open( + "chrome://browser/content/preferences/dialogs/permissions.xhtml", + { features: "resizable=yes" }, + params + ); + + let [dialogWin] = await dialogLoaded; + return dialogWin; +} + +function addMockPrincipalToIPPVPN(site, capability) { + let principal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(site); + Services.perms.addFromPrincipal(principal, PERM_NAME, capability); +} + +/** + * If MODE_PREF === "all", adds 2 exclusions and 1 inclusion. + * If MODE === "select", adds 2 inclusions and 1 exclusion. + * + * Once permissions have been set up, return an array of site origins + * that we expect to be displayed in the dialog. + * Eg. If our mode is "all", we expect the 2 site origins to be displayed + * when our dialog is filtered to show exclusions only. + * + * @param {"all" | "select"} mode + * The site exceptions mode + * @returns {Array<string>} + * An array of site origins that we expect to be displayed in the dialog + * + * @see MODE_PREF + */ +function setupTestExceptions(mode) { + const site1 = "https://www.example.com"; + const site2 = "https://www.another.example.com"; + const site3 = "https://www.shouldbeignored.example.org"; + + let expectedCapability; + let unexpectedCapability; + + if (mode === ALL_MODE) { + expectedCapability = Ci.nsIPermissionManager.DENY_ACTION; + unexpectedCapability = Ci.nsIPermissionManager.ALLOW_ACTION; + } else { + expectedCapability = Ci.nsIPermissionManager.ALLOW_ACTION; + unexpectedCapability = Ci.nsIPermissionManager.DENY_ACTION; + } + + // Let's add 2 exclusions if mode === "all", OR 2 inclusions if mode === "select" + addMockPrincipalToIPPVPN(site1, expectedCapability); + addMockPrincipalToIPPVPN(site2, expectedCapability); + + // And let's add 1 inclusion if mode === "all", OR 2 exceptions if mode === "select" + addMockPrincipalToIPPVPN(site3, unexpectedCapability); + + // Before we test our dialog, let's double check that exceptions were loaded correctly. + let expectedExceptions = Services.perms + .getAllByTypes([PERM_NAME]) + .filter(perm => perm.capability === expectedCapability); + let unexpectedExceptions = Services.perms + .getAllByTypes([PERM_NAME]) + .filter(perm => perm.capability === unexpectedCapability); + + let expectedTypeName = mode === ALL_MODE ? "exclusions" : "inclusions"; + let unexpectedTypeName = mode === ALL_MODE ? "inclusion" : "exclusion"; + + Assert.equal( + expectedExceptions.length, + 2, + `There should be 2 ${expectedTypeName}` + ); + Assert.equal( + unexpectedExceptions.length, + 1, + `There should be 1 ${unexpectedTypeName}` + ); + + return expectedExceptions.map(permission => permission.principal.prePath); +} + +/** + * Opens the permissions dialog for ipp-vpn permissions with a capabilityFilter. + * If our filter is set to DENY, then tests that only site exclusions / principals + * with DENY capability are listed. + * If our filter is set to ALLOW, then tests that only site inclusions / principals + * with ALLOW capability are listed. + * + * @param {Array<string>} expectedSites + * An array of site origins that we expect to be displayed in the dialog. + * @param {1 | 2} capabilityFilter + * Ci.nsIPermissionManager.DENY_ACTION or Ci.nsIPermissionManager.ALLOW_ACTION + */ +async function testExceptionsInDialog(expectedSites, capabilityFilter) { + await BrowserTestUtils.withNewTab("about:preferences", async browser => { + let dialog = await openDialog(browser, capabilityFilter); + Assert.ok(dialog, "Dialog was found"); + + let permissionsBox = dialog.document.getElementById("permissionsBox"); + + // Wait for all exceptions to load + await BrowserTestUtils.waitForMutationCondition( + permissionsBox, + { childList: true, subtree: true }, + () => { + return permissionsBox.itemCount === 2; + } + ); + + let numberOfExceptions = permissionsBox.itemCount; + Assert.equal( + numberOfExceptions, + 2, + "There should be 2 inclusions in the dialog" + ); + + let displayedSites = Array.from(permissionsBox.children).map(entry => + entry.getAttribute("origin") + ); + Assert.ok( + displayedSites.includes(expectedSites[0]), + `${expectedSites[0]} was displayed in the dialog as an exception` + ); + Assert.ok( + displayedSites.includes(expectedSites[1]), + `${expectedSites[1]} was displayed in the dialog as an exception` + ); + }); +} + +function cleanupExceptions() { + Services.perms.removeByType(PERM_NAME); +} + +/** + * Tests that we can filter the permissions dialog to only show + * exclusions. + */ +add_task(async function test_filter_dialog_exclusions_only() { + await SpecialPowers.pushPrefEnv({ + set: [[MODE_PREF, ALL_MODE]], + }); + + let exclusions = setupTestExceptions(ALL_MODE); + + const capabilityFilter = Ci.nsIPermissionManager.DENY_ACTION; + + await testExceptionsInDialog(exclusions, capabilityFilter); + + cleanupExceptions(); +}); + +/** + * Tests that we can filter the permissions dialog to only show + * inclusions. + */ +add_task(async function test_filter_dialog_inclusions_only() { + await SpecialPowers.pushPrefEnv({ + set: [[MODE_PREF, SELECT_MODE]], + }); + + let inclusions = setupTestExceptions(SELECT_MODE); + + const capabilityFilter = Ci.nsIPermissionManager.ALLOW_ACTION; + + await testExceptionsInDialog(inclusions, capabilityFilter); + + cleanupExceptions(); +}); diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager.js b/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager.js @@ -0,0 +1,211 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { IPPExceptionsManager } = ChromeUtils.importESModule( + "resource:///modules/ipprotection/IPPExceptionsManager.sys.mjs" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); + +const MODE_PREF = "browser.ipProtection.exceptionsMode"; +const ALL_MODE = "all"; +const SELECT_MODE = "select"; + +const PERM_NAME = "ipp-vpn"; + +/** + * Tests the manager can modify exclusions in ipp-vpn permission. + */ +add_task(async function test_IPPExceptionsManager_exclusions() { + const site1 = "https://www.example.com"; + const site2 = "https://www.another.example.com"; + + Services.prefs.setStringPref(MODE_PREF, ALL_MODE); + + IPPExceptionsManager.init(); + + // Make mock principals and add two exclusions + let contentPrincipal1 = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(site1); + let contentPrincpal2 = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(site2); + + // Add two exclusions + IPPExceptionsManager.addException(contentPrincipal1); + IPPExceptionsManager.addException(contentPrincpal2); + + // Verify the permission data + let permissionObj1 = + IPPExceptionsManager.getExceptionPermissionObject(contentPrincipal1); + let permissionObj2 = + IPPExceptionsManager.getExceptionPermissionObject(contentPrincpal2); + + Assert.equal( + permissionObj1?.capability, + Ci.nsIPermissionManager.DENY_ACTION, + `Permission object for ${site1} exists and has capability DENY` + ); + Assert.equal( + permissionObj2?.capability, + Ci.nsIPermissionManager.DENY_ACTION, + `Permission object for ${site2} exists and has capability DENY` + ); + + // Now remove the exceptions + IPPExceptionsManager.removeException(contentPrincipal1); + IPPExceptionsManager.removeException(contentPrincpal2); + + // Verify the permission data no longer exists + permissionObj1 = + IPPExceptionsManager.getExceptionPermissionObject(contentPrincipal1); + permissionObj2 = + IPPExceptionsManager.getExceptionPermissionObject(contentPrincpal2); + + Assert.ok(!permissionObj1, `Permission object for ${site1} no longer exists`); + Assert.ok(!permissionObj2, `Permission object for ${site2} no longer exists`); + + Services.prefs.clearUserPref(MODE_PREF); + IPPExceptionsManager.uninit(); +}); + +/** + * Tests the manager can modify inclusions in ipp-vpn permission. + */ +add_task(async function test_IPPExceptionsManager_inclusions() { + const site1 = "https://www.example.com"; + const site2 = "https://www.another.example.com"; + + Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); + + IPPExceptionsManager.init(); + + // Make mock principals and add two inclusions + let contentPrincipal1 = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(site1); + let contentPrincpal2 = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(site2); + + // Add two inclusions + IPPExceptionsManager.addException(contentPrincipal1); + IPPExceptionsManager.addException(contentPrincpal2); + + // Verify the permission data + let permissionObj1 = + IPPExceptionsManager.getExceptionPermissionObject(contentPrincipal1); + let permissionObj2 = + IPPExceptionsManager.getExceptionPermissionObject(contentPrincpal2); + + Assert.equal( + permissionObj1?.capability, + Ci.nsIPermissionManager.ALLOW_ACTION, + `Permission object for ${site1} exists and has capability ALLOW` + ); + Assert.equal( + permissionObj2?.capability, + Ci.nsIPermissionManager.ALLOW_ACTION, + `Permission object for ${site2} exists and has capability ALLOW` + ); + + // Now remove the exceptions + IPPExceptionsManager.removeException(contentPrincipal1); + IPPExceptionsManager.removeException(contentPrincpal2); + + // Verify the permission data no longer exists + permissionObj1 = + IPPExceptionsManager.getExceptionPermissionObject(contentPrincipal1); + permissionObj2 = + IPPExceptionsManager.getExceptionPermissionObject(contentPrincpal2); + + Assert.ok(!permissionObj1, `Permission object for ${site1} no longer exists`); + Assert.ok(!permissionObj2, `Permission object for ${site2} no longer exists`); + + Services.prefs.clearUserPref(MODE_PREF); + IPPExceptionsManager.uninit(); +}); + +/** + * Tests the manager correctly tracks exclusions and inclusions after + * changing mode. + */ +add_task(async function test_IPPExceptionsManager_switch_mode() { + const site1 = "https://www.example.com"; + const site2 = "https://www.another.example.com"; + + // Start with "all" mode first + Services.prefs.setStringPref(MODE_PREF, ALL_MODE); + + IPPExceptionsManager.init(); + + // Add an exclusion + let contentPrincipal1 = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(site1); + IPPExceptionsManager.addException(contentPrincipal1); + + // Switch mode to "select" + let prefChanged = TestUtils.waitForPrefChange(MODE_PREF); + Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); + await prefChanged; + + // Now add an inclusion + let contentPrincipal2 = + Services.scriptSecurityManager.createContentPrincipalFromOrigin(site2); + IPPExceptionsManager.addException(contentPrincipal2); + + // Ensure exception types were preserved and that we have the right number of exceptions + let savedSites = Services.perms.getAllByTypes([PERM_NAME]); + Assert.equal( + savedSites.length, + 2, + `There should be only 2 site exceptions in ${PERM_NAME}` + ); + + let exclusions = Services.perms + .getAllByTypes([PERM_NAME]) + .filter(perm => perm.capability == Ci.nsIPermissionManager.DENY_ACTION); + let inclusions = Services.perms + .getAllByTypes([PERM_NAME]) + .filter(perm => perm.capability == Ci.nsIPermissionManager.ALLOW_ACTION); + Assert.equal( + exclusions.length, + 1, + `There should be 1 exclusion in ${PERM_NAME}` + ); + Assert.equal( + inclusions.length, + 1, + `There should be 1 inclusion in ${PERM_NAME}` + ); + + // Now let's try adding the same principal for the excluded site as an INCLUSION instead + IPPExceptionsManager.addException(contentPrincipal1); + + savedSites = Services.perms.getAllByTypes([PERM_NAME]); + Assert.equal( + savedSites.length, + 2, + `There should still be 2 site exceptions in ${PERM_NAME}` + ); + + exclusions = Services.perms + .getAllByTypes([PERM_NAME]) + .filter(perm => perm.capability == Ci.nsIPermissionManager.DENY_ACTION); + inclusions = Services.perms + .getAllByTypes([PERM_NAME]) + .filter(perm => perm.capability == Ci.nsIPermissionManager.ALLOW_ACTION); + Assert.equal( + exclusions.length, + 0, + `There should be 0 exclusions now in ${PERM_NAME}` + ); + Assert.equal( + inclusions.length, + 2, + `There should be 2 inclusions now in ${PERM_NAME}` + ); + + Services.prefs.clearUserPref(MODE_PREF); + IPPExceptionsManager.uninit(); +}); diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_exclusions.js b/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_exclusions.js @@ -1,192 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. -https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -const { IPPExceptionsManager } = ChromeUtils.importESModule( - "resource:///modules/ipprotection/IPPExceptionsManager.sys.mjs" -); - -const MODE_PREF = "browser.ipProtection.exceptionsMode"; -const EXCLUSIONS_PREF = "browser.ipProtection.domainExclusions"; - -const ALL_MODE = "all"; - -/** - * Tests manager initialization when there are no exclusions. - */ -add_task(async function test_IPPExceptionsManager_init_with_no_exclusions() { - const stringPref = ""; - - Services.prefs.setStringPref(MODE_PREF, ALL_MODE); - Services.prefs.setStringPref(EXCLUSIONS_PREF, stringPref); - - IPPExceptionsManager.init(); - - Assert.ok(IPPExceptionsManager.exclusions, "exclusions set found"); - Assert.ok( - !IPPExceptionsManager.exclusions.size, - "exclusions set should be empty" - ); - - let newStringPref = Services.prefs.getStringPref(EXCLUSIONS_PREF); - - Assert.ok(!newStringPref, "String pref should be empty"); - - Services.prefs.clearUserPref(MODE_PREF); - Services.prefs.clearUserPref(EXCLUSIONS_PREF); - - IPPExceptionsManager.uninit(); -}); - -/** - * Tests the manager initialization with registered exclusions. - */ -add_task(async function test_IPPExceptionsManager_init_with_exclusions() { - const site1 = "https://www.example.com"; - const site2 = "https://www.example.org"; - const site3 = "https://www.another.example.ca"; - const stringPref = `${site1},${site2},${site3}`; - - Services.prefs.setStringPref(MODE_PREF, ALL_MODE); - Services.prefs.setStringPref(EXCLUSIONS_PREF, stringPref); - - IPPExceptionsManager.init(); - - Assert.ok(IPPExceptionsManager.exclusions, "exclusions set found"); - Assert.equal( - IPPExceptionsManager.exclusions.size, - 3, - "exclusions set should have 3 domains" - ); - - Assert.ok( - IPPExceptionsManager.exclusions.has(site1), - `exclusions set should include ${site1}` - ); - Assert.ok( - IPPExceptionsManager.exclusions.has(site2), - `exclusions set should include ${site2}` - ); - Assert.ok( - IPPExceptionsManager.exclusions.has(site3), - `exclusions set should include ${site3}` - ); - - let newStringPref = Services.prefs.getStringPref(EXCLUSIONS_PREF); - - Assert.ok(newStringPref.includes(site1), `String pref includes ${site1}`); - Assert.ok(newStringPref.includes(site2), `String pref includes ${site2}`); - Assert.ok(newStringPref.includes(site3), `String pref includes ${site3}`); - - Services.prefs.clearUserPref(MODE_PREF); - Services.prefs.clearUserPref(EXCLUSIONS_PREF); - IPPExceptionsManager.uninit(); -}); - -/** - * Tests the manager initialization with an invalid pref string for exclusions. - */ -add_task( - async function test_IPPExceptionsManager_init_with_invalid_exclusions() { - const invalidStringPref = "noturl"; - - Services.prefs.setStringPref(MODE_PREF, ALL_MODE); - Services.prefs.setStringPref(EXCLUSIONS_PREF, invalidStringPref); - - IPPExceptionsManager.init(); - - Assert.ok(IPPExceptionsManager.exclusions, "exclusions set found"); - Assert.ok( - !IPPExceptionsManager.exclusions.size, - "exclusions set should have 0 valid domains" - ); - - Services.prefs.clearUserPref(MODE_PREF); - Services.prefs.clearUserPref(EXCLUSIONS_PREF); - IPPExceptionsManager.uninit(); - } -); - -/** - * Tests that we can add valid domains to the exclusions set. - */ -add_task(async function test_IPPExceptionsManager_add_exclusions() { - const site1 = "https://www.example.com"; - - Services.prefs.setStringPref(MODE_PREF, ALL_MODE); - Services.prefs.setStringPref(EXCLUSIONS_PREF, site1); - - IPPExceptionsManager.init(); - - Assert.ok(IPPExceptionsManager.exclusions, "exclusions set found"); - - const validSite = "https://www.another.example.com"; - const dupeSite = site1; - const invalidSite = "noturl"; - - IPPExceptionsManager.addException(validSite); - IPPExceptionsManager.addException(dupeSite); - IPPExceptionsManager.addException(invalidSite); - IPPExceptionsManager.addException(null); - IPPExceptionsManager.addException(undefined); - - Assert.equal( - IPPExceptionsManager.exclusions.size, - 2, - "exclusions set should only have 2 domains" - ); - - Assert.ok( - IPPExceptionsManager.exclusions.has(site1), - `exclusions set should include ${site1}` - ); - Assert.ok( - IPPExceptionsManager.exclusions.has(validSite), - `exclusions set should include ${validSite}` - ); - - let newStringPref = Services.prefs.getStringPref(EXCLUSIONS_PREF); - - Assert.ok(newStringPref.includes(site1), `String pref includes ${site1}`); - Assert.ok( - newStringPref.includes(validSite), - `String pref includes ${validSite}` - ); - - Services.prefs.clearUserPref(MODE_PREF); - Services.prefs.clearUserPref(EXCLUSIONS_PREF); - IPPExceptionsManager.uninit(); -}); - -/** - * Tests that we can remove domains from the exclusions set. - */ -add_task(async function test_IPPExceptionsManager_remove_exclusions() { - const site1 = "https://www.example.com"; - - Services.prefs.setStringPref(MODE_PREF, ALL_MODE); - Services.prefs.setStringPref(EXCLUSIONS_PREF, site1); - - IPPExceptionsManager.init(); - - Assert.ok(IPPExceptionsManager.exclusions, "exclusions set found"); - - const invalidSite = "urlDoesntExist"; - - IPPExceptionsManager.removeException(site1); - IPPExceptionsManager.removeException(invalidSite); - - Assert.ok( - !IPPExceptionsManager.exclusions.size, - "exclusions set should be empty" - ); - - let newStringPref = Services.prefs.getStringPref(EXCLUSIONS_PREF); - - Assert.ok(!newStringPref, "String pref should be empty"); - - Services.prefs.clearUserPref(MODE_PREF); - Services.prefs.clearUserPref(EXCLUSIONS_PREF); - IPPExceptionsManager.uninit(); -}); diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_inclusions.js b/browser/components/ipprotection/tests/xpcshell/test_IPPExceptionsManager_inclusions.js @@ -1,182 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. -https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -const { IPPExceptionsManager } = ChromeUtils.importESModule( - "resource:///modules/ipprotection/IPPExceptionsManager.sys.mjs" -); - -const MODE_PREF = "browser.ipProtection.exceptionsMode"; -const INCLUSIONS_PREF = "browser.ipProtection.domainInclusions"; - -const SELECT_MODE = "select"; - -/** - * Tests manager initialization when there are no inclusions. - */ -add_task(async function test_IPPExceptionsManager_init_with_no_inclusions() { - const stringPref = ""; - - Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); - Services.prefs.setStringPref(INCLUSIONS_PREF, stringPref); - - IPPExceptionsManager.init(); - - Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found"); - Assert.ok( - !IPPExceptionsManager.inclusions.size, - "inclusions set should be empty" - ); - - Services.prefs.clearUserPref(MODE_PREF); - Services.prefs.clearUserPref(INCLUSIONS_PREF); - - IPPExceptionsManager.uninit(); -}); - -/** - * Tests the manager initialization with registered inclusions. - */ -add_task(async function test_IPPExceptionsManager_init_with_inclusions() { - const site1 = "https://www.example.com"; - const site2 = "https://www.example.org"; - const site3 = "https://www.another.example.ca"; - const stringPref = `${site1},${site2},${site3}`; - - Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); - Services.prefs.setStringPref(INCLUSIONS_PREF, stringPref); - - IPPExceptionsManager.init(); - - Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found"); - Assert.equal( - IPPExceptionsManager.inclusions.size, - 3, - "inclusions set should have 3 domains" - ); - - Assert.ok( - IPPExceptionsManager.inclusions.has(site1), - `inclusions set should include ${site1}` - ); - Assert.ok( - IPPExceptionsManager.inclusions.has(site2), - `inclusions set should include ${site2}` - ); - Assert.ok( - IPPExceptionsManager.inclusions.has(site3), - `inclusions set should include ${site3}` - ); - - Services.prefs.clearUserPref(MODE_PREF); - Services.prefs.clearUserPref(INCLUSIONS_PREF); - IPPExceptionsManager.uninit(); -}); - -/** - * Tests the manager initialization with an invalid pref string for inclusions. - */ -add_task( - async function test_IPPExceptionsManager_init_with_invalid_inclusions() { - const invalidStringPref = "noturl"; - - Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); - Services.prefs.setStringPref(INCLUSIONS_PREF, invalidStringPref); - - IPPExceptionsManager.init(); - - Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found"); - Assert.ok( - !IPPExceptionsManager.inclusions.size, - "inclusions set should have 0 valid domains" - ); - - Services.prefs.clearUserPref(MODE_PREF); - Services.prefs.clearUserPref(INCLUSIONS_PREF); - IPPExceptionsManager.uninit(); - } -); - -/** - * Tests that we can add valid domains to the inclusions set. - */ -add_task(async function test_IPPExceptionsManager_add_inclusions() { - const site1 = "https://www.example.com"; - - Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); - Services.prefs.setStringPref(INCLUSIONS_PREF, site1); - - IPPExceptionsManager.init(); - - Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found"); - - const validSite = "https://www.another.example.com"; - const dupeSite = site1; - const invalidSite = "noturl"; - - IPPExceptionsManager.addException(validSite); - IPPExceptionsManager.addException(dupeSite); - IPPExceptionsManager.addException(invalidSite); - IPPExceptionsManager.addException(null); - IPPExceptionsManager.addException(undefined); - - Assert.equal( - IPPExceptionsManager.inclusions.size, - 2, - "inclusions set should only have 2 domains" - ); - - Assert.ok( - IPPExceptionsManager.inclusions.has(site1), - `inclusions set should include ${site1}` - ); - Assert.ok( - IPPExceptionsManager.inclusions.has(validSite), - `inclusions set should include ${validSite}` - ); - - let newStringPref = Services.prefs.getStringPref(INCLUSIONS_PREF); - - Assert.ok(newStringPref.includes(site1), `String pref includes ${site1}`); - Assert.ok( - newStringPref.includes(validSite), - `String pref includes ${validSite}` - ); - - Services.prefs.clearUserPref(MODE_PREF); - Services.prefs.clearUserPref(INCLUSIONS_PREF); - IPPExceptionsManager.uninit(); -}); - -/** - * Tests that we can remove domains from the inclusions set. - */ -add_task(async function test_IPPExceptionsManager_remove_inclusions() { - const site1 = "https://www.example.com"; - - Services.prefs.setStringPref(MODE_PREF, SELECT_MODE); - Services.prefs.setStringPref(INCLUSIONS_PREF, site1); - - IPPExceptionsManager.init(); - - Assert.ok(IPPExceptionsManager.inclusions, "inclusions set found"); - - const invalidSite = "urlDoesntExist"; - - IPPExceptionsManager.removeException(site1); - IPPExceptionsManager.removeException(invalidSite); - - Assert.ok( - !IPPExceptionsManager.inclusions.size, - "inclusions set should be empty" - ); - - let newStringPref = Services.prefs.getStringPref(INCLUSIONS_PREF); - - Assert.ok(!newStringPref, "String pref should be empty"); - - Services.prefs.clearUserPref(MODE_PREF); - Services.prefs.clearUserPref(INCLUSIONS_PREF); - IPPExceptionsManager.uninit(); -}); diff --git a/browser/components/ipprotection/tests/xpcshell/xpcshell.toml b/browser/components/ipprotection/tests/xpcshell/xpcshell.toml @@ -9,9 +9,7 @@ prefs = [ ["test_GuardianClient.js"] -["test_IPPExceptionsManager_exclusions.js"] - -["test_IPPExceptionsManager_inclusions.js"] +["test_IPPExceptionsManager.js"] ["test_IPPStartupCache.js"] diff --git a/browser/components/preferences/dialogs/permissions.js b/browser/components/preferences/dialogs/permissions.js @@ -44,6 +44,10 @@ const permissionExceptionsL10n = { window: "permissions-exceptions-addons-window2", description: "permissions-exceptions-addons-desc", }, + "ipp-vpn": { + window: "ip-protection-exceptions-dialog-window", + description: "ip-protection-exclusions-desc", + }, }; function Permission(principal, type, capability) { @@ -64,6 +68,7 @@ var gPermissionManager = { _removeButton: null, _removeAllButton: null, _forcedHTTP: null, + _capabilityFilter: null, onLoad() { let params = window.arguments[0]; @@ -80,6 +85,7 @@ var gPermissionManager = { * @param {boolean} params.disableETPVisible Display the "Add Exception" button in the dialog (Only for ETP permissions) * @param {boolean} params.hideStatusColumn Hide the "Status" column in the dialog * @param {boolean} params.forcedHTTP Save inputs whose URI has a HTTPS scheme with a HTTP scheme (Used by HTTPS-Only) + * @param {number} params.capabilityFilter Display permissions that have the specified capability only. See Ci.nsIPermissionManager. */ async init(params) { if (!this._isObserving) { @@ -101,9 +107,30 @@ var gPermissionManager = { this._btnHttpsOnlyOff = document.getElementById("btnHttpsOnlyOff"); this._btnHttpsOnlyOffTmp = document.getElementById("btnHttpsOnlyOffTmp"); + this._capabilityFilter = params.capabilityFilter; + let permissionsText = document.getElementById("permissionsText"); - let l10n = permissionExceptionsL10n[this._type]; + let l10n; + + // For ipp-vpn, we want to override strings based on capability. + // Valid capabilities for this permission are ALLOW and DENY. + if (this._type === "ipp-vpn") { + if (params.capabilityFilter === Ci.nsIPermissionManager.ALLOW_ACTION) { + l10n = { + window: "ip-protection-exceptions-dialog-window", + description: "ip-protection-exclusions-desc", + }; + } else { + l10n = { + window: "ip-protection-exceptions-dialog-window", + description: "ip-protection-inclusions-desc", + }; + } + } else { + l10n = permissionExceptionsL10n[this._type]; + } + document.l10n.setAttributes(permissionsText, l10n.description); document.l10n.setAttributes(document.documentElement, l10n.window); @@ -330,6 +357,12 @@ var gPermissionManager = { return; } + // If filtering is enabled, don't bother showing permissions that don't have + // the capability we want. + if (this._capabilityFilter && perm.capability !== this._capabilityFilter) { + return; + } + // Skip private browsing session permissions. if ( perm.principal.privateBrowsingId !== diff --git a/browser/components/preferences/dialogs/permissions.xhtml b/browser/components/preferences/dialogs/permissions.xhtml @@ -36,6 +36,7 @@ rel="localization" href="browser/preferences/permissions.ftl" /> + <html:link rel="localization" href="preview/ipProtection.ftl" /> </linkset> <script src="chrome://browser/content/preferences/dialogs/permissions.js" /> diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js @@ -1265,9 +1265,19 @@ Preferences.addSetting({ 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 + let params = { + blockVisible: true, + hideStatusColumn: true, + prefilledHost: "", + permissionType: "ipp-vpn", + capabilityFilter: Ci.nsIPermissionManager.DENY_ACTION, + }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/permissions.xhtml", + { features: "resizable=yes" }, + params + ); }, }); Preferences.addSetting({ @@ -1276,9 +1286,19 @@ Preferences.addSetting({ 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 + let params = { + allowVisible: true, + hideStatusColumn: true, + prefilledHost: "", + permissionType: "ipp-vpn", + capabilityFilter: Ci.nsIPermissionManager.ALLOW_ACTION, + }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/permissions.xhtml", + { features: "resizable=yes" }, + params + ); }, }); Preferences.addSetting({ diff --git a/browser/locales-preview/ipProtection.ftl b/browser/locales-preview/ipProtection.ftl @@ -110,4 +110,11 @@ ip-protection-site-exceptions-select-sites-button = .label = { -firefox-vpn-brand-name } is on for these websites .description = No websites added yet +## IP Protection dialogs + +ip-protection-exceptions-dialog-window = + .title = Where to use { -firefox-vpn-brand-name } +ip-protection-exclusions-desc = Use VPN for all websites except ones on this list. +ip-protection-inclusions-desc = Use VPN for only select websites. + ##