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