Bug 1957729 - Show data collection permissions in the update prompt. r=rpl,fluent-reviewers,bolsson
Differential Revision: https://phabricator.services.mozilla.com/D244067
This commit is contained in:
@@ -237,7 +237,11 @@ export var ExtensionsUI = {
|
||||
let strings = this._buildStrings(info);
|
||||
|
||||
// If this is an update with no promptable permissions, just apply it
|
||||
if (info.type == "update" && !strings.msgs.length) {
|
||||
if (
|
||||
info.type == "update" &&
|
||||
!strings.msgs.length &&
|
||||
!strings.dataCollectionPermissions?.msg
|
||||
) {
|
||||
info.resolve();
|
||||
return;
|
||||
}
|
||||
@@ -283,7 +287,7 @@ export var ExtensionsUI = {
|
||||
let strings = this._buildStrings(info);
|
||||
|
||||
// If we don't prompt for any new permissions, just apply it
|
||||
if (!strings.msgs.length) {
|
||||
if (!strings.msgs.length && !strings.dataCollectionPermissions?.msg) {
|
||||
info.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1304,6 +1304,8 @@ export class ExtensionData {
|
||||
* Returns additional permissions that extensions is requesting based on its
|
||||
* manifest. For now, this is host_permissions (and content scripts) in mv3,
|
||||
* and the "technicalAndInteraction" optional data collection permission.
|
||||
*
|
||||
* @returns {null | Permissions}
|
||||
*/
|
||||
getRequestedPermissions() {
|
||||
if (this.type !== "extension") {
|
||||
@@ -1336,7 +1338,9 @@ export class ExtensionData {
|
||||
|
||||
/**
|
||||
* Returns optional permissions from the manifest, including host permissions
|
||||
* if originControls is true.
|
||||
* if originControls is true, and optional data collection (if enabled).
|
||||
*
|
||||
* @returns {null | Permissions}
|
||||
*/
|
||||
get manifestOptionalPermissions() {
|
||||
if (this.type !== "extension") {
|
||||
@@ -1353,11 +1357,14 @@ export class ExtensionData {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Bug 1955990 - add support for data collection permissions.
|
||||
const data_collection = lazy.dataCollectionPermissionsEnabled
|
||||
? this.getDataCollectionPermissions().optional
|
||||
: [];
|
||||
|
||||
return {
|
||||
permissions: Array.from(permissions),
|
||||
origins: Array.from(origins),
|
||||
data_collection,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1413,6 +1420,9 @@ export class ExtensionData {
|
||||
permissions: newPermissions.permissions.filter(
|
||||
perm => !oldPermissions.permissions.includes(perm)
|
||||
),
|
||||
data_collection: newPermissions.data_collection.filter(
|
||||
perm => newPermissions.data_collection.includes(perm) && perm !== "none"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1431,6 +1441,9 @@ export class ExtensionData {
|
||||
permissions: oldPermissions.permissions.filter(perm =>
|
||||
newPermissions.permissions.includes(perm)
|
||||
),
|
||||
data_collection: oldPermissions.data_collection.filter(
|
||||
perm => newPermissions.data_collection.includes(perm) && perm !== "none"
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2865,7 +2878,10 @@ export class ExtensionData {
|
||||
permissions.data_collection?.length
|
||||
) {
|
||||
result.dataCollectionPermissions =
|
||||
this._formatDataCollectionPermissions(permissions.data_collection);
|
||||
this._formatDataCollectionPermissions(
|
||||
permissions.data_collection,
|
||||
type
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2945,10 +2961,17 @@ export class ExtensionData {
|
||||
: "webext-perms-sideload-text-no-perms"
|
||||
);
|
||||
break;
|
||||
case "update":
|
||||
case "update": {
|
||||
if (!lazy.dataCollectionPermissionsEnabled) {
|
||||
headerId = "webext-perms-update-text";
|
||||
} else {
|
||||
headerId = hasDataCollectionOnly
|
||||
? "webext-perms-update-data-collection-only-text"
|
||||
: "webext-perms-update-data-collection-text";
|
||||
}
|
||||
acceptId = "webext-perms-update-accept";
|
||||
break;
|
||||
}
|
||||
case "optional":
|
||||
headerId = "webext-perms-optional-perms-header";
|
||||
acceptId = "webext-perms-optional-perms-allow";
|
||||
@@ -2981,7 +3004,7 @@ export class ExtensionData {
|
||||
* @returns {{msg: string, collectsTechnicalAndInteractionData: boolean}} An
|
||||
* object with information about data collection permissions for the UI.
|
||||
*/
|
||||
static _formatDataCollectionPermissions(dataPermissions) {
|
||||
static _formatDataCollectionPermissions(dataPermissions, type) {
|
||||
const dataCollectionPermissions = {};
|
||||
const permissions = new Set(dataPermissions);
|
||||
|
||||
@@ -3011,8 +3034,17 @@ export class ExtensionData {
|
||||
}
|
||||
}
|
||||
|
||||
let id;
|
||||
switch (type) {
|
||||
case "update":
|
||||
id = "webext-perms-description-data-some-update";
|
||||
break;
|
||||
default:
|
||||
id = "webext-perms-description-data-some";
|
||||
}
|
||||
|
||||
const fluentIdAndArgs = {
|
||||
id: "webext-perms-description-data-some",
|
||||
id,
|
||||
args: {
|
||||
permissions: new Intl.ListFormat(undefined, {
|
||||
style: "narrow",
|
||||
|
||||
@@ -14,6 +14,17 @@ webext-perms-description-data-none = The developer says this extension doesn’t
|
||||
# $permissions (String): a list of data collection permissions formatted with `Intl.ListFormat` using the "narrow" style.
|
||||
webext-perms-description-data-some = The developer says this extension collects: { $permissions }.
|
||||
|
||||
# Variables:
|
||||
# $permissions (String): a list of data collection permissions formatted with `Intl.ListFormat` using the "narrow" style.
|
||||
webext-perms-description-data-some-update = The developer says the extension will collect: { $permissions }.
|
||||
# Variables:
|
||||
# $extension (String): replaced with the localized name of the extension.
|
||||
webext-perms-update-data-collection-text = { $extension } has been updated. You must approve new settings before the updated version will install. Choosing “Cancel” will maintain your current extension version. This extension will have permission to:
|
||||
|
||||
# Variables:
|
||||
# $extension (String): replaced with the localized name of the extension.
|
||||
webext-perms-update-data-collection-only-text = { $extension } has been updated. You must approve new settings before the updated version will install. Choosing “Cancel” will maintain your current extension version.
|
||||
|
||||
## Short form to be used in lists or in a string (`webext-perms-description-data-some`)
|
||||
## that formats some of these permissions below using `Intl.ListFormat`.
|
||||
##
|
||||
|
||||
@@ -1248,7 +1248,11 @@ var AddonManagerInternal = {
|
||||
let difference = lazy.Extension.comparePermissions(oldPerms, newPerms);
|
||||
|
||||
// If there are no new permissions, just go ahead with the update
|
||||
if (!difference.origins.length && !difference.permissions.length) {
|
||||
if (
|
||||
!difference.origins.length &&
|
||||
!difference.permissions.length &&
|
||||
!difference.data_collection.length
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,11 @@ function installPromptHandler(info) {
|
||||
let difference = Extension.comparePermissions(oldPerms, newPerms);
|
||||
|
||||
// If there are no new permissions, just proceed
|
||||
if (!difference.origins.length && !difference.permissions.length) {
|
||||
if (
|
||||
!difference.origins.length &&
|
||||
!difference.permissions.length &&
|
||||
!difference.data_collection.length
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ const { AddonTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/AddonTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
ChromeUtils.defineESModuleGetters(this, {
|
||||
PERMISSION_L10N: "resource://gre/modules/ExtensionPermissionMessages.sys.mjs",
|
||||
});
|
||||
|
||||
AddonTestUtils.initMochitest(this);
|
||||
|
||||
const server = AddonTestUtils.createHttpServer();
|
||||
@@ -49,6 +53,7 @@ add_setup(async function () {
|
||||
function createTestExtension({
|
||||
id = "test-pending-update@test",
|
||||
newManifest = {},
|
||||
oldManifest = {},
|
||||
}) {
|
||||
function background() {
|
||||
browser.runtime.onUpdateAvailable.addListener(() => {
|
||||
@@ -64,6 +69,7 @@ function createTestExtension({
|
||||
|
||||
const manifest = {
|
||||
name: "Test Pending Update",
|
||||
...oldManifest,
|
||||
browser_specific_settings: {
|
||||
gecko: { id, update_url },
|
||||
},
|
||||
@@ -72,7 +78,16 @@ function createTestExtension({
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
background,
|
||||
manifest,
|
||||
manifest: {
|
||||
...oldManifest,
|
||||
...manifest,
|
||||
browser_specific_settings: {
|
||||
gecko: {
|
||||
...(oldManifest.browser_specific_settings?.gecko ?? {}),
|
||||
...manifest.browser_specific_settings.gecko,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Use permanent so the add-on can be updated.
|
||||
useAddonManager: "permanent",
|
||||
});
|
||||
@@ -309,3 +324,270 @@ add_task(async function test_pending_update_no_prompted_permission() {
|
||||
await closeView(win);
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function test_pending_update_with_prompted_permission() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["extensions.dataCollectionPermissions.enabled", true]],
|
||||
});
|
||||
|
||||
const TEST_CASES = [
|
||||
{
|
||||
title: "With data collection",
|
||||
data_collection_permissions: {
|
||||
required: ["locationInfo"],
|
||||
},
|
||||
verifyDialog(popupContentEl, { extensionId }) {
|
||||
Assert.equal(
|
||||
popupContentEl.querySelector(".popup-notification-description")
|
||||
.textContent,
|
||||
PERMISSION_L10N.formatValueSync(
|
||||
"webext-perms-update-data-collection-only-text",
|
||||
{ extension: extensionId }
|
||||
),
|
||||
"Expected header string without perms"
|
||||
);
|
||||
Assert.equal(
|
||||
popupContentEl.permsListEl.childElementCount,
|
||||
1,
|
||||
"Expected a permission entry in the list"
|
||||
);
|
||||
Assert.ok(
|
||||
popupContentEl.permsListEl.querySelector(
|
||||
"li.webext-data-collection-perm-granted"
|
||||
),
|
||||
"Expected data collection item"
|
||||
);
|
||||
Assert.equal(
|
||||
popupContentEl.permsListEl.firstChild.textContent,
|
||||
PERMISSION_L10N.formatValueSync(
|
||||
"webext-perms-description-data-some-update",
|
||||
{
|
||||
permissions: "location",
|
||||
}
|
||||
),
|
||||
"Expected formatted data collection permission string"
|
||||
);
|
||||
Assert.ok(
|
||||
popupContentEl.hasAttribute("learnmoreurl"),
|
||||
"Expected a learn more link"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "With data collection and required permission",
|
||||
permissions: ["bookmarks"],
|
||||
data_collection_permissions: {
|
||||
required: ["locationInfo", "healthInfo"],
|
||||
},
|
||||
verifyDialog(popupContentEl, { extensionId }) {
|
||||
Assert.equal(
|
||||
popupContentEl.querySelector(".popup-notification-description")
|
||||
.textContent,
|
||||
PERMISSION_L10N.formatValueSync(
|
||||
"webext-perms-update-data-collection-text",
|
||||
{ extension: extensionId }
|
||||
),
|
||||
"Expected header string with perms"
|
||||
);
|
||||
Assert.equal(
|
||||
popupContentEl.permsListEl.childElementCount,
|
||||
2,
|
||||
"Expected two permission entries in the list"
|
||||
);
|
||||
Assert.equal(
|
||||
popupContentEl.permsListEl.firstChild.textContent,
|
||||
PERMISSION_L10N.formatValueSync("webext-perms-description-bookmarks"),
|
||||
"Expected formatted permission string"
|
||||
);
|
||||
Assert.equal(
|
||||
popupContentEl.permsListEl.lastChild.textContent,
|
||||
PERMISSION_L10N.formatValueSync(
|
||||
"webext-perms-description-data-some-update",
|
||||
{
|
||||
permissions: "location, health information",
|
||||
}
|
||||
),
|
||||
"Expected formatted data collection permission string"
|
||||
);
|
||||
Assert.ok(
|
||||
popupContentEl.hasAttribute("learnmoreurl"),
|
||||
"Expected a learn more link"
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "With data collection changing from required to optional",
|
||||
permissions: ["bookmarks"],
|
||||
old_data_collection_permissions: {
|
||||
required: ["bookmarksInfo"],
|
||||
},
|
||||
data_collection_permissions: {
|
||||
required: ["locationInfo", "healthInfo"],
|
||||
optional: ["bookmarksInfo"],
|
||||
},
|
||||
verifyDialog(popupContentEl, { extensionId }) {
|
||||
Assert.equal(
|
||||
popupContentEl.querySelector(".popup-notification-description")
|
||||
.textContent,
|
||||
PERMISSION_L10N.formatValueSync(
|
||||
"webext-perms-update-data-collection-text",
|
||||
{ extension: extensionId }
|
||||
),
|
||||
"Expected header string with perms"
|
||||
);
|
||||
Assert.equal(
|
||||
popupContentEl.permsListEl.childElementCount,
|
||||
2,
|
||||
"Expected two permission entries in the list"
|
||||
);
|
||||
Assert.equal(
|
||||
popupContentEl.permsListEl.childNodes[0].textContent,
|
||||
PERMISSION_L10N.formatValueSync("webext-perms-description-bookmarks"),
|
||||
"Expected formatted bookmarks permission string"
|
||||
);
|
||||
Assert.equal(
|
||||
popupContentEl.permsListEl.childNodes[1].textContent,
|
||||
PERMISSION_L10N.formatValueSync(
|
||||
"webext-perms-description-data-some-update",
|
||||
{
|
||||
permissions: "location, health information",
|
||||
}
|
||||
),
|
||||
"Expected formatted data collection permission string"
|
||||
);
|
||||
Assert.ok(
|
||||
popupContentEl.hasAttribute("learnmoreurl"),
|
||||
"Expected a learn more link"
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const {
|
||||
title,
|
||||
permissions,
|
||||
data_collection_permissions,
|
||||
old_data_collection_permissions,
|
||||
verifyDialog,
|
||||
} of TEST_CASES) {
|
||||
info(title);
|
||||
|
||||
const id = `@${title.toLowerCase().replaceAll(/[^\w]+/g, "-")}`;
|
||||
const { extension } = createTestExtension({
|
||||
id,
|
||||
oldManifest: {
|
||||
browser_specific_settings: {
|
||||
gecko: {
|
||||
...(old_data_collection_permissions
|
||||
? { data_collection_permissions: old_data_collection_permissions }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
newManifest: {
|
||||
name: id,
|
||||
permissions,
|
||||
browser_specific_settings: {
|
||||
gecko: {
|
||||
id,
|
||||
data_collection_permissions,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("bgpage-ready");
|
||||
const win = await loadInitialView("extension");
|
||||
|
||||
const dialogPromise = promisePopupNotificationShown(
|
||||
"addon-webext-permissions"
|
||||
);
|
||||
// This `promptPromise` is retrieving data from the prompt internals, while
|
||||
// the `dialogPromise` will return the actual dialog element.
|
||||
const promptPromise = promisePermissionPrompt(id);
|
||||
win.checkForUpdates();
|
||||
const [popupContentEl, infoProps] = await Promise.all([
|
||||
dialogPromise,
|
||||
promptPromise,
|
||||
]);
|
||||
|
||||
verifyDialog(popupContentEl, { extensionId: id });
|
||||
|
||||
// Confirm the update, and proceed.
|
||||
const waitForManagementUpdate = new Promise(resolve => {
|
||||
const { Management } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/Extension.sys.mjs"
|
||||
);
|
||||
Management.once("update", resolve);
|
||||
});
|
||||
infoProps.resolve();
|
||||
await promiseUpdateAvailable(extension);
|
||||
await completePostponedUpdate({ id, win });
|
||||
// Ensure that the bootstrap scope update method has been executed
|
||||
// successfully and emitted the update Management event.
|
||||
info("Wait for the Management update to be emitted");
|
||||
await waitForManagementUpdate;
|
||||
|
||||
await closeView(win);
|
||||
await extension.unload();
|
||||
}
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
add_task(async function test_pending_update_with_no_prompted_permission() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["extensions.dataCollectionPermissions.enabled", true]],
|
||||
});
|
||||
|
||||
const TEST_CASES = [
|
||||
{
|
||||
title: "No data collection",
|
||||
data_collection_permissions: {},
|
||||
},
|
||||
{
|
||||
title: "Explicit no data collection",
|
||||
data_collection_permissions: {
|
||||
required: ["none"],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Optional data collection",
|
||||
data_collection_permissions: {
|
||||
optional: ["technicalAndInteraction"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const { title, data_collection_permissions } of TEST_CASES) {
|
||||
info(title);
|
||||
|
||||
const id = `@${title.toLowerCase().replaceAll(/[^\w]+/g, "-")}`;
|
||||
const { extension } = createTestExtension({
|
||||
id,
|
||||
newManifest: {
|
||||
name: id,
|
||||
browser_specific_settings: {
|
||||
gecko: {
|
||||
id,
|
||||
data_collection_permissions,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("bgpage-ready");
|
||||
let win = await loadInitialView("extension");
|
||||
|
||||
win.checkForUpdates();
|
||||
await promiseUpdateAvailable(extension);
|
||||
await completePostponedUpdate({ id, win });
|
||||
|
||||
await closeView(win);
|
||||
await extension.unload();
|
||||
}
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user