tor-browser

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

commit 40d6f18bfdc90b8dac1a5fdcca43fab4fc19b7c2
parent 1d6bb2199f322ff1544ca2a346c10f9473faaf9c
Author: Rob Wu <rob@robwu.nl>
Date:   Thu,  9 Oct 2025 01:59:36 +0000

Bug 1992179 - Do not suggest to "enable" when when extension cannot be enabled r=rpl,fluent-reviewers,bolsson

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

Diffstat:
Mbrowser/base/content/browser-addons.js | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mbrowser/components/extensions/test/browser/browser_unified_extensions_empty_panel.js | 77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mbrowser/locales/en-US/browser/unifiedExtensions.ftl | 1+
3 files changed, 121 insertions(+), 17 deletions(-)

diff --git a/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js @@ -10,6 +10,7 @@ var { XPCOMUtils } = ChromeUtils.importESModule( const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs", AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs", ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs", @@ -2298,9 +2299,44 @@ var gUnifiedExtensions = { return policies.some(p => !p.privateBrowsingAllowed); }, - async isAtLeastOneExtensionDisabled() { + /** + * Returns whether there is any active extension without private browsing + * access, for which the user can toggle the "Run in Private Windows" option. + * This complements the isPrivateWindowMissingExtensionsWithoutPBMAccess() + * method, by distinguishing cases where the user can enable any extension + * in the private window, vs cases where the user cannot. + * + * @returns {Promise<boolean>} Whether there is any "Run in Private Windows" + * option that is Off and can be set to On. + */ + async isAtLeastOneExtensionWithPBMOptIn() { const addons = await AddonManager.getAddonsByTypes(["extension"]); - return addons.some(a => !a.hidden && !a.isActive); + return addons.some(addon => { + if ( + // We only care about extensions shown in the panel and about:addons. + addon.hidden || + // We only care about extensions whose PBM access can be toggled. + !( + addon.permissions & + lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS + ) + ) { + return false; + } + const policy = WebExtensionPolicy.getByID(addon.id); + // policy can be null if the extension is not active. + return policy && !policy.privateBrowsingAllowed; + }); + }, + + async getDisabledExtensionsInfo() { + let addons = await AddonManager.getAddonsByTypes(["extension"]); + addons = addons.filter(a => !a.hidden && !a.isActive); + const isAnyDisabled = !!addons.length; + const isAnyEnableable = addons.some( + a => a.permissions & lazy.AddonManager.PERM_CAN_ENABLE + ); + return { isAnyDisabled, isAnyEnableable }; }, handleEvent(event) { @@ -2394,17 +2430,29 @@ var gUnifiedExtensions = { "unified-extensions-empty-content-explain-enable" ); emptyStateBox.hidden = false; + this.isAtLeastOneExtensionWithPBMOptIn().then(result => { + // The "enable" message is somewhat misleading when the user cannot + // enable the extension, show a generic message instead (bug 1992179). + if (!result) { + document.l10n.setAttributes( + emptyStateBox.querySelector("description"), + "unified-extensions-empty-content-explain-manage" + ); + } + }); } else { emptyStateBox.hidden = true; - this.isAtLeastOneExtensionDisabled().then(result => { - if (result) { + this.getDisabledExtensionsInfo().then(disabledExtensionsInfo => { + if (disabledExtensionsInfo.isAnyDisabled) { document.l10n.setAttributes( emptyStateBox.querySelector("h2"), "unified-extensions-empty-reason-extension-not-enabled" ); document.l10n.setAttributes( emptyStateBox.querySelector("description"), - "unified-extensions-empty-content-explain-enable" + disabledExtensionsInfo.isAnyEnableable + ? "unified-extensions-empty-content-explain-enable" + : "unified-extensions-empty-content-explain-manage" ); emptyStateBox.hidden = false; } else if (!policies.length) { @@ -2658,7 +2706,7 @@ var gUnifiedExtensions = { policies.length && !this.hasExtensionsInPanel(policies) && !this.isPrivateWindowMissingExtensionsWithoutPBMAccess() && - !(await this.isAtLeastOneExtensionDisabled()) + !(await this.getDisabledExtensionsInfo()).isAnyDisabled ) { // This may happen if the user has pinned all of their extensions. // In that case, the extensions panel is empty. diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_empty_panel.js b/browser/components/extensions/test/browser/browser_unified_extensions_empty_panel.js @@ -36,14 +36,17 @@ add_setup(async function () { // the behavior when there are no extensions to render in the list. // Temporarily fake-hide these extensions to ensure that we start with zero // extensions from the test's POV. - function fakeHideExtension(extensionId) { + async function fakeHideExtension(extensionId) { const { extension } = WebExtensionPolicy.getByID(extensionId); // This shadows ExtensionData.isHidden of the Extension subclass, causing // gUnifiedExtensions.getActivePolicies() to ignore the extension. sandbox.stub(extension, "isHidden").get(() => true); + + const addon = await AddonManager.getAddonByID(extensionId); + sandbox.stub(addon.__AddonInternal__, "hidden").get(() => true); } - fakeHideExtension("mochikit@mozilla.org"); - fakeHideExtension("special-powers@mozilla.org"); + await fakeHideExtension("mochikit@mozilla.org"); + await fakeHideExtension("special-powers@mozilla.org"); }); function getEmptyStateContainer(win) { @@ -354,6 +357,36 @@ add_task(async function test_empty_state_is_hidden_when_panel_is_non_empty() { await Promise.all(extensions.map(extension => extension.unload())); }); +// Verify empty state when private browsing permission is missing, but +// incognito:not_allowed is specified. See bug 1992179 for context. +add_task(async function test_button_click_in_pbm_and_incognito_not_allowed() { + const extensions = createExtensions([ + { name: "ext with incognito:not_allowed", incognito: "not_allowed" }, + ]); + await Promise.all(extensions.map(extension => extension.startup())); + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + // This clicks on gUnifiedExtensions.button and waits for panel to show. + await openExtensionsPanel(win); + + let emptyStateBox = getEmptyStateContainer(win); + ok(BrowserTestUtils.isVisible(emptyStateBox), "Empty state is visible"); + is( + emptyStateBox.querySelector("h2").getAttribute("data-l10n-id"), + "unified-extensions-empty-reason-private-browsing-not-allowed", + "Has header 'You have extensions installed, but not enabled in private windows'" + ); + is( + emptyStateBox.querySelector("description").getAttribute("data-l10n-id"), + "unified-extensions-empty-content-explain-manage", + "Has description pointing to Manage extensions button with text MANAGE, not ENABLE" + ); + + await BrowserTestUtils.closeWindow(win); + + await Promise.all(extensions.map(extension => extension.unload())); +}); + // Verify the behavior when there is an extension with private access but is // pinned, and an extension without private access. add_task(async function test_button_click_in_pbm_pinned_and_no_access() { @@ -475,7 +508,9 @@ add_task(async function test_no_empty_state_with_disabled_non_extension() { // Verifies that if the only add-on is disabled by blocklisting, that we still // see a panel and that the blocklist message is visible. -add_task(async function test_empty_state_with_blocklisted_addon() { +// Between hard block and soft blocks, the only difference is that soft block +// can be re-enabled, and that should be reflected in the message. +async function do_test_empty_state_with_blocklisted_addon(isSoftBlock) { const addonId = "@extension-that-is-blocked"; const addon = await promiseInstallWebExtension({ manifest: { @@ -487,7 +522,9 @@ add_task(async function test_empty_state_with_blocklisted_addon() { let promiseBlocklistAttentionUpdated = AddonTestUtils.promiseManagerEvent( "onBlocklistAttentionUpdated" ); - const cleanupBlocklist = await loadBlocklistRawData({ blocked: [addon] }); + const cleanupBlocklist = await loadBlocklistRawData({ + [isSoftBlock ? "softblocked" : "blocked"]: [addon], + }); info("Wait for onBlocklistAttentionUpdated manager listener call"); await promiseBlocklistAttentionUpdated; @@ -500,7 +537,9 @@ add_task(async function test_empty_state_with_blocklisted_addon() { Assert.deepEqual( window.document.l10n.getAttributes(messages[0]), { - id: "unified-extensions-mb-blocklist-error-single", + id: isSoftBlock + ? "unified-extensions-mb-blocklist-warning-single" + : "unified-extensions-mb-blocklist-error-single", args: { extensionName: "Name of the blocked ext", extensionsCount: 1, @@ -516,11 +555,19 @@ add_task(async function test_empty_state_with_blocklisted_addon() { "unified-extensions-empty-reason-extension-not-enabled", "Has header 'You have extensions installed, but not enabled'" ); - is( - emptyStateBox.querySelector("description").getAttribute("data-l10n-id"), - "unified-extensions-empty-content-explain-enable", - "Has description pointing to Manage extensions button." - ); + if (isSoftBlock) { + is( + emptyStateBox.querySelector("description").getAttribute("data-l10n-id"), + "unified-extensions-empty-content-explain-enable", + "Has description pointing to Manage extensions button with text ENABLE" + ); + } else { + is( + emptyStateBox.querySelector("description").getAttribute("data-l10n-id"), + "unified-extensions-empty-content-explain-manage", + "Has description pointing to Manage extensions button with text MANAGE, not ENABLE" + ); + } await closeExtensionsPanel(window); @@ -538,4 +585,12 @@ add_task(async function test_empty_state_with_blocklisted_addon() { await closeExtensionsPanel(window); await addon.uninstall(); +} + +add_task(async function test_empty_state_with_blocklisted_addon_hardblock() { + await do_test_empty_state_with_blocklisted_addon(/* isSoftBlock */ false); +}); + +add_task(async function test_empty_state_with_blocklisted_addon_softblock() { + await do_test_empty_state_with_blocklisted_addon(/* isSoftBlock */ true); }); diff --git a/browser/locales/en-US/browser/unifiedExtensions.ftl b/browser/locales/en-US/browser/unifiedExtensions.ftl @@ -16,6 +16,7 @@ unified-extensions-empty-reason-extension-not-enabled = You have extensions inst # In this headline, “Level up” means to enhance your browsing experience. unified-extensions-empty-reason-zero-extensions-onboarding = Level up your browsing with extensions unified-extensions-empty-content-explain-enable = Select “{ unified-extensions-item-message-manage }” to enable them in settings. +unified-extensions-empty-content-explain-manage = Select “{ unified-extensions-item-message-manage }” to manage them in settings. unified-extensions-empty-content-explain-extensions-onboarding = Personalize { -brand-short-name } by changing how it looks and performs or boosting privacy and safety. ## An extension in the main list