diff --git a/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl b/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
index f7e1ef7bb17f..98f61eb5404c 100644
--- a/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
+++ b/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
@@ -391,6 +391,7 @@ preferences-addon-button =
}
details-addon-button = Details
release-notes-addon-button = Release Notes
+permissions-addon-button = Permissions
addons-enabled-heading = Enabled
addons-disabled-heading = Disabled
@@ -452,3 +453,5 @@ recent-updates-heading = Recent Updates
release-notes-loading = Loading…
release-notes-error = Sorry, but there was an error loading the release notes.
+
+addon-permissions-empty = This extension doesn’t require any permissions
diff --git a/toolkit/mozapps/extensions/content/aboutaddons.css b/toolkit/mozapps/extensions/content/aboutaddons.css
index a5b377c72f07..9e3c7c24b2e1 100644
--- a/toolkit/mozapps/extensions/content/aboutaddons.css
+++ b/toolkit/mozapps/extensions/content/aboutaddons.css
@@ -362,6 +362,10 @@ panel-item-separator[hidden] {
text-decoration: none;
}
+addon-permissions-list > .addon-detail-row:first-of-type {
+ border-top: none;
+}
+
.deck-tab-group {
border-bottom: 1px solid var(--grey-90-a20);
border-top: 1px solid var(--grey-90-a20);
diff --git a/toolkit/mozapps/extensions/content/aboutaddons.html b/toolkit/mozapps/extensions/content/aboutaddons.html
index 07a73c457b06..159d3b25dd37 100644
--- a/toolkit/mozapps/extensions/content/aboutaddons.html
+++ b/toolkit/mozapps/extensions/content/aboutaddons.html
@@ -87,6 +87,7 @@
+
@@ -156,6 +157,7 @@
+
diff --git a/toolkit/mozapps/extensions/content/aboutaddons.js b/toolkit/mozapps/extensions/content/aboutaddons.js
index 65dc975d6610..ac442851de45 100644
--- a/toolkit/mozapps/extensions/content/aboutaddons.js
+++ b/toolkit/mozapps/extensions/content/aboutaddons.js
@@ -18,6 +18,15 @@ XPCOMUtils.defineLazyModuleGetters(this, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.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.defineLazyPreferenceGetter(
this, "allowPrivateBrowsingByDefault",
"extensions.allowPrivateBrowsingByDefault", true);
@@ -753,6 +762,49 @@ class UpdateReleaseNotes extends HTMLElement {
}
customElements.define("update-release-notes", UpdateReleaseNotes);
+class AddonPermissionsList extends HTMLElement {
+ setAddon(addon) {
+ this.addon = addon;
+ this.render();
+ }
+
+ render() {
+ let appName = brandBundle.GetStringFromName("brandShortName");
+ let {msgs} = Extension.formatPermissionStrings({
+ permissions: this.addon.userPermissions,
+ appName,
+ }, browserBundle);
+
+ this.textContent = "";
+
+ if (msgs.length > 0) {
+ // Add a row for each permission message.
+ for (let msg of msgs) {
+ let row = document.createElement("div");
+ row.classList.add("addon-detail-row", "permission-info");
+ row.textContent = msg;
+ this.appendChild(row);
+ }
+ } else {
+ let emptyMessage = document.createElement("div");
+ emptyMessage.classList.add("addon-detail-row");
+ document.l10n.setAttributes(emptyMessage, "addon-permissions-empty");
+ this.appendChild(emptyMessage);
+ }
+
+ // Add a learn more link.
+ let learnMoreRow = document.createElement("div");
+ learnMoreRow.classList.add("addon-detail-row");
+ let learnMoreLink = document.createElement("a");
+ learnMoreLink.setAttribute("target", "_blank");
+ learnMoreLink.href = SUPPORT_URL + "extension-permissions";
+ learnMoreLink.textContent =
+ browserBundle.GetStringFromName("webextPerms.learnMore");
+ learnMoreRow.appendChild(learnMoreLink);
+ this.appendChild(learnMoreRow);
+ }
+}
+customElements.define("addon-permissions-list", AddonPermissionsList);
class AddonDetails extends HTMLElement {
connectedCallback() {
@@ -793,6 +845,8 @@ class AddonDetails extends HTMLElement {
// Hide tab buttons that won't have any content.
let getButtonByName =
name => this.tabGroup.querySelector(`[name="${name}"]`);
+ let permsBtn = getButtonByName("permissions");
+ permsBtn.hidden = addon.type != "extension";
let notesBtn = getButtonByName("release-notes");
notesBtn.hidden = !this.releaseNotesUri;
@@ -828,6 +882,10 @@ class AddonDetails extends HTMLElement {
this.deck = this.querySelector("named-deck");
this.tabGroup = this.querySelector(".deck-tab-group");
+ // Set the add-on for the permissions section.
+ this.permissionsList = this.querySelector("addon-permissions-list");
+ this.permissionsList.setAddon(addon);
+
// Full description.
let description = this.querySelector(".addon-detail-description");
if (addon.getFullDescription) {
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
index 74b6eab651a1..241ec642165a 100644
--- a/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
@@ -61,6 +61,26 @@ function checkOptions(doc, options, expectedOptions) {
}
}
+function assertDeckHeadingHidden(group) {
+ ok(group.hidden, "The tab group is hidden");
+ for (let button of group.children) {
+ ok(button.offsetHeight == 0, `The ${button.name} is hidden`);
+ }
+}
+
+function assertDeckHeadingButtons(group, visibleButtons) {
+ ok(!group.hidden, "The tab group is shown");
+ ok(group.children.length >= visibleButtons.length,
+ `There should be at least ${visibleButtons.length} buttons`);
+ for (let button of group.children) {
+ if (visibleButtons.includes(button.name)) {
+ ok(!button.hidden, `The ${button.name} is shown`);
+ } else {
+ ok(button.hidden, `The ${button.name} is hidden`);
+ }
+ }
+}
+
async function hasPrivateAllowed(id) {
let perms = await ExtensionPermissions.get(id);
return perms.permissions.includes("internal:privateBrowsingAllowed");
@@ -83,6 +103,10 @@ add_task(async function enableHtmlViews() {
type: "extension",
contributionURL: "http://foo.com",
averageRating: 4.279,
+ userPermissions: {
+ origins: ["", "file://*/*"],
+ permissions: ["alarms", "contextMenus", "tabs", "webNavigation"],
+ },
reviewCount: 5,
reviewURL: "http://example.com/reviews",
homepageURL: "http://example.com/addon1",
@@ -93,6 +117,10 @@ add_task(async function enableHtmlViews() {
name: "Test add-on 2",
creator: {name: "I made it"},
description: "Short description",
+ userPermissions: {
+ origins: [],
+ permissions: ["alarms", "contextMenus"],
+ },
type: "extension",
}, {
id: "theme1@mochi.test",
@@ -284,6 +312,10 @@ add_task(async function testFullDetails() {
is(preview.hidden, true, "The preview is hidden");
let details = card.querySelector("addon-details");
+
+ // Check all the deck buttons are hidden.
+ assertDeckHeadingButtons(details.tabGroup, ["details", "permissions"]);
+
let desc = details.querySelector(".addon-detail-description");
is(desc.innerHTML, "Longer description
With brs!",
"The full description replaces newlines with
");
@@ -291,7 +323,8 @@ add_task(async function testFullDetails() {
let contrib = details.querySelector(".addon-detail-contribute");
ok(contrib, "The contribution section is visible");
- let rows = Array.from(details.querySelectorAll(".addon-detail-row"));
+ let rows = Array.from(
+ card.querySelectorAll('[name="details"] .addon-detail-row'));
// Auto updates.
let row = rows.shift();
@@ -396,13 +429,17 @@ add_task(async function testMinimalExtension() {
card = getAddonCard(doc, "addon2@mochi.test");
let details = card.querySelector("addon-details");
+ // Check all the deck buttons are hidden.
+ assertDeckHeadingButtons(details.tabGroup, ["details", "permissions"]);
+
let desc = details.querySelector(".addon-detail-description");
is(desc.textContent, "", "There is no full description");
let contrib = details.querySelector(".addon-detail-contribute");
ok(!contrib, "There is no contribution element");
- let rows = Array.from(details.querySelectorAll(".addon-detail-row"));
+ let rows = Array.from(
+ card.querySelectorAll('[name="details"] .addon-detail-row'));
// Automatic updates.
let row = rows.shift();
@@ -454,7 +491,11 @@ add_task(async function testDefaultTheme() {
ok(preview, "There is a preview");
is(preview.hidden, true, "The preview is hidden");
- let rows = Array.from(card.querySelectorAll(".addon-detail-row"));
+ // Check all the deck buttons are hidden.
+ assertDeckHeadingHidden(card.details.tabGroup);
+
+ let rows = Array.from(
+ card.querySelectorAll('[name="details"] .addon-detail-row'));
// Author.
let author = rows.shift();
@@ -508,7 +549,11 @@ add_task(async function testStaticTheme() {
is(preview.height, "90", "The height is set");
is(preview.hidden, false, "The preview is visible");
- let rows = Array.from(card.querySelectorAll(".addon-detail-row"));
+ // Check all the deck buttons are hidden.
+ assertDeckHeadingHidden(card.details.tabGroup);
+
+ let rows = Array.from(
+ card.querySelectorAll('[name="details"] .addon-detail-row'));
// Automatic updates.
let row = rows.shift();
@@ -621,3 +666,61 @@ add_task(async function testPrivateBrowsingAllowedListView() {
await extension.unload();
await closeView(win);
});
+
+add_task(async function testPermissions() {
+ async function runTest(id, permissions) {
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = getAddonCard(doc, id);
+ ok(!card.hasAttribute("expanded"), "The list card is not expanded");
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = getAddonCard(doc, id);
+ let {deck, tabGroup} = card.details;
+
+ // Check all the deck buttons are hidden.
+ assertDeckHeadingButtons(tabGroup, ["details", "permissions"]);
+
+ let permsBtn = tabGroup.querySelector('[name="permissions"]');
+ let permsShown = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ permsBtn.click();
+ await permsShown;
+
+ let permsSection = card.querySelector("addon-permissions-list");
+ let rows = Array.from(permsSection.querySelectorAll(".addon-detail-row"));
+
+ info("Check displayed permissions");
+ if (permissions) {
+ for (let name in permissions) {
+ // Check the permission-info class to make sure it's for a permission.
+ let row = rows.shift();
+ ok(row.classList.contains("permission-info"),
+ `There's a row for ${name}`);
+ }
+ } else {
+ let row = rows.shift();
+ is(doc.l10n.getAttributes(row).id, "addon-permissions-empty",
+ "There's a message when no permissions are shown");
+ }
+
+ info("Check learn more link");
+ let row = rows.shift();
+ is(row.children.length, 1, "There's one child for learn more");
+ let link = row.firstElementChild;
+ let rootUrl = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ let url = rootUrl + "extension-permissions";
+ is(link.href, url, "The URL is set");
+ is(link.getAttribute("target"), "_blank", "The link opens in a new tab");
+
+ await closeView(win);
+ }
+
+ info("Check permissions for add-on with permission message");
+ await runTest("addon1@mochi.test", ["", "tabs", "webNavigation"]);
+
+ info("Check permissions for add-on without permission messages");
+ await runTest("addon2@mochi.test");
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
index 1a51c2277578..b25ee2b514e9 100644
--- a/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
@@ -218,8 +218,7 @@ function assertUpdateState({
`The update check button is ${shown ? "hidden" : "shown"}`);
let {tabGroup} = card.details;
- is(tabGroup.hidden, !releaseNotes,
- `The tab group is ${releaseNotes ? "shown" : "hidden"}`);
+ is(tabGroup.hidden, false, "The tab group is shown");
let notesBtn = tabGroup.querySelector('[name="release-notes"]');
is(notesBtn.hidden, !releaseNotes,
`The release notes button is ${releaseNotes ? "shown" : "hidden"}`);