diff --git a/browser/base/content/test/webextensions/browser_extension_sideloading.js b/browser/base/content/test/webextensions/browser_extension_sideloading.js index 9eca43da0b17..affb818bf0e8 100644 --- a/browser/base/content/test/webextensions/browser_extension_sideloading.js +++ b/browser/base/content/test/webextensions/browser_extension_sideloading.js @@ -179,8 +179,8 @@ add_task(async function test_sideloading() { panel, /\/foo-icon\.png$/, [ - ["webextPerms.hostDescription.allUrls"], - ["webextPerms.description.history"], + ["webext-perms-host-description-all-urls"], + ["webext-perms-description-history"], ], kSideloaded ); @@ -229,7 +229,7 @@ add_task(async function test_sideloading() { checkNotification( panel, DEFAULT_ICON_URL, - [["webextPerms.hostDescription.allUrls"]], + [["webext-perms-host-description-all-urls"]], kSideloaded ); @@ -296,7 +296,7 @@ add_task(async function test_sideloading() { checkNotification( panel, DEFAULT_ICON_URL, - [["webextPerms.hostDescription.allUrls"]], + [["webext-perms-host-description-all-urls"]], kSideloaded ); diff --git a/browser/base/content/test/webextensions/browser_permissions_unsigned.js b/browser/base/content/test/webextensions/browser_permissions_unsigned.js index 0d2e3dcdcc22..dff7ad872e8e 100644 --- a/browser/base/content/test/webextensions/browser_permissions_unsigned.js +++ b/browser/base/content/test/webextensions/browser_permissions_unsigned.js @@ -41,12 +41,15 @@ add_task(async function test_unsigned() { let description = panel.querySelector( ".popup-notification-description" ).textContent; - checkPermissionString( - description, - "webextPerms.headerUnsignedWithPerms", - undefined, - `Install notification includes unsigned warning` - ); + const expected = formatExtValue("webext-perms-header-unsigned-with-perms", { + extension: "<>", + }); + for (let part of expected.split("<>")) { + ok( + description.includes(part), + "Install notification includes unsigned warning" + ); + } // cancel the install let promise = promiseInstallEvent({ id: ID }, "onInstallCancelled"); diff --git a/browser/base/content/test/webextensions/head.js b/browser/base/content/test/webextensions/head.js index ecd5be47665f..7514092213e3 100644 --- a/browser/base/content/test/webextensions/head.js +++ b/browser/base/content/test/webextensions/head.js @@ -26,6 +26,26 @@ const { PermissionTestUtils } = ChromeUtils.importESModule( "resource://testing-common/PermissionTestUtils.sys.mjs" ); +let extL10n = null; +/** + * @param {string} id + * @param {object} [args] + * @returns {string} + */ +function formatExtValue(id, args) { + if (!extL10n) { + extL10n = new Localization( + [ + "toolkit/global/extensions.ftl", + "toolkit/global/extensionPermissions.ftl", + "branding/brand.ftl", + ], + true + ); + } + return extL10n.formatValueSync(id, args); +} + /** * Wait for the given PopupNotification to display * @@ -185,37 +205,6 @@ function isDefaultIcon(icon) { return icon == "chrome://mozapps/skin/extensions/extensionGeneric.svg"; } -/** - * Check the contents of an individual permission string. - * This function is fairly specific to the use here and probably not - * suitable for re-use elsewhere... - * - * @param {string} string - * The string value to check (i.e., pulled from the DOM) - * @param {string} key - * The key in browser.properties for the localized string to - * compare with. - * @param {string|null} param - * Optional string to substitute for %S in the localized string. - * @param {string} msg - * The message to be emitted as part of the actual test. - */ -function checkPermissionString(string, key, param, msg) { - let localizedString = param - ? gBrowserBundle.formatStringFromName(key, [param]) - : gBrowserBundle.GetStringFromName(key); - - // If this is a parameterized string and the parameter isn't given, - // just do a simple comparison of the text before and after the %S - if (localizedString.includes("%S")) { - let i = localizedString.indexOf("%S"); - ok(string.startsWith(localizedString.slice(0, i)), msg); - ok(string.endsWith(localizedString.slice(i + 2)), msg); - } else { - is(string, localizedString, msg); - } -} - /** * Check the contents of a permission popup notification * @@ -230,7 +219,7 @@ function checkPermissionString(string, key, param, msg) { * @param {array} permissions * The expected entries in the permissions list. Each element * in this array is itself a 2-element array with the string key - * for the item (e.g., "webextPerms.description.foo") and an + * for the item (e.g., "webext-perms-description-foo") and an * optional formatting parameter. * @param {boolean} sideloaded * Whether the notification is for a sideloaded extenion. @@ -255,19 +244,17 @@ function checkNotification(panel, checkIcon, permissions, sideloaded) { let description = panel.querySelector( ".popup-notification-description" ).textContent; - let expectedDescription = "webextPerms.header"; + let descL10nId = "webext-perms-header"; if (permissions.length) { - expectedDescription += "WithPerms"; + descL10nId = "webext-perms-header-with-perms"; } if (sideloaded) { - expectedDescription = "webextPerms.sideloadHeader"; + descL10nId = "webext-perms-sideload-header"; } - checkPermissionString( - description, - expectedDescription, - undefined, - `Description is the expected one` - ); + const exp = formatExtValue(descL10nId, { extension: "<>" }).split("<>"); + ok(description.startsWith(exp.at(0)), "Description is the expected one"); + ok(description.endsWith(exp.at(-1)), "Description is the expected one"); + is( learnMoreLink.hidden, !permissions.length, @@ -293,10 +280,10 @@ function checkNotification(panel, checkIcon, permissions, sideloaded) { ); for (let i in permissions) { let [key, param] = permissions[i]; - checkPermissionString( + const expected = formatExtValue(key, param); + is( ul.children[i].textContent, - key, - param, + expected, `Permission number ${i + 1} is correct` ); } @@ -374,13 +361,19 @@ async function testInstallMethod(installFn, telemetryBase) { // path, just make sure we've got a jar url pointing to the right path // inside the jar. checkNotification(panel, /^jar:file:\/\/.*\/icon\.png$/, [ - ["webextPerms.hostDescription.wildcard", "wildcard.domain"], - ["webextPerms.hostDescription.oneSite", "singlehost.domain"], - ["webextPerms.description.nativeMessaging"], + [ + "webext-perms-host-description-wildcard", + { domain: "wildcard.domain" }, + ], + [ + "webext-perms-host-description-one-site", + { domain: "singlehost.domain" }, + ], + ["webext-perms-description-nativeMessaging"], // The below permissions are deliberately in this order as permissions // are sorted alphabetically by the permission string to match AMO. - ["webextPerms.description.history"], - ["webextPerms.description.tabs"], + ["webext-perms-description-history"], + ["webext-perms-description-tabs"], ]); } else if (filename == NO_PERMS_XPI) { checkNotification(panel, isDefaultIcon, []); diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index a25e8a9ab9e2..9d92333aa1a8 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -73,153 +73,18 @@ addonInstallBlockedByPolicy=%1$S (%2$S) is blocked by your system administrator. addonDomainBlockedByPolicy=Your system administrator prevented this site from asking you to install software on your computer. addonInstallFullScreenBlocked=Add-on installation is not allowed while in or before entering fullscreen mode. -# LOCALIZATION NOTE (webextPerms.header,webextPerms.headerWithPerms,webextPerms.headerUnsigned,webextPerms.headerUnsignedWithPerms) -# This string is used as a header in the webextension permissions dialog, -# %S is replaced with the localized name of the extension being installed. -# See https://bug1308309.bmoattachments.org/attachment.cgi?id=8814612 -# for an example of the full dialog. -# Note, this string will be used as raw markup. Avoid characters like <, >, & -webextPerms.header=Add %S? -webextPerms.headerWithPerms=Add %S? This extension will have permission to: -webextPerms.headerUnsigned=Add %S? This extension is unverified. Malicious extensions can steal your private information or compromise your computer. Only add it if you trust the source. -webextPerms.headerUnsignedWithPerms=Add %S? This extension is unverified. Malicious extensions can steal your private information or compromise your computer. Only add it if you trust the source. This extension will have permission to: - webextPerms.learnMore2=Learn more -webextPerms.add.label=Add -webextPerms.add.accessKey=A -webextPerms.cancel.label=Cancel -webextPerms.cancel.accessKey=C # LOCALIZATION NOTE (webextPerms.sideloadMenuItem) # %1$S will be replaced with the localized name of the sideloaded add-on. # %2$S will be replace with the name of the application (e.g., Firefox, Nightly) webextPerms.sideloadMenuItem=%1$S added to %2$S -# LOCALIZATION NOTE (webextPerms.sideloadHeader) -# This string is used as a header in the webextension permissions dialog -# when the extension is side-loaded. -# %S is replaced with the localized name of the extension being installed. -# Note, this string will be used as raw markup. Avoid characters like <, >, & -webextPerms.sideloadHeader=%S added -webextPerms.sideloadText2=Another program on your computer installed an add-on that may affect your browser. Please review this add-on’s permissions requests and choose to Enable or Cancel (to leave it disabled). -webextPerms.sideloadTextNoPerms=Another program on your computer installed an add-on that may affect your browser. Please choose to Enable or Cancel (to leave it disabled). - -webextPerms.sideloadEnable.label=Enable -webextPerms.sideloadEnable.accessKey=E -webextPerms.sideloadCancel.label=Cancel -webextPerms.sideloadCancel.accessKey=C - # LOCALIZATION NOTE (webextPerms.updateMenuItem) # %S will be replaced with the localized name of the extension which # has been updated. webextPerms.updateMenuItem=%S requires new permissions -# LOCALIZATION NOTE (webextPerms.updateText) -# %S is replaced with the localized name of the updated extension. -# Note, this string will be used as raw markup. Avoid characters like <, >, & -webextPerms.updateText2=%S has been updated. You must approve new permissions before the updated version will install. Choosing “Cancel” will maintain your current extension version. This extension will have permission to: - -webextPerms.updateAccept.label=Update -webextPerms.updateAccept.accessKey=U - -# LOCALIZATION NOTE (webextPerms.optionalPermsHeader) -# %S is replace with the localized name of the extension requested new -# permissions. -# Note, this string will be used as raw markup. Avoid characters like <, >, & -webextPerms.optionalPermsHeader=%S requests additional permissions. -webextPerms.optionalPermsListIntro=It wants to: -webextPerms.optionalPermsAllow.label=Allow -webextPerms.optionalPermsAllow.accessKey=A -webextPerms.optionalPermsDeny.label=Deny -webextPerms.optionalPermsDeny.accessKey=D - -webextPerms.description.bookmarks=Read and modify bookmarks -webextPerms.description.browserSettings=Read and modify browser settings -webextPerms.description.browsingData=Clear recent browsing history, cookies, and related data -webextPerms.description.clipboardRead=Get data from the clipboard -webextPerms.description.clipboardWrite=Input data to the clipboard -webextPerms.description.declarativeNetRequest=Block content on any page -webextPerms.description.declarativeNetRequestFeedback=Read your browsing history -webextPerms.description.devtools=Extend developer tools to access your data in open tabs -webextPerms.description.downloads=Download files and read and modify the browser’s download history -webextPerms.description.downloads.open=Open files downloaded to your computer -webextPerms.description.find=Read the text of all open tabs -webextPerms.description.geolocation=Access your location -webextPerms.description.history=Access browsing history -webextPerms.description.management=Monitor extension usage and manage themes -# LOCALIZATION NOTE (webextPerms.description.nativeMessaging) -# %S will be replaced with the name of the application -webextPerms.description.nativeMessaging=Exchange messages with programs other than %S -webextPerms.description.notifications=Display notifications to you -webextPerms.description.pkcs11=Provide cryptographic authentication services -webextPerms.description.privacy=Read and modify privacy settings -webextPerms.description.proxy=Control browser proxy settings -webextPerms.description.sessions=Access recently closed tabs -webextPerms.description.tabs=Access browser tabs -webextPerms.description.tabHide=Hide and show browser tabs -webextPerms.description.topSites=Access browsing history -webextPerms.description.webNavigation=Access browser activity during navigation - -webextPerms.hostDescription.allUrls=Access your data for all websites - -# LOCALIZATION NOTE (webextPerms.hostDescription.wildcard) -# %S will be replaced by the DNS domain for which a webextension -# is requesting access (e.g., mozilla.org) -webextPerms.hostDescription.wildcard=Access your data for sites in the %S domain - -# LOCALIZATION NOTE (webextPerms.hostDescription.tooManyWildcards): -# Semi-colon list of plural forms. -# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals -# #1 will be replaced by an integer indicating the number of additional -# domains for which this webextension is requesting permission. -webextPerms.hostDescription.tooManyWildcards=Access your data in #1 other domain;Access your data in #1 other domains - -# LOCALIZATION NOTE (webextPerms.hostDescription.oneSite) -# %S will be replaced by the DNS host name for which a webextension -# is requesting access (e.g., www.mozilla.org) -webextPerms.hostDescription.oneSite=Access your data for %S - -# LOCALIZATION NOTE (webextPerms.hostDescription.tooManySites) -# Semi-colon list of plural forms. -# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals -# #1 will be replaced by an integer indicating the number of additional -# hosts for which this webextension is requesting permission. -webextPerms.hostDescription.tooManySites=Access your data on #1 other site;Access your data on #1 other sites - -# LOCALIZATION NOTE (webextSitePerms.headerWithPerms,webextSitePerms.headerUnsignedWithPerms) -# This string is used as a header in the webextension permissions dialog, -# %1$S is replaced with the localized name of the extension being installed. -# %2$S will be replaced by the DNS host name for which a webextension enables permissions -# Note, this string will be used as raw markup. Avoid characters like <, >, & -webextSitePerms.headerWithPerms=Add %1$S? This extension grants the following capabilities to %2$S: -webextSitePerms.headerUnsignedWithPerms=Add %1$S? This extension is unverified. Malicious extensions can steal your private information or compromise your computer. Only add it if you trust the source. This extension grants the following capabilities to %2$S: - -# LOCALIZATION NOTE (webextSitePerms.headerWithGatedPerms.midi) -# This string is used as a header in the webextension permissions dialog for synthetic add-ons. -# The part of the string describing what privileges the extension gives should be consistent -# with the value of webextSitePerms.description.{sitePermission}. -# %S is the hostname of the site the add-on is being installed from. -# Note, this string will be used as raw markup. Avoid characters like <, >, & -webextSitePerms.headerWithGatedPerms.midi=This add-on gives %S access to your MIDI devices. - -# LOCALIZATION NOTE (webextSitePerms.headerWithGatedPerms.midi-sysex) -# This string is used as a header in the webextension permissions dialog for synthetic add-ons. -# The part of the string describing what privileges the extension gives should be consistent -# with the value of webextSitePerms.description.{sitePermission}. -# %S is the hostname of the site the add-on is being installed from. -# Note, this string will be used as raw markup. Avoid characters like <, >, & -webextSitePerms.headerWithGatedPerms.midi-sysex=This add-on gives %S access to your MIDI devices (with SysEx support). - -# LOCALIZATION NOTE (webextSitePerms.descriptionGatedPerms) -# This string is used as description in the webextension permissions dialog for synthetic add-ons. -# Note, the \n\n is used to create a line break between the two sections. -# Note, this string will be used as raw markup. Avoid characters like <, >, & -webextSitePerms.descriptionGatedPerms.midi=These are usually plug-in devices like audio synthesizers, but might also be built into your computer.\n\nWebsites are normally not allowed to access MIDI devices. Improper usage could cause damage or compromise security. - -# These should remain in sync with permissions.NAME.label in sitePermissions.properties -webextSitePerms.description.midi=Access MIDI devices -webextSitePerms.description.midi-sysex=Access MIDI devices with SysEx support - # LOCALIZATION NOTE (webext.defaultSearch.description) # %1$S is replaced with the localized named of the extension that is asking to change the default search engine. # %2$S is replaced with the name of the current search engine diff --git a/browser/modules/ExtensionsUI.jsm b/browser/modules/ExtensionsUI.jsm index b4674f6a8186..53648786577f 100644 --- a/browser/modules/ExtensionsUI.jsm +++ b/browser/modules/ExtensionsUI.jsm @@ -31,7 +31,6 @@ const DEFAULT_EXTENSION_ICON = "chrome://mozapps/skin/extensions/extensionGeneric.svg"; const BROWSER_PROPERTIES = "chrome://browser/locale/browser.properties"; -const BRAND_PROPERTIES = "chrome://branding/locale/brand.properties"; const HTML_NS = "http://www.w3.org/1999/xhtml"; @@ -317,11 +316,8 @@ var ExtensionsUI = { // Create a set of formatted strings for a permission prompt _buildStrings(info) { let bundle = Services.strings.createBundle(BROWSER_PROPERTIES); - let brandBundle = Services.strings.createBundle(BRAND_PROPERTIES); - let appName = brandBundle.GetStringFromName("brandShortName"); - let info2 = Object.assign({ appName }, info); - let strings = lazy.ExtensionData.formatPermissionStrings(info2, bundle, { + let strings = lazy.ExtensionData.formatPermissionStrings(info, { collapseOrigins: true, }); strings.addonName = info.addon.name; diff --git a/dom/midi/tests/browser_midi_permission_gated.js b/dom/midi/tests/browser_midi_permission_gated.js index 2573ba7268bd..25edf7a126c7 100644 --- a/dom/midi/tests/browser_midi_permission_gated.js +++ b/dom/midi/tests/browser_midi_permission_gated.js @@ -12,6 +12,15 @@ const PAGE_WITH_IFRAMES_URL = `https://example.org/document-builder.sjs?html= 'https://example.net/document-builder.sjs?html=CrossOrigin"' )}">`; +const l10n = new Localization( + [ + "toolkit/global/extensions.ftl", + "toolkit/global/extensionPermissions.ftl", + "branding/brand.ftl", + ], + true +); + const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); ChromeUtils.defineModuleGetter( this, @@ -141,19 +150,16 @@ add_task(async function testRequestMIDIAccess() { let installDialog = await dialogPromise; is( installDialog.querySelector(".popup-notification-description").textContent, - gNavigatorBundle.getFormattedString( - "webextSitePerms.headerWithGatedPerms.midi-sysex", - [testPageHost] + l10n.formatValueSync( + "webext-site-perms-header-with-gated-perms-midi-sysex", + { hostname: testPageHost } ), "Install dialog has expected header text" ); is( installDialog.querySelector("popupnotificationcontent description") .textContent, - gNavigatorBundle.getFormattedString( - "webextSitePerms.descriptionGatedPerms.midi", - [testPageHost] - ), + l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"), "Install dialog has expected description" ); @@ -288,19 +294,15 @@ add_task(async function testRequestMIDIAccess() { is( installDialog.querySelector(".popup-notification-description").textContent, - gNavigatorBundle.getFormattedString( - "webextSitePerms.headerWithGatedPerms.midi", - [testPageHost] - ), + l10n.formatValueSync("webext-site-perms-header-with-gated-perms-midi", { + hostname: testPageHost, + }), "Install dialog has expected header text" ); is( installDialog.querySelector("popupnotificationcontent description") .textContent, - gNavigatorBundle.getFormattedString( - "webextSitePerms.descriptionGatedPerms.midi", - [testPageHost] - ), + l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"), "Install dialog has expected description" ); diff --git a/mobile/android/locales/en-US/chrome/browser.properties b/mobile/android/locales/en-US/chrome/browser.properties index ddcfc00e8023..4172e3ecb83a 100644 --- a/mobile/android/locales/en-US/chrome/browser.properties +++ b/mobile/android/locales/en-US/chrome/browser.properties @@ -2,99 +2,6 @@ # 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/. -# In Extension.jsm - -# LOCALIZATION NOTE (webextPerms.header) -# This string is used as a header in the webextension permissions dialog, -# %S is replaced with the localized name of the extension being installed. -# See https://bug1308309.bmoattachments.org/attachment.cgi?id=8814612 -# for an example of the full dialog. -# Note, this string will be used as raw markup. Avoid characters like <, >, & -webextPerms.header=Add %S? -webextPerms.headerWithPerms=Add %S? This extension will have permission to: -webextPerms.headerUnsigned=Add %S? This extension is unverified. Malicious extensions can steal your private information or compromise your computer. Only add it if you trust the source. -webextPerms.headerUnsignedWithPerms=Add %S? This extension is unverified. Malicious extensions can steal your private information or compromise your computer. Only add it if you trust the source. This extension will have permission to: - -webextPerms.add.label=Add -webextPerms.cancel.label=Cancel - -# LOCALIZATION NOTE (webextSitePerms.headerWithPerms,webextSitePerms.headerUnsignedWithPerms) -# This string is used as a header in the webextension permissions dialog, -# %1$S is replaced with the localized name of the extension being installed. -# %2$S will be replaced by the DNS host name for which a webextension enables permissions -# Note, this string will be used as raw markup. Avoid characters like <, >, & -webextSitePerms.headerWithPerms=Add %1$S? This extension grants the following capabilities to %2$S: -webextSitePerms.headerUnsignedWithPerms=Add %1$S? This extension is unverified. Malicious extensions can steal your private information or compromise your computer. Only add it if you trust the source. This extension grants the following capabilities to %2$S: - -# These should remain in sync with permissions.NAME.label in sitePermissions.properties -webextSitePerms.description.midi=Access MIDI devices -webextSitePerms.description.midi-sysex=Access MIDI devices with SysEx support - -# LOCALIZATION NOTE (webextPerms.updateText) -# %S is replaced with the localized name of the updated extension. -webextPerms.updateText=%S has been updated. You must approve new permissions before the updated version will install. Choosing “Cancel” will maintain your current add-on version. - -webextPerms.updateAccept.label=Update - -# LOCALIZATION NOTE (webextPerms.optionalPermsHeader) -# %S is replaced with the localized name of the extension requesting new -# permissions. -webextPerms.optionalPermsHeader=%S requests additional permissions. -webextPerms.optionalPermsListIntro=It wants to: -webextPerms.optionalPermsAllow.label=Allow -webextPerms.optionalPermsDeny.label=Deny - -webextPerms.description.bookmarks=Read and modify bookmarks -webextPerms.description.browserSettings=Read and modify browser settings -webextPerms.description.browsingData=Clear recent browsing history, cookies, and related data -webextPerms.description.clipboardRead=Get data from the clipboard -webextPerms.description.clipboardWrite=Input data to the clipboard -webextPerms.description.declarativeNetRequest=Block content on any page -webextPerms.description.declarativeNetRequestFeedback=Read your browsing history -webextPerms.description.devtools=Extend developer tools to access your data in open tabs -webextPerms.description.downloads=Download files and read and modify the browser’s download history -webextPerms.description.downloads.open=Open files downloaded to your computer -webextPerms.description.find=Read the text of all open tabs -webextPerms.description.geolocation=Access your location -webextPerms.description.history=Access browsing history -webextPerms.description.management=Monitor extension usage and manage themes -# LOCALIZATION NOTE (webextPerms.description.nativeMessaging) -# %S will be replaced with the name of the application -webextPerms.description.nativeMessaging=Exchange messages with programs other than %S -webextPerms.description.notifications=Display notifications to you -webextPerms.description.privacy=Read and modify privacy settings -webextPerms.description.proxy=Control browser proxy settings -webextPerms.description.sessions=Access recently closed tabs -webextPerms.description.tabs=Access browser tabs -webextPerms.description.topSites=Access browsing history -webextPerms.description.webNavigation=Access browser activity during navigation - -webextPerms.hostDescription.allUrls=Access your data for all websites - -# LOCALIZATION NOTE (webextPerms.hostDescription.wildcard) -# %S will be replaced by the DNS domain for which a webextension -# is requesting access (e.g., mozilla.org) -webextPerms.hostDescription.wildcard=Access your data for sites in the %S domain - -# LOCALIZATION NOTE (webextPerms.hostDescription.tooManyWildcards): -# Semi-colon list of plural forms. -# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals -# #1 will be replaced by an integer indicating the number of additional -# domains for which this webextension is requesting permission. -webextPerms.hostDescription.tooManyWildcards=Access your data in #1 other domain;Access your data in #1 other domains - -# LOCALIZATION NOTE (webextPerms.hostDescription.oneSite) -# %S will be replaced by the DNS host name for which a webextension -# is requesting access (e.g., www.mozilla.org) -webextPerms.hostDescription.oneSite=Access your data for %S - -# LOCALIZATION NOTE (webextPerms.hostDescription.tooManySites) -# Semi-colon list of plural forms. -# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals -# #1 will be replaced by an integer indicating the number of additional -# hosts for which this webextension is requesting permission. -webextPerms.hostDescription.tooManySites=Access your data on #1 other site;Access your data on #1 other sites - # Web Console API (in GeckoViewConsole.jsm) stacktrace.anonymousFunction= stacktrace.outputMessage=Stack trace from %S, function %S, line %S. diff --git a/python/l10n/fluent_migrations/bug_1793557_extensions.py b/python/l10n/fluent_migrations/bug_1793557_extensions.py new file mode 100644 index 000000000000..79d16d2f84ef --- /dev/null +++ b/python/l10n/fluent_migrations/bug_1793557_extensions.py @@ -0,0 +1,391 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +import fluent.syntax.ast as FTL +from fluent.migrate.helpers import TERM_REFERENCE, VARIABLE_REFERENCE +from fluent.migrate.transforms import COPY, PLURALS, REPLACE, REPLACE_IN_TEXT + + +def migrate(ctx): + """Bug 1793557 - Convert Extension.jsm to Fluent, part {index}.""" + + source = "browser/chrome/browser/browser.properties" + extensions = "toolkit/toolkit/global/extensions.ftl" + permissions = "toolkit/toolkit/global/extensionPermissions.ftl" + + ctx.add_transforms( + extensions, + extensions, + [ + FTL.Message( + id=FTL.Identifier("webext-perms-header"), + value=REPLACE( + source, + "webextPerms.header", + {"%1$S": VARIABLE_REFERENCE("extension")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-header-with-perms"), + value=REPLACE( + source, + "webextPerms.headerWithPerms", + {"%1$S": VARIABLE_REFERENCE("extension")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-header-unsigned"), + value=REPLACE( + source, + "webextPerms.headerUnsigned", + {"%1$S": VARIABLE_REFERENCE("extension")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-header-unsigned-with-perms"), + value=REPLACE( + source, + "webextPerms.headerUnsignedWithPerms", + {"%1$S": VARIABLE_REFERENCE("extension")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-add"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(source, "webextPerms.add.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "webextPerms.add.accessKey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("webext-perms-cancel"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(source, "webextPerms.cancel.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "webextPerms.cancel.accessKey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("webext-perms-sideload-header"), + value=REPLACE( + source, + "webextPerms.sideloadHeader", + {"%1$S": VARIABLE_REFERENCE("extension")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-sideload-text"), + value=COPY(source, "webextPerms.sideloadText2"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-sideload-text-no-perms"), + value=COPY(source, "webextPerms.sideloadTextNoPerms"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-sideload-enable"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(source, "webextPerms.sideloadEnable.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "webextPerms.sideloadEnable.accessKey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("webext-perms-sideload-cancel"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(source, "webextPerms.sideloadCancel.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "webextPerms.sideloadCancel.accessKey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("webext-perms-update-text"), + value=REPLACE( + source, + "webextPerms.updateText2", + {"%1$S": VARIABLE_REFERENCE("extension")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-update-accept"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(source, "webextPerms.updateAccept.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "webextPerms.updateAccept.accessKey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("webext-perms-optional-perms-header"), + value=REPLACE( + source, + "webextPerms.optionalPermsHeader", + {"%1$S": VARIABLE_REFERENCE("extension")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-optional-perms-list-intro"), + value=COPY(source, "webextPerms.optionalPermsListIntro"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-optional-perms-allow"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(source, "webextPerms.optionalPermsAllow.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "webextPerms.optionalPermsAllow.accessKey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("webext-perms-optional-perms-deny"), + attributes=[ + FTL.Attribute( + id=FTL.Identifier("label"), + value=COPY(source, "webextPerms.optionalPermsDeny.label"), + ), + FTL.Attribute( + id=FTL.Identifier("accesskey"), + value=COPY(source, "webextPerms.optionalPermsDeny.accessKey"), + ), + ], + ), + FTL.Message( + id=FTL.Identifier("webext-perms-host-description-all-urls"), + value=COPY(source, "webextPerms.hostDescription.allUrls"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-host-description-wildcard"), + value=REPLACE( + source, + "webextPerms.hostDescription.wildcard", + {"%1$S": VARIABLE_REFERENCE("domain")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-host-description-too-many-wildcards"), + value=PLURALS( + source, + "webextPerms.hostDescription.tooManyWildcards", + VARIABLE_REFERENCE("domainCount"), + foreach=lambda n: REPLACE_IN_TEXT( + n, + {"#1": VARIABLE_REFERENCE("domainCount")}, + ), + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-host-description-one-site"), + value=REPLACE( + source, + "webextPerms.hostDescription.oneSite", + {"%1$S": VARIABLE_REFERENCE("domain")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-host-description-too-many-sites"), + value=PLURALS( + source, + "webextPerms.hostDescription.tooManySites", + VARIABLE_REFERENCE("domainCount"), + foreach=lambda n: REPLACE_IN_TEXT( + n, + {"#1": VARIABLE_REFERENCE("domainCount")}, + ), + ), + ), + FTL.Message( + id=FTL.Identifier("webext-site-perms-header-with-gated-perms-midi"), + value=REPLACE( + source, + "webextSitePerms.headerWithGatedPerms.midi", + { + "%1$S": VARIABLE_REFERENCE("hostname"), + }, + ), + ), + FTL.Message( + id=FTL.Identifier( + "webext-site-perms-header-with-gated-perms-midi-sysex" + ), + value=REPLACE( + source, + "webextSitePerms.headerWithGatedPerms.midi-sysex", + { + "%1$S": VARIABLE_REFERENCE("hostname"), + }, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-site-perms-description-gated-perms-midi"), + value=COPY(source, "webextSitePerms.descriptionGatedPerms.midi"), + ), + FTL.Message( + id=FTL.Identifier("webext-site-perms-header-with-perms"), + value=REPLACE( + source, + "webextSitePerms.headerWithPerms", + { + "%1$S": VARIABLE_REFERENCE("extension"), + "%2$S": VARIABLE_REFERENCE("hostname"), + }, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-site-perms-header-unsigned-with-perms"), + value=REPLACE( + source, + "webextSitePerms.headerUnsignedWithPerms", + { + "%1$S": VARIABLE_REFERENCE("extension"), + "%2$S": VARIABLE_REFERENCE("hostname"), + }, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-site-perms-midi"), + value=COPY(source, "webextSitePerms.description.midi"), + ), + FTL.Message( + id=FTL.Identifier("webext-site-perms-midi-sysex"), + value=COPY(source, "webextSitePerms.description.midi-sysex"), + ), + ], + ) + + ctx.add_transforms( + permissions, + permissions, + [ + FTL.Message( + id=FTL.Identifier("webext-perms-description-bookmarks"), + value=COPY(source, "webextPerms.description.bookmarks"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-browserSettings"), + value=COPY(source, "webextPerms.description.browserSettings"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-browsingData"), + value=COPY(source, "webextPerms.description.browsingData"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-clipboardRead"), + value=COPY(source, "webextPerms.description.clipboardRead"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-clipboardWrite"), + value=COPY(source, "webextPerms.description.clipboardWrite"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-declarativeNetRequest"), + value=COPY(source, "webextPerms.description.declarativeNetRequest"), + ), + FTL.Message( + id=FTL.Identifier( + "webext-perms-description-declarativeNetRequestFeedback" + ), + value=COPY( + source, "webextPerms.description.declarativeNetRequestFeedback" + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-devtools"), + value=COPY(source, "webextPerms.description.devtools"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-downloads"), + value=COPY(source, "webextPerms.description.downloads"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-downloads-open"), + value=COPY(source, "webextPerms.description.downloads.open"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-find"), + value=COPY(source, "webextPerms.description.find"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-geolocation"), + value=COPY(source, "webextPerms.description.geolocation"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-history"), + value=COPY(source, "webextPerms.description.history"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-management"), + value=COPY(source, "webextPerms.description.management"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-nativeMessaging"), + value=REPLACE( + source, + "webextPerms.description.nativeMessaging", + {"%1$S": TERM_REFERENCE("brand-short-name")}, + ), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-notifications"), + value=COPY(source, "webextPerms.description.notifications"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-pkcs11"), + value=COPY(source, "webextPerms.description.pkcs11"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-privacy"), + value=COPY(source, "webextPerms.description.privacy"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-proxy"), + value=COPY(source, "webextPerms.description.proxy"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-sessions"), + value=COPY(source, "webextPerms.description.sessions"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-tabs"), + value=COPY(source, "webextPerms.description.tabs"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-tabHide"), + value=COPY(source, "webextPerms.description.tabHide"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-topSites"), + value=COPY(source, "webextPerms.description.topSites"), + ), + FTL.Message( + id=FTL.Identifier("webext-perms-description-webNavigation"), + value=COPY(source, "webextPerms.description.webNavigation"), + ), + ], + ) diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index 3250f31a9ec7..1cf63b1f4dfe 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -56,6 +56,8 @@ ChromeUtils.defineESModuleGetters(lazy, { ExtensionDNR: "resource://gre/modules/ExtensionDNR.sys.mjs", ExtensionDNRStore: "resource://gre/modules/ExtensionDNRStore.sys.mjs", Log: "resource://gre/modules/Log.sys.mjs", + permissionToL10nId: + "resource://gre/modules/ExtensionPermissionMessages.sys.mjs", SITEPERMS_ADDON_TYPE: "resource://gre/modules/addons/siteperms-addon-utils.sys.mjs", }); @@ -75,7 +77,6 @@ XPCOMUtils.defineLazyModuleGetters(lazy, { ExtensionTelemetry: "resource://gre/modules/ExtensionTelemetry.jsm", LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm", NetUtil: "resource://gre/modules/NetUtil.jsm", - PluralForm: "resource://gre/modules/PluralForm.jsm", Schemas: "resource://gre/modules/Schemas.jsm", ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm", }); @@ -86,6 +87,20 @@ XPCOMUtils.defineLazyGetter(lazy, "resourceProtocol", () => .QueryInterface(Ci.nsIResProtocolHandler) ); +XPCOMUtils.defineLazyGetter( + lazy, + "l10n", + () => + new Localization( + [ + "toolkit/global/extensions.ftl", + "toolkit/global/extensionPermissions.ftl", + "branding/brand.ftl", + ], + true + ) +); + const { ExtensionCommon } = ChromeUtils.import( "resource://gre/modules/ExtensionCommon.jsm" ); @@ -2179,30 +2194,29 @@ class ExtensionData { return { allUrls, wildcards, sites, wildcardsMap, sitesMap }; } + /** + * @typedef {object} Permissions + * @property {Array} origins Origin permissions. + * @property {Array} permissions Regular (non-origin) permissions. + */ + /** * Formats all the strings for a permissions dialog/notification. * * @param {object} info Information about the permissions being requested. * - * @param {Array} info.permissions.origins - * Origin permissions requested. - * @param {Array} info.permissions.permissions - * Regular (non-origin) permissions requested. - * @param {Array} info.optionalPermissions.origins - * Optional origin permissions listed in the manifest. - * @param {Array} info.optionalPermissions.permissions - * Optional (non-origin) permissions listed in the manifest. + * @param {object} [info.addon] Optional information about the addon. + * @param {Permissions} [info.optionalPermissions] + * Optional permissions listed in the manifest. + * @param {Permissions} info.permissions Requested permissions. + * @param {string} info.siteOrigin + * @param {Array} [info.sitePermissions] * @param {boolean} info.unsigned * True if the prompt is for installing an unsigned addon. * @param {string} info.type * The type of prompt being shown. May be one of "update", * "sideload", "optional", or omitted for a regular * install prompt. - * @param {string} info.appName - * The localized name of the application, to be substituted - * in computed strings as needed. - * @param {nsIStringBundle} bundle - * The string bundle to use for l10n. * @param {object} options * @param {boolean} options.collapseOrigins * Wether to limit the number of displayed host permissions. @@ -2210,13 +2224,8 @@ class ExtensionData { * @param {boolean} options.buildOptionalOrigins * Wether to build optional origins Maps for permission * controls. Defaults to false. - * @param {Function} options.getKeyForPermission - * An optional callback function that returns the locale key for a given - * permission name (set by default to a callback returning the locale - * key following the default convention `webextPerms.description.PERMNAME`). - * Overriding the default mapping can become necessary, when a permission - * description needs to be modified and a non-default locale key has to be - * used. There is at least one non-default locale key used in Thunderbird. + * @param {Localization} options.localization + * Optional custom localization instance. * * @returns {object} An object with properties containing localized strings * for various elements of a permission dialog. The "header" @@ -2234,32 +2243,58 @@ class ExtensionData { * all url style permissions are included. */ static formatPermissionStrings( - info, - bundle, { - collapseOrigins = false, - buildOptionalOrigins = false, - getKeyForPermission = perm => `webextPerms.description.${perm}`, - } = {} + addon, + optionalPermissions, + permissions, + siteOrigin, + sitePermissions, + type, + unsigned, + }, + { collapseOrigins = false, buildOptionalOrigins = false, localization } = {} ) { - let result = { + const l10n = localization ?? lazy.l10n; + + const msgIds = []; + const headerArgs = { extension: "<>" }; + let acceptId = "webext-perms-add"; + let cancelId = "webext-perms-cancel"; + + const result = { msgs: [], optionalPermissions: {}, optionalOrigins: {}, + text: "", + listIntro: "", }; - const haveAccessKeys = AppConstants.platform !== "android"; + // To keep the label & accesskey in sync for localizations, + // they need to be stored as attributes of the same Fluent message. + // This unpacks them into the shape expected of them in `result`. + function setAcceptCancel(acceptId, cancelId) { + const haveAccessKeys = AppConstants.platform !== "android"; - let headerKey; - result.text = ""; - result.listIntro = ""; - result.acceptText = bundle.GetStringFromName("webextPerms.add.label"); - result.cancelText = bundle.GetStringFromName("webextPerms.cancel.label"); - if (haveAccessKeys) { - result.acceptKey = bundle.GetStringFromName("webextPerms.add.accessKey"); - result.cancelKey = bundle.GetStringFromName( - "webextPerms.cancel.accessKey" - ); + const [accept, cancel] = l10n.formatMessagesSync([ + { id: acceptId }, + { id: cancelId }, + ]); + + for (let { name, value } of accept.attributes) { + if (name === "label") { + result.acceptText = value; + } else if (name === "accesskey" && haveAccessKeys) { + result.acceptKey = value; + } + } + + for (let { name, value } of cancel.attributes) { + if (name === "label") { + result.cancelText = value; + } else if (name === "accesskey" && haveAccessKeys) { + result.cancelKey = value; + } + } } // Synthetic addon install can only grant access to a single permission so we can have @@ -2267,252 +2302,230 @@ class ExtensionData { // NOTE: this is used as part of the synthetic addon install flow implemented for the // SitePermissionAddonProvider. // (and so it should not be removed as part of Bug 1789718 changes, while this additional note should be). - if (info.addon?.type === lazy.SITEPERMS_ADDON_TYPE) { + // FIXME + if (addon?.type === lazy.SITEPERMS_ADDON_TYPE) { // We simplify the origin to make it more user friendly. The origin is assured to be // available because the SitePermsAddon install is always expected to be triggered // from a website, making the siteOrigin always available through the installing principal. - const host = new URL(info.siteOrigin).hostname; + headerArgs.hostname = new URL(siteOrigin).hostname; // messages are specific to the type of gated permission being installed - result.header = bundle.formatStringFromName( - `webextSitePerms.headerWithGatedPerms.${info.sitePermissions[0]}`, - [host] - ); + const headerId = + sitePermissions[0] === "midi-sysex" + ? "webext-site-perms-header-with-gated-perms-midi-sysex" + : "webext-site-perms-header-with-gated-perms-midi"; + result.header = l10n.formatValueSync(headerId, headerArgs); // We use the same string for midi and midi-sysex, and don't support any // other types of site permission add-ons. So we just hard-code the // descriptor for now. See bug 1826747. - result.text = bundle.GetStringFromName( - `webextSitePerms.descriptionGatedPerms.midi` + result.text = l10n.formatValueSync( + "webext-site-perms-description-gated-perms-midi" ); + setAcceptCancel(acceptId, cancelId); return result; } // TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed. - if (info.sitePermissions) { - // Generate a map of site_permission names to permission strings for site permissions. - for (let permission of info.sitePermissions) { - try { - result.msgs.push( - bundle.GetStringFromName( - `webextSitePerms.description.${permission}` - ) - ); - } catch (err) { - Cu.reportError( - `site_permission ${permission} missing readable text property` - ); - // We must never have a DOM api permission that is hidden so in - // the case of any error, we'll use the plain permission string. - // test_ext_sitepermissions.js tests for no missing messages, this - // is just an extra fallback. - result.msgs.push(permission); + if (sitePermissions) { + for (let permission of sitePermissions) { + let permMsg; + switch (permission) { + case "midi": + permMsg = l10n.formatValueSync("webext-site-perms-midi"); + break; + case "midi-sysex": + permMsg = l10n.formatValueSync("webext-site-perms-midi-sysex"); + break; + default: + Cu.reportError( + `site_permission ${permission} missing readable text property` + ); + // We must never have a DOM api permission that is hidden so in + // the case of any error, we'll use the plain permission string. + // test_ext_sitepermissions.js tests for no missing messages, this + // is just an extra fallback. + permMsg = permission; } + result.msgs.push(permMsg); } // We simplify the origin to make it more user friendly. The origin is // assured to be available via schema requirement. - const host = new URL(info.siteOrigin).hostname; - - headerKey = info.unsigned - ? "webextSitePerms.headerUnsignedWithPerms" - : "webextSitePerms.headerWithPerms"; - result.header = bundle.formatStringFromName(headerKey, ["<>", host]); + headerArgs.hostname = new URL(siteOrigin).hostname; + const headerId = unsigned + ? "webext-site-perms-header-unsigned-with-perms" + : "webext-site-perms-header-with-perms"; + result.header = l10n.formatValueSync(headerId, headerArgs); + setAcceptCancel(acceptId, cancelId); return result; } - let perms = info.permissions || { origins: [], permissions: [] }; - let optional_permissions = info.optionalPermissions || { - origins: [], - permissions: [], - }; + if (permissions) { + // First classify our host permissions + let { allUrls, wildcards, sites } = + ExtensionData.classifyOriginPermissions(permissions.origins); - // First classify our host permissions - let { allUrls, wildcards, sites } = ExtensionData.classifyOriginPermissions( - perms.origins - ); + // Format the host permissions. If we have a wildcard for all urls, + // a single string will suffice. Otherwise, show domain wildcards + // first, then individual host permissions. + if (allUrls) { + msgIds.push("webext-perms-host-description-all-urls"); + } else { + // Formats a list of host permissions. If we have 4 or fewer, display + // them all, otherwise display the first 3 followed by an item that + // says "...plus N others" + const addMessages = (set, l10nId, moreL10nId) => { + if (collapseOrigins && set.size > 4) { + for (let domain of Array.from(set).slice(0, 3)) { + msgIds.push({ id: l10nId, args: { domain } }); + } + msgIds.push({ + id: moreL10nId, + args: { domainCount: set.size - 3 }, + }); + } else { + for (let domain of set) { + msgIds.push({ id: l10nId, args: { domain } }); + } + } + }; - // Format the host permissions. If we have a wildcard for all urls, - // a single string will suffice. Otherwise, show domain wildcards - // first, then individual host permissions. - if (allUrls) { - result.msgs.push( - bundle.GetStringFromName("webextPerms.hostDescription.allUrls") - ); - } else { - // Formats a list of host permissions. If we have 4 or fewer, display - // them all, otherwise display the first 3 followed by an item that - // says "...plus N others" - let format = (list, itemKey, moreKey) => { - function formatItems(items) { - result.msgs.push( - ...items.map(item => bundle.formatStringFromName(itemKey, [item])) - ); - } - if (list.length < 5 || !collapseOrigins) { - formatItems(list); - } else { - formatItems(list.slice(0, 3)); - - let remaining = list.length - 3; - result.msgs.push( - lazy.PluralForm.get( - remaining, - bundle.GetStringFromName(moreKey) - ).replace("#1", remaining) - ); - } - }; - - format( - Array.from(wildcards), - "webextPerms.hostDescription.wildcard", - "webextPerms.hostDescription.tooManyWildcards" - ); - format( - Array.from(sites), - "webextPerms.hostDescription.oneSite", - "webextPerms.hostDescription.tooManySites" - ); - } - - // Next, show the native messaging permission if it is present. - const NATIVE_MSG_PERM = "nativeMessaging"; - if (perms.permissions.includes(NATIVE_MSG_PERM)) { - result.msgs.push( - bundle.formatStringFromName(getKeyForPermission(NATIVE_MSG_PERM), [ - info.appName, - ]) - ); - } - - // Finally, show remaining permissions, in the same order as AMO. - // The permissions are sorted alphabetically by the permission - // string to match AMO. - let permissionsCopy = perms.permissions.slice(0); - for (let permission of permissionsCopy.sort()) { - // Handled above - if (permission == NATIVE_MSG_PERM) { - continue; - } - try { - result.msgs.push( - bundle.GetStringFromName(getKeyForPermission(permission)) + addMessages( + wildcards, + "webext-perms-host-description-wildcard", + "webext-perms-host-description-too-many-wildcards" ); - } catch (err) { + addMessages( + sites, + "webext-perms-host-description-one-site", + "webext-perms-host-description-too-many-sites" + ); + } + + // Finally, show remaining permissions, in the same order as AMO. + // The permissions are sorted alphabetically by the permission + // string to match AMO. + // Show the native messaging permission first if it is present. + const NATIVE_MSG_PERM = "nativeMessaging"; + const permissionsSorted = permissions.permissions.sort((a, b) => { + if (a === NATIVE_MSG_PERM) { + return -1; + } else if (b === NATIVE_MSG_PERM) { + return 1; + } + return a < b ? -1 : 1; + }); + for (let permission of permissionsSorted) { + const l10nId = lazy.permissionToL10nId(permission); // We deliberately do not include all permissions in the prompt. // So if we don't find one then just skip it. + if (l10nId) { + msgIds.push(l10nId); + } } } - // Generate a map of permission names to permission strings for optional - // permissions. The key is necessary to handle toggling those permissions. - for (let permission of optional_permissions.permissions) { - if (permission == NATIVE_MSG_PERM) { - result.optionalPermissions[permission] = bundle.formatStringFromName( - getKeyForPermission(permission), - [info.appName] - ); - continue; - } - try { - result.optionalPermissions[permission] = bundle.GetStringFromName( - getKeyForPermission(permission) - ); - } catch (err) { - // We deliberately do not have strings for all permissions. + if (optionalPermissions) { + // Generate a map of permission names to permission strings for optional + // permissions. The key is necessary to handle toggling those permissions. + const opKeys = []; + const opL10nIds = []; + for (let permission of optionalPermissions.permissions) { + const l10nId = lazy.permissionToL10nId(permission); + // We deliberately do not include all permissions in the prompt. // So if we don't find one then just skip it. + if (l10nId) { + opKeys.push(permission); + opL10nIds.push(l10nId); + } + } + if (opKeys.length) { + const opRes = l10n.formatValuesSync(opL10nIds); + for (let i = 0; i < opKeys.length; ++i) { + result.optionalPermissions[opKeys[i]] = opRes[i]; + } + } + + const { allUrls, sitesMap, wildcardsMap } = + ExtensionData.classifyOriginPermissions( + optionalPermissions.origins, + true + ); + const ooKeys = []; + const ooL10nIds = []; + if (allUrls) { + ooKeys.push(allUrls); + ooL10nIds.push("webext-perms-host-description-all-urls"); + } + + // Current UX controls are meant for developer testing with mv3. + if (buildOptionalOrigins) { + for (let [pattern, domain] of wildcardsMap.entries()) { + ooKeys.push(pattern); + ooL10nIds.push({ + id: "webext-perms-host-description-wildcard", + args: { domain }, + }); + } + for (let [pattern, domain] of sitesMap.entries()) { + ooKeys.push(pattern); + ooL10nIds.push({ + id: "webext-perms-host-description-one-site", + args: { domain }, + }); + } + } + + if (ooKeys.length) { + const res = l10n.formatValuesSync(ooL10nIds); + for (let i = 0; i < res.length; ++i) { + result.optionalOrigins[ooKeys[i]] = res[i]; + } } } - let optionalInfo = ExtensionData.classifyOriginPermissions( - optional_permissions.origins, - true - ); - if (optionalInfo.allUrls) { - result.optionalOrigins[optionalInfo.allUrls] = bundle.GetStringFromName( - "webextPerms.hostDescription.allUrls" - ); + let headerId; + switch (type) { + case "sideload": + headerId = "webext-perms-sideload-header"; + acceptId = "webext-perms-sideload-enable"; + cancelId = "webext-perms-sideload-cancel"; + result.text = l10n.formatValueSync( + msgIds.length + ? "webext-perms-sideload-text" + : "webext-perms-sideload-text-no-perms" + ); + break; + case "update": + headerId = "webext-perms-update-text"; + acceptId = "webext-perms-update-accept"; + break; + case "optional": + headerId = "webext-perms-optional-perms-header"; + acceptId = "webext-perms-optional-perms-allow"; + cancelId = "webext-perms-optional-perms-deny"; + result.listIntro = l10n.formatValueSync( + "webext-perms-optional-perms-list-intro" + ); + break; + default: + if (msgIds.length) { + headerId = unsigned + ? "webext-perms-header-unsigned-with-perms" + : "webext-perms-header-with-perms"; + } else { + headerId = unsigned + ? "webext-perms-header-unsigned" + : "webext-perms-header"; + } } - // Current UX controls are meant for developer testing with mv3. - if (buildOptionalOrigins) { - for (let [pattern, originLabel] of optionalInfo.wildcardsMap.entries()) { - let key = "webextPerms.hostDescription.wildcard"; - let str = bundle.formatStringFromName(key, [originLabel]); - result.optionalOrigins[pattern] = str; - } - for (let [pattern, originLabel] of optionalInfo.sitesMap.entries()) { - let key = "webextPerms.hostDescription.oneSite"; - let str = bundle.formatStringFromName(key, [originLabel]); - result.optionalOrigins[pattern] = str; - } - } - - if (info.type == "sideload") { - headerKey = "webextPerms.sideloadHeader"; - let key = !result.msgs.length - ? "webextPerms.sideloadTextNoPerms" - : "webextPerms.sideloadText2"; - result.text = bundle.GetStringFromName(key); - result.acceptText = bundle.GetStringFromName( - "webextPerms.sideloadEnable.label" - ); - result.cancelText = bundle.GetStringFromName( - "webextPerms.sideloadCancel.label" - ); - if (haveAccessKeys) { - result.acceptKey = bundle.GetStringFromName( - "webextPerms.sideloadEnable.accessKey" - ); - result.cancelKey = bundle.GetStringFromName( - "webextPerms.sideloadCancel.accessKey" - ); - } - } else if (info.type == "update") { - headerKey = "webextPerms.updateText2"; - result.text = ""; - result.acceptText = bundle.GetStringFromName( - "webextPerms.updateAccept.label" - ); - if (haveAccessKeys) { - result.acceptKey = bundle.GetStringFromName( - "webextPerms.updateAccept.accessKey" - ); - } - } else if (info.type == "optional") { - headerKey = "webextPerms.optionalPermsHeader"; - result.text = ""; - result.listIntro = bundle.GetStringFromName( - "webextPerms.optionalPermsListIntro" - ); - result.acceptText = bundle.GetStringFromName( - "webextPerms.optionalPermsAllow.label" - ); - result.cancelText = bundle.GetStringFromName( - "webextPerms.optionalPermsDeny.label" - ); - if (haveAccessKeys) { - result.acceptKey = bundle.GetStringFromName( - "webextPerms.optionalPermsAllow.accessKey" - ); - result.cancelKey = bundle.GetStringFromName( - "webextPerms.optionalPermsDeny.accessKey" - ); - } - } else { - headerKey = "webextPerms.header"; - if (result.msgs.length) { - headerKey = info.unsigned - ? "webextPerms.headerUnsignedWithPerms" - : "webextPerms.headerWithPerms"; - } else if (info.unsigned) { - headerKey = "webextPerms.headerUnsigned"; - } - } - result.header = bundle.formatStringFromName(headerKey, ["<>"]); + result.header = l10n.formatValueSync(headerId, headerArgs); + result.msgs = l10n.formatValuesSync(msgIds); + setAcceptCancel(acceptId, cancelId); return result; } } diff --git a/toolkit/components/extensions/ExtensionPermissionMessages.sys.mjs b/toolkit/components/extensions/ExtensionPermissionMessages.sys.mjs new file mode 100644 index 000000000000..9276abbd1f0b --- /dev/null +++ b/toolkit/components/extensions/ExtensionPermissionMessages.sys.mjs @@ -0,0 +1,79 @@ +/* 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/. */ + +/** + * List of permissions that are associated with a permission message. + * + * Keep this list in sync with: + * - The messages in `toolkit/locales/en-US/toolkit/global/extensionPermissions.ftl` + * - `permissionToTranslation` at https://github.com/mozilla-mobile/firefox-android/blob/d9c08c387917e3e53963386ad53229e69d52da6e/android-components/components/feature/addons/src/main/java/mozilla/components/feature/addons/Addon.kt#L174-L206 + * - https://extensionworkshop.com/documentation/develop/request-the-right-permissions/#advised-permissions + * - https://support.mozilla.org/en-US/kb/permission-request-messages-firefox-extensions + * + * This is exported to allow builds (e.g. Thunderbird) to extend or modify the set. + */ +export const PERMISSIONS_WITH_MESSAGE = new Set([ + "bookmarks", + "browserSettings", + "browsingData", + "clipboardRead", + "clipboardWrite", + "declarativeNetRequest", + "declarativeNetRequestFeedback", + "devtools", + "downloads", + "downloads.open", + "find", + "geolocation", + "history", + "management", + "nativeMessaging", + "notifications", + "pkcs11", + "privacy", + "proxy", + "sessions", + "tabs", + "tabHide", + "topSites", + "webNavigation", +]); + +/** + * Overrides for permission description l10n identifiers, + * which by default use the pattern `webext-perms-description-${permission}` + * where `permission` is sanitized to be a valid Fluent identifier. + * + * This is exported to allow builds (e.g. Thunderbird) to extend or modify the map. + */ +export const PERMISSION_L10N_ID_OVERRIDES = new Map(); + +/** + * Maps a permission name to its l10n identifier. + * + * Returns `null` for permissions not in `PERMISSIONS_WITH_MESSAGE`. + * + * The default `webext-perms-description-${permission}` mapping + * may be overridden by entries in `PERMISSION_L10N_ID_OVERRIDES`. + * + * @param {string} permission + * @returns {string | null} + */ +export function permissionToL10nId(permission) { + if (!PERMISSIONS_WITH_MESSAGE.has(permission)) { + return null; + } + + if (PERMISSION_L10N_ID_OVERRIDES.has(permission)) { + return PERMISSION_L10N_ID_OVERRIDES.get(permission); + } + + // Sanitize input to end up with a valid l10n id. + // E.g. "" to "all-urls", "downloads.open" to "downloads-open". + const sanitized = permission + .replace(/[^a-zA-Z0-9]+/g, "-") + .replace(/^-|-$/g, ""); + + return `webext-perms-description-${sanitized}`; +} diff --git a/toolkit/components/extensions/moz.build b/toolkit/components/extensions/moz.build index 31e6e52f5d71..c721999f88cf 100644 --- a/toolkit/components/extensions/moz.build +++ b/toolkit/components/extensions/moz.build @@ -23,6 +23,7 @@ EXTRA_JS_MODULES += [ "ExtensionDNRStore.sys.mjs", "ExtensionPageChild.jsm", "ExtensionParent.jsm", + "ExtensionPermissionMessages.sys.mjs", "ExtensionPermissions.jsm", "ExtensionPreferencesManager.jsm", "ExtensionProcessScript.jsm", diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js b/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js index 7e3ed9800424..c60efd03352f 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_permission_warnings.js @@ -4,19 +4,26 @@ let { ExtensionTestCommon } = ChromeUtils.import( "resource://testing-common/ExtensionTestCommon.jsm" ); -let bundle; -if (AppConstants.MOZ_APP_NAME == "thunderbird") { - bundle = Services.strings.createBundle( - "chrome://messenger/locale/addons.properties" - ); -} else { - // For Android, these strings are only used in tests. In the actual UI, the - // warnings are in Android-Components, as explained in bug 1671453. - bundle = Services.strings.createBundle( - "chrome://browser/locale/browser.properties" - ); -} -const DUMMY_APP_NAME = "Dummy brandName"; +const { PERMISSION_L10N_ID_OVERRIDES } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionPermissionMessages.sys.mjs" +); + +const EXTENSION_L10N_PATHS = + AppConstants.MOZ_APP_NAME == "thunderbird" + ? [ + "messenger/addons.ftl", // FIXME: mock path, file does not exist + "messenger/addonPermissions.ftl", // FIXME: mock path, file does not exist + "branding/brand.ftl", + ] + : [ + "toolkit/global/extensions.ftl", + "toolkit/global/extensionPermissions.ftl", + "branding/brand.ftl", + ]; + +// For Android, these strings are only used in tests. In the actual UI, the +// warnings are in Android-Components, as explained in bug 1671453. +const l10n = new Localization(EXTENSION_L10N_PATHS, true); // nativeMessaging is in PRIVILEGED_PERMS on Android. const IS_NATIVE_MESSAGING_PRIVILEGED = AppConstants.platform == "android"; @@ -50,18 +57,9 @@ async function getManifestPermissions(extensionData) { return result; } -function getPermissionWarnings( - manifestPermissions, - options, - stringBundle = bundle -) { - let info = { - permissions: manifestPermissions, - appName: DUMMY_APP_NAME, - }; +function getPermissionWarnings(permissions, options) { let { msgs } = ExtensionData.formatPermissionStrings( - info, - stringBundle, + { permissions }, options ); return msgs; @@ -80,44 +78,53 @@ async function getPermissionWarningsForUpdate( // Tests that the callers of ExtensionData.formatPermissionStrings can customize the // mapping between the permission names and related localized strings. add_task(async function customized_permission_keys_mapping() { - const mockBundle = { - // Mocked nsIStringBundle getStringFromName to returns a fake localized string. - GetStringFromName: key => `Fake localized ${key}`, - formatStringFromName: (name, params) => "Fake formatted string", + const mockLocalization = { + formatMessagesSync: args => + args.map(arg => ({ + value: `Fake localized ${arg.id ?? arg}`, + attributes: [], + })), + formatValueSync: key => `Fake localized ${key}`, + formatValuesSync: args => + args.map(arg => `Fake localized ${arg.id ?? arg}`), }; // Define a non-default mapping for permission names -> locale keys. - const getKeyForPermission = perm => `customWebExtPerms.description.${perm}`; + const getKeyForPermission = perm => `custom-webext-perms-description-${perm}`; const manifest = { permissions: ["downloads", "proxy"], }; - const expectedWarnings = manifest.permissions.map(k => - mockBundle.GetStringFromName(getKeyForPermission(k)) + const expectedWarnings = mockLocalization.formatValuesSync( + manifest.permissions.map(getKeyForPermission) ); - const manifestPermissions = await getManifestPermissions({ manifest }); - // Pass the callback function for the non-default key mapping to - // ExtensionData.formatPermissionStrings() and verify it being used. - const warnings = getPermissionWarnings( - manifestPermissions, - { getKeyForPermission }, - mockBundle - ); - deepEqual( - warnings, - expectedWarnings, - "Got the expected string from customized permission mapping" - ); + try { + for (let perm of manifest.permissions) { + PERMISSION_L10N_ID_OVERRIDES.set(perm, getKeyForPermission(perm)); + } + const manifestPermissions = await getManifestPermissions({ manifest }); + + // Pass the callback function for the non-default key mapping to + // ExtensionData.formatPermissionStrings() and verify it being used. + const warnings = getPermissionWarnings(manifestPermissions, { + localization: mockLocalization, + }); + deepEqual( + warnings, + expectedWarnings, + "Got the expected string from customized permission mapping" + ); + } finally { + for (let perm of manifest.permissions) { + PERMISSION_L10N_ID_OVERRIDES.delete(perm); + } + } }); // Tests that the expected permission warnings are generated for various // combinations of host permissions. add_task(async function host_permissions() { - let { PluralForm } = ChromeUtils.import( - "resource://gre/modules/PluralForm.jsm" - ); - let permissionTestCases = [ { description: "Empty manifest without permissions", @@ -167,7 +174,7 @@ add_task(async function host_permissions() { }, expectedOrigins: [""], expectedWarnings: [ - bundle.GetStringFromName("webextPerms.hostDescription.allUrls"), + l10n.formatValueSync("webext-perms-host-description-all-urls"), ], }, { @@ -177,7 +184,7 @@ add_task(async function host_permissions() { }, expectedOrigins: ["file://*/"], expectedWarnings: [ - bundle.GetStringFromName("webextPerms.hostDescription.allUrls"), + l10n.formatValueSync("webext-perms-host-description-all-urls"), ], }, { @@ -187,7 +194,7 @@ add_task(async function host_permissions() { }, expectedOrigins: ["http://*/"], expectedWarnings: [ - bundle.GetStringFromName("webextPerms.hostDescription.allUrls"), + l10n.formatValueSync("webext-perms-host-description-all-urls"), ], }, { @@ -197,7 +204,7 @@ add_task(async function host_permissions() { }, expectedOrigins: ["*://*/"], expectedWarnings: [ - bundle.GetStringFromName("webextPerms.hostDescription.allUrls"), + l10n.formatValueSync("webext-perms-host-description-all-urls"), ], }, { @@ -214,7 +221,7 @@ add_task(async function host_permissions() { }, expectedOrigins: ["https://*/"], expectedWarnings: [ - bundle.GetStringFromName("webextPerms.hostDescription.allUrls"), + l10n.formatValueSync("webext-perms-host-description-all-urls"), ], }, { @@ -223,18 +230,21 @@ add_task(async function host_permissions() { permissions: ["http://a/", "http://*.b/", "http://c/*"], }, expectedOrigins: ["http://a/", "http://*.b/", "http://c/*"], - expectedWarnings: [ + expectedWarnings: l10n.formatValuesSync([ // Wildcard hosts take precedence in the permission list. - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "b", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", [ - "a", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", [ - "c", - ]), - ], + { + id: "webext-perms-host-description-wildcard", + args: { domain: "b" }, + }, + { + id: "webext-perms-host-description-one-site", + args: { domain: "a" }, + }, + { + id: "webext-perms-host-description-one-site", + args: { domain: "c" }, + }, + ]), }, { description: "many host permission", @@ -262,34 +272,41 @@ add_task(async function host_permissions() { "http://*.3/", "http://*.4/", ], - expectedWarnings: [ + expectedWarnings: l10n.formatValuesSync([ // Wildcard hosts take precedence in the permission list. - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "1", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "2", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "3", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "4", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", [ - "a", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", [ - "b", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", [ - "c", - ]), - PluralForm.get( - 2, - bundle.GetStringFromName("webextPerms.hostDescription.tooManySites") - ).replace("#1", "2"), - ], + { + id: "webext-perms-host-description-wildcard", + args: { domain: "1" }, + }, + { + id: "webext-perms-host-description-wildcard", + args: { domain: "2" }, + }, + { + id: "webext-perms-host-description-wildcard", + args: { domain: "3" }, + }, + { + id: "webext-perms-host-description-wildcard", + args: { domain: "4" }, + }, + { + id: "webext-perms-host-description-one-site", + args: { domain: "a" }, + }, + { + id: "webext-perms-host-description-one-site", + args: { domain: "b" }, + }, + { + id: "webext-perms-host-description-one-site", + args: { domain: "c" }, + }, + { + id: "webext-perms-host-description-too-many-sites", + args: { domainCount: 2 }, + }, + ]), options: { collapseOrigins: true, }, @@ -323,38 +340,18 @@ add_task(async function host_permissions() { "http://*.4/", "http://*.5/", ], - expectedWarnings: [ - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "1", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "2", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "3", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "4", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "5", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", [ - "a", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", [ - "b", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", [ - "c", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", [ - "d", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", [ - "e", - ]), - ], + expectedWarnings: l10n.formatValuesSync([ + { id: "webext-perms-host-description-wildcard", args: { domain: "1" } }, + { id: "webext-perms-host-description-wildcard", args: { domain: "2" } }, + { id: "webext-perms-host-description-wildcard", args: { domain: "3" } }, + { id: "webext-perms-host-description-wildcard", args: { domain: "4" } }, + { id: "webext-perms-host-description-wildcard", args: { domain: "5" } }, + { id: "webext-perms-host-description-one-site", args: { domain: "a" } }, + { id: "webext-perms-host-description-one-site", args: { domain: "b" } }, + { id: "webext-perms-host-description-one-site", args: { domain: "c" } }, + { id: "webext-perms-host-description-one-site", args: { domain: "d" } }, + { id: "webext-perms-host-description-one-site", args: { domain: "e" } }, + ]), }, ]; for (let manifest_version of [2, 3]) { @@ -423,24 +420,18 @@ add_task(async function api_permissions() { deepEqual( getPermissionWarnings(manifestPermissions), - [ + l10n.formatValuesSync([ // Host permissions first, with wildcards on top. - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "x", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "tld", - ]), - bundle.formatStringFromName("webextPerms.hostDescription.oneSite", ["x"]), + { id: "webext-perms-host-description-wildcard", args: { domain: "x" } }, + { id: "webext-perms-host-description-wildcard", args: { domain: "tld" } }, + { id: "webext-perms-host-description-one-site", args: { domain: "x" } }, // nativeMessaging permission warning first of all permissions. - bundle.formatStringFromName("webextPerms.description.nativeMessaging", [ - DUMMY_APP_NAME, - ]), + "webext-perms-description-nativeMessaging", // Other permissions in alphabetical order. // Note: activeTab has no permission warning string. - bundle.GetStringFromName("webextPerms.description.tabs"), - bundle.GetStringFromName("webextPerms.description.webNavigation"), - ], + "webext-perms-description-tabs", + "webext-perms-description-webNavigation", + ]), "Expected warnings" ); }); @@ -492,14 +483,10 @@ add_task( deepEqual( getPermissionWarnings(manifestPermissions), - [ - bundle.GetStringFromName( - "webextPerms.description.declarativeNetRequest" - ), - bundle.GetStringFromName( - "webextPerms.description.declarativeNetRequestFeedback" - ), - ], + l10n.formatValuesSync([ + "webext-perms-description-declarativeNetRequest", + "webext-perms-description-declarativeNetRequestFeedback", + ]), "Expected warnings" ); } @@ -553,7 +540,7 @@ add_task(async function privileged_with_mozillaAddons() { deepEqual( getPermissionWarnings(manifestPermissions), - [bundle.GetStringFromName("webextPerms.hostDescription.allUrls")], + [l10n.formatValueSync("webext-perms-host-description-all-urls")], "Expected warnings for privileged add-on with mozillaAddons permission." ); }); @@ -584,7 +571,11 @@ add_task(async function unprivileged_with_mozillaAddons() { deepEqual( getPermissionWarnings(manifestPermissions), - [bundle.formatStringFromName("webextPerms.hostDescription.oneSite", ["a"])], + [ + l10n.formatValueSync("webext-perms-host-description-one-site", { + domain: "a", + }), + ], "Expected warnings for unprivileged add-on with mozillaAddons permission." ); }); @@ -671,14 +662,10 @@ add_task(async function update_change_permissions() { ); deepEqual( warnings, - [ - bundle.formatStringFromName("webextPerms.hostDescription.wildcard", [ - "c", - ]), - bundle.formatStringFromName("webextPerms.description.proxy", [ - DUMMY_APP_NAME, - ]), - ], + l10n.formatValuesSync([ + { id: "webext-perms-host-description-wildcard", args: { domain: "c" } }, + "webext-perms-description-proxy", + ]), "Expected permission warnings for new permissions only" ); }); @@ -702,7 +689,11 @@ add_task(async function update_privileged_with_mozillaAddons() { ); deepEqual( warnings, - [bundle.formatStringFromName("webextPerms.hostDescription.oneSite", ["b"])], + [ + l10n.formatValueSync("webext-perms-host-description-one-site", { + domain: "b", + }), + ], "Expected permission warnings for new host only" ); }); diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js index 25bcfaaa0e00..8e07fde1912b 100644 --- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js @@ -20,7 +20,13 @@ ChromeUtils.defineModuleGetter( "resource://gre/modules/ExtensionParent.jsm" ); -const BROWSER_PROPERTIES = "chrome://browser/locale/browser.properties"; +const l10n = new Localization([ + "toolkit/global/extensions.ftl", + "toolkit/global/extensionPermissions.ftl", + "branding/brand.ftl", +]); +// Localization resources need to be first iterated outside a test +l10n.formatValue("webext-perms-add"); AddonTestUtils.init(this); AddonTestUtils.overrideCertDB(); @@ -677,7 +683,7 @@ const GRANTED_WITHOUT_USER_PROMPT = [ "webRequestFilterResponse.serviceWorkerScript", ]; -add_task(function test_permissions_have_localization_strings() { +add_task(async function test_permissions_have_localization_strings() { let noPromptNames = Schemas.getPermissionNames([ "PermissionNoPrompt", "OptionalPermissionNoPrompt", @@ -689,11 +695,10 @@ add_task(function test_permissions_have_localization_strings() { "List of no-prompt permissions is correct." ); - const bundle = Services.strings.createBundle(BROWSER_PROPERTIES); - for (const perm of Schemas.getPermissionNames()) { try { - const str = bundle.GetStringFromName(`webextPerms.description.${perm}`); + const permId = perm.replace(/\./g, "-"); + const str = await l10n.formatValue(`webext-perms-description-${permId}`); ok(str.length, `Found localization string for '${perm}' permission`); } catch (e) { diff --git a/toolkit/components/extensions/test/xpcshell/test_site_permissions.js b/toolkit/components/extensions/test/xpcshell/test_site_permissions.js index d16bc9216d21..58ac7e1003b5 100644 --- a/toolkit/components/extensions/test/xpcshell/test_site_permissions.js +++ b/toolkit/components/extensions/test/xpcshell/test_site_permissions.js @@ -28,10 +28,13 @@ AddonTestUtils.createAppInfo( "42" ); -const BROWSER_PROPERTIES = - AppConstants.MOZ_APP_NAME == "thunderbird" - ? "chrome://messenger/locale/addons.properties" - : "chrome://browser/locale/browser.properties"; +const l10n = new Localization([ + "toolkit/global/extensions.ftl", + "toolkit/global/extensionPermissions.ftl", + "branding/brand.ftl", +]); +// Localization resources need to be first iterated outside a test +l10n.formatValue("webext-perms-add"); // Lazily import ExtensionParent to allow AddonTestUtils.createAppInfo to // override Services.appinfo. @@ -371,13 +374,10 @@ add_task(async function test_site_permissions_have_localization_strings() { ]); ok(SCHEMA_SITE_PERMISSIONS.length, "we have site permissions"); - const bundle = Services.strings.createBundle(BROWSER_PROPERTIES); - for (const perm of SCHEMA_SITE_PERMISSIONS) { + const l10nId = `webext-site-perms-${perm}`; try { - const str = bundle.GetStringFromName( - `webextSitePerms.description.${perm}` - ); + const str = await l10n.formatValue(l10nId); ok(str.length, `Found localization string for '${perm}' site permission`); } catch (e) { diff --git a/toolkit/locales/en-US/toolkit/global/extensionPermissions.ftl b/toolkit/locales/en-US/toolkit/global/extensionPermissions.ftl new file mode 100644 index 000000000000..1938e062fd41 --- /dev/null +++ b/toolkit/locales/en-US/toolkit/global/extensionPermissions.ftl @@ -0,0 +1,32 @@ +# 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/. + +## Extension permission description keys are derived from permission names. +## Permissions for which the message has been changed and the key updated +## must have a corresponding entry in the `PERMISSION_L10N_ID_OVERRIDES` map. + +webext-perms-description-bookmarks = Read and modify bookmarks +webext-perms-description-browserSettings = Read and modify browser settings +webext-perms-description-browsingData = Clear recent browsing history, cookies, and related data +webext-perms-description-clipboardRead = Get data from the clipboard +webext-perms-description-clipboardWrite = Input data to the clipboard +webext-perms-description-declarativeNetRequest = Block content on any page +webext-perms-description-declarativeNetRequestFeedback = Read your browsing history +webext-perms-description-devtools = Extend developer tools to access your data in open tabs +webext-perms-description-downloads = Download files and read and modify the browser’s download history +webext-perms-description-downloads-open = Open files downloaded to your computer +webext-perms-description-find = Read the text of all open tabs +webext-perms-description-geolocation = Access your location +webext-perms-description-history = Access browsing history +webext-perms-description-management = Monitor extension usage and manage themes +webext-perms-description-nativeMessaging = Exchange messages with programs other than { -brand-short-name } +webext-perms-description-notifications = Display notifications to you +webext-perms-description-pkcs11 = Provide cryptographic authentication services +webext-perms-description-privacy = Read and modify privacy settings +webext-perms-description-proxy = Control browser proxy settings +webext-perms-description-sessions = Access recently closed tabs +webext-perms-description-tabs = Access browser tabs +webext-perms-description-tabHide = Hide and show browser tabs +webext-perms-description-topSites = Access browsing history +webext-perms-description-webNavigation = Access browser activity during navigation diff --git a/toolkit/locales/en-US/toolkit/global/extensions.ftl b/toolkit/locales/en-US/toolkit/global/extensions.ftl new file mode 100644 index 000000000000..32f0bbe3843a --- /dev/null +++ b/toolkit/locales/en-US/toolkit/global/extensions.ftl @@ -0,0 +1,111 @@ +# 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/. + +## Headers used in the webextension permissions dialog, +## See https://bug1308309.bmoattachments.org/attachment.cgi?id=8814612 +## for an example of the full dialog. +## Note: This string will be used as raw markup. Avoid characters like <, >, & +## Variables: +## $extension (String): replaced with the localized name of the extension. + +webext-perms-header = Add { $extension }? +webext-perms-header-with-perms = Add { $extension }? This extension will have permission to: +webext-perms-header-unsigned = Add { $extension }? This extension is unverified. Malicious extensions can steal your private information or compromise your computer. Only add it if you trust the source. +webext-perms-header-unsigned-with-perms = Add { $extension }? This extension is unverified. Malicious extensions can steal your private information or compromise your computer. Only add it if you trust the source. This extension will have permission to: +webext-perms-sideload-header = { $extension } added +webext-perms-optional-perms-header = { $extension } requests additional permissions. + +## + +webext-perms-add = + .label = Add + .accesskey = A +webext-perms-cancel = + .label = Cancel + .accesskey = C + +webext-perms-sideload-text = Another program on your computer installed an add-on that may affect your browser. Please review this add-on’s permissions requests and choose to Enable or Cancel (to leave it disabled). +webext-perms-sideload-text-no-perms = Another program on your computer installed an add-on that may affect your browser. Please choose to Enable or Cancel (to leave it disabled). +webext-perms-sideload-enable = + .label = Enable + .accesskey = E +webext-perms-sideload-cancel = + .label = Cancel + .accesskey = C + +# Variables: +# $extension (String): replaced with the localized name of the extension. +webext-perms-update-text = { $extension } has been updated. You must approve new permissions before the updated version will install. Choosing “Cancel” will maintain your current extension version. This extension will have permission to: +webext-perms-update-accept = + .label = Update + .accesskey = U + +webext-perms-optional-perms-list-intro = It wants to: +webext-perms-optional-perms-allow = + .label = Allow + .accesskey = A +webext-perms-optional-perms-deny = + .label = Deny + .accesskey = D + +webext-perms-host-description-all-urls = Access your data for all websites + +# Variables: +# $domain (String): will be replaced by the DNS domain for which a webextension is requesting access (e.g., mozilla.org) +webext-perms-host-description-wildcard = Access your data for sites in the { $domain } domain + +# Variables: +# $domainCount (Number): Integer indicating the number of additional +# hosts for which this webextension is requesting permission. +webext-perms-host-description-too-many-wildcards = + { $domainCount -> + [one] Access your data in { $domainCount } other domain + *[other] Access your data in { $domainCount } other domains + } +# Variables: +# $domain (String): will be replaced by the DNS host name for which a webextension is requesting access (e.g., www.mozilla.org) +webext-perms-host-description-one-site = Access your data for { $domain } + +# Variables: +# $domainCount (Number): Integer indicating the number of additional +# hosts for which this webextension is requesting permission. +webext-perms-host-description-too-many-sites = + { $domainCount -> + [one] Access your data on { $domainCount } other site + *[other] Access your data on { $domainCount } other sites + } + +## Headers used in the webextension permissions dialog for synthetic add-ons. +## The part of the string describing what privileges the extension gives should be consistent +## with the value of webext-site-perms-description-gated-perms-{sitePermission}. +## Note, this string will be used as raw markup. Avoid characters like <, >, & +## Variables: +## $hostname (String): the hostname of the site the add-on is being installed from. + +webext-site-perms-header-with-gated-perms-midi = This add-on gives { $hostname } access to your MIDI devices. +webext-site-perms-header-with-gated-perms-midi-sysex = This add-on gives { $hostname } access to your MIDI devices (with SysEx support). + +## + +# This string is used as description in the webextension permissions dialog for synthetic add-ons. +# Note, the empty line is used to create a line break between the two sections. +# Note, this string will be used as raw markup. Avoid characters like <, >, & +webext-site-perms-description-gated-perms-midi = + These are usually plug-in devices like audio synthesizers, but might also be built into your computer. + + Websites are normally not allowed to access MIDI devices. Improper usage could cause damage or compromise security. + +## Headers used in the webextension permissions dialog. +## Note: This string will be used as raw markup. Avoid characters like <, >, & +## Variables: +## $extension (String): replaced with the localized name of the extension being installed. +## $hostname (String): will be replaced by the DNS host name for which a webextension enables permissions. + +webext-site-perms-header-with-perms = Add { $extension }? This extension grants the following capabilities to { $hostname }: +webext-site-perms-header-unsigned-with-perms = Add { $extension }? This extension is unverified. Malicious extensions can steal your private information or compromise your computer. Only add it if you trust the source. This extension grants the following capabilities to { $hostname }: + +## These should remain in sync with permissions.NAME.label in sitePermissions.properties + +webext-site-perms-midi = Access MIDI devices +webext-site-perms-midi-sysex = Access MIDI devices with SysEx support diff --git a/toolkit/mozapps/extensions/content/aboutaddons.js b/toolkit/mozapps/extensions/content/aboutaddons.js index afa053f98b95..3cbc33b97c91 100644 --- a/toolkit/mozapps/extensions/content/aboutaddons.js +++ b/toolkit/mozapps/extensions/content/aboutaddons.js @@ -25,16 +25,6 @@ XPCOMUtils.defineLazyModuleGetters(this, { ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm", }); -XPCOMUtils.defineLazyGetter(this, "browserBundle", () => { - return Services.strings.createBundle( - "chrome://browser/locale/browser.properties" - ); -}); -XPCOMUtils.defineLazyGetter(this, "brandBundle", () => { - return Services.strings.createBundle( - "chrome://branding/locale/brand.properties" - ); -}); XPCOMUtils.defineLazyGetter(this, "extensionStylesheets", () => { const { ExtensionParent } = ChromeUtils.import( "resource://gre/modules/ExtensionParent.jsm" @@ -1945,8 +1935,6 @@ class AddonPermissionsList extends HTMLElement { } async render() { - let appName = brandBundle.GetStringFromName("brandShortName"); - let empty = { origins: [], permissions: [] }; let requiredPerms = { ...(this.addon.userPermissions ?? empty) }; let optionalPerms = { ...(this.addon.optionalPermissions ?? empty) }; @@ -1966,9 +1954,7 @@ class AddonPermissionsList extends HTMLElement { { permissions: requiredPerms, optionalPermissions: optionalPerms, - appName, }, - browserBundle, { buildOptionalOrigins: manifestV3enabled } ); let optionalEntries = [ @@ -2049,15 +2035,10 @@ class AddonSitePermissionsList extends HTMLElement { } async render() { - let appName = brandBundle.GetStringFromName("brandShortName"); - let permissions = Extension.formatPermissionStrings( - { - sitePermissions: this.addon.sitePermissions, - siteOrigin: this.addon.siteOrigin, - appName, - }, - browserBundle - ); + let permissions = Extension.formatPermissionStrings({ + sitePermissions: this.addon.sitePermissions, + siteOrigin: this.addon.siteOrigin, + }); this.textContent = ""; let frag = importTemplate("addon-sitepermissions-list"); diff --git a/tools/lint/fluent-lint/exclusions.yml b/tools/lint/fluent-lint/exclusions.yml index 38acfa2834a3..ebcd1b00297e 100644 --- a/tools/lint/fluent-lint/exclusions.yml +++ b/tools/lint/fluent-lint/exclusions.yml @@ -82,6 +82,9 @@ ID01: # policies-descriptions.ftl: These IDs are generated programmatically # from policy names. - browser/locales/en-US/browser/policies/policies-descriptions.ftl + # The webext-perms-description-* IDs are generated programmatically + # from permission names + - toolkit/locales/en-US/toolkit/global/extensionPermissions.ftl ID02: messages: # browser/components/ion/content/ion.ftl @@ -185,6 +188,8 @@ CO01: - about-telemetry-firefox-data-doc - about-telemetry-telemetry-client-doc - about-telemetry-telemetry-dashboard + # toolkit/locales/en-US/toolkit/global/extensionPermissions.ftl + - webext-perms-description-management # toolkit/locales/en-US/toolkit/global/processTypes.ftl - process-type-privilegedmozilla files: