Bug 1531964 - Permissions tab on add-on details page r=rpl,flod
authorMark Striemer <mstriemer@mozilla.com>
Thu, 16 May 2019 14:14:29 +0000
changeset 532945 2925cfa3e8c3577f427c28e1c36ee6fb69393b94
parent 532944 b7d165b8966e6a9ca0576fa6ee7c49a52d604d4c
child 532946 72820b3eb2f053dde3339ec9679139b9dd78df4a
push id11276
push userrgurzau@mozilla.com
push dateMon, 20 May 2019 13:11:24 +0000
treeherdermozilla-beta@847755a7c325 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrpl, flod
bugs1531964
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1531964 - Permissions tab on add-on details page r=rpl,flod Differential Revision: https://phabricator.services.mozilla.com/D30429
toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
toolkit/mozapps/extensions/content/aboutaddons.css
toolkit/mozapps/extensions/content/aboutaddons.html
toolkit/mozapps/extensions/content/aboutaddons.js
toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
toolkit/mozapps/extensions/test/browser/browser_html_updates.js
--- a/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
+++ b/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
@@ -386,16 +386,17 @@ enable-addon-button = Enable
 expand-addon-button = More Options
 preferences-addon-button =
     { PLATFORM() ->
         [windows] Options
        *[other] Preferences
     }
 details-addon-button = Details
 release-notes-addon-button = Release Notes
+permissions-addon-button = Permissions
 
 addons-enabled-heading = Enabled
 addons-disabled-heading = Disabled
 
 ask-to-activate-button = Ask to Activate
 always-activate-button = Always Activate
 never-activate-button = Never Activate
 
@@ -447,8 +448,10 @@ addon-detail-private-browsing-help = Whe
 addon-detail-private-browsing-allow = Allow
 addon-detail-private-browsing-disallow = Don’t Allow
 
 available-updates-heading = Available Updates
 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
--- a/toolkit/mozapps/extensions/content/aboutaddons.css
+++ b/toolkit/mozapps/extensions/content/aboutaddons.css
@@ -357,16 +357,20 @@ panel-item-separator[hidden] {
   text-decoration: underline;
 }
 
 .button-link:active {
   color: var(--in-content-link-color-active) !important;
   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);
   margin-top: 8px;
   /* Pull the buttons flush with the side of the card */
   margin-inline: calc(var(--card-padding) * -1);
   font-size: 0;
   line-height: 0;
--- a/toolkit/mozapps/extensions/content/aboutaddons.html
+++ b/toolkit/mozapps/extensions/content/aboutaddons.html
@@ -82,16 +82,17 @@
         <five-star-rating></five-star-rating>
         <span class="disco-user-count"></span>
       </div>
     </template>
 
     <template name="addon-details">
       <div class="deck-tab-group">
         <named-deck-button deck="details-deck" name="details" data-l10n-id="details-addon-button"></named-deck-button>
+        <named-deck-button deck="details-deck" name="permissions" data-l10n-id="permissions-addon-button"></named-deck-button>
         <named-deck-button deck="details-deck" name="release-notes" data-l10n-id="release-notes-addon-button"></named-deck-button>
       </div>
       <named-deck id="details-deck">
         <section name="details">
           <div class="addon-detail-description"></div>
           <div class="addon-detail-contribute">
             <label data-l10n-id="detail-contributions-description"></label>
             <button
@@ -151,16 +152,17 @@
           <div class="addon-detail-row addon-detail-row-rating">
             <label data-l10n-id="addon-detail-rating-label"></label>
             <div class="addon-detail-rating">
               <five-star-rating></five-star-rating>
               <a target="_blank"></a>
             </div>
           </div>
         </section>
+        <addon-permissions-list name="permissions"></addon-permissions-list>
         <update-release-notes name="release-notes"></update-release-notes>
       </named-deck>
     </template>
 
     <template name="five-star-rating">
       <link rel="stylesheet" href="chrome://mozapps/content/extensions/rating-star.css">
       <span class="rating-star"></span>
       <span class="rating-star"></span>
--- a/toolkit/mozapps/extensions/content/aboutaddons.js
+++ b/toolkit/mozapps/extensions/content/aboutaddons.js
@@ -13,16 +13,25 @@ XPCOMUtils.defineLazyModuleGetters(this,
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
   AMTelemetry: "resource://gre/modules/AddonManager.jsm",
   ClientID: "resource://gre/modules/ClientID.jsm",
   ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
   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);
 XPCOMUtils.defineLazyPreferenceGetter(
   this, "SUPPORT_URL", "app.support.baseURL",
   "", null, val => Services.urlFormatter.formatURL(val));
 
 const UPDATES_RECENT_TIMESPAN = 2 * 24 * 3600000; // 2 days (in milliseconds)
@@ -748,16 +757,59 @@ class UpdateReleaseNotes extends HTMLEle
   }
 
   setErrorMessage() {
     this.setMessage("release-notes-error");
   }
 }
 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() {
     if (this.children.length == 0) {
       this.render();
     }
     this.deck.addEventListener("view-changed", this);
   }
@@ -788,16 +840,18 @@ class AddonDetails extends HTMLElement {
   }
 
   update() {
     let {addon} = this;
 
     // 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;
 
     // Hide the tab group if "details" is the only visible button.
     this.tabGroup.hidden = Array.from(this.tabGroup.children).every(button => {
       return button.name == "details" || button.hidden;
     });
 
@@ -823,16 +877,20 @@ class AddonDetails extends HTMLElement {
     }
 
     this.textContent = "";
     this.appendChild(importTemplate("addon-details"));
 
     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) {
       description.appendChild(addon.getFullDescription(document));
     } else if (addon.fullDescription) {
       description.appendChild(nl2br(addon.fullDescription));
     }
 
--- a/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
@@ -56,16 +56,36 @@ function checkOptions(doc, options, expe
     is(input.value, expected.value, "The value is right");
     is(input.checked, expected.checked, "The checked property is correct");
     Assert.deepEqual(
       doc.l10n.getAttributes(text), {id: expected.label},
       "The label has the right text");
   }
 }
 
+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");
 }
 
 add_task(async function enableHtmlViews() {
   await SpecialPowers.pushPrefEnv({
     set: [["extensions.htmlaboutaddons.enabled", true],
@@ -78,26 +98,34 @@ add_task(async function enableHtmlViews(
     name: "Test add-on 1",
     creator: {name: "The creator", url: "http://example.com/me"},
     version: "3.1",
     description: "Short description",
     fullDescription: "Longer description\nWith brs!",
     type: "extension",
     contributionURL: "http://foo.com",
     averageRating: 4.279,
+    userPermissions: {
+      origins: ["<all_urls>", "file://*/*"],
+      permissions: ["alarms", "contextMenus", "tabs", "webNavigation"],
+    },
     reviewCount: 5,
     reviewURL: "http://example.com/reviews",
     homepageURL: "http://example.com/addon1",
     updateDate: new Date("2019-03-07T01:00:00"),
     applyBackgroundUpdates: AddonManager.AUTOUPDATE_ENABLE,
   }, {
     id: "addon2@mochi.test",
     name: "Test add-on 2",
     creator: {name: "I made it"},
     description: "Short description",
+    userPermissions: {
+      origins: [],
+      permissions: ["alarms", "contextMenus"],
+    },
     type: "extension",
   }, {
     id: "theme1@mochi.test",
     name: "Test theme",
     creator: {name: "Artist", url: "http://example.com/artist"},
     description: "A nice tree",
     type: "theme",
     screenshots: [{
@@ -279,24 +307,29 @@ add_task(async function testFullDetails(
   ok(card.hasAttribute("expanded"), "The detail card is expanded");
 
   // Make sure the preview is hidden.
   preview = card.querySelector(".card-heading-image");
   ok(preview, "There is a preview");
   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<br>With brs!",
      "The full description replaces newlines with <br>");
 
   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();
   checkLabel(row, "updates");
   let expectedOptions = [
     {value: "1", label: "addon-detail-updates-radio-default", checked: false},
     {value: "2", label: "addon-detail-updates-radio-on", checked: true},
     {value: "0", label: "addon-detail-updates-radio-off", checked: false},
@@ -391,23 +424,27 @@ add_task(async function testMinimalExten
   ok(!card.hasAttribute("expanded"), "The list card is not expanded");
   let loaded = waitForViewLoad(win);
   card.querySelector('[action="expand"]').click();
   await loaded;
 
   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();
   checkLabel(row, "updates");
 
   // Private browsing settings.
   row = rows.shift();
   checkLabel(row, "private-browsing");
@@ -449,17 +486,21 @@ add_task(async function testDefaultTheme
 
   card = getAddonCard(doc, "default-theme@mozilla.org");
 
   // Make sure the preview is hidden.
   preview = card.querySelector(".card-heading-image");
   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();
   checkLabel(author, "author");
   let text = author.lastChild;
   is(text.textContent, "Mozilla", "The author is set");
 
   // Version.
@@ -503,17 +544,21 @@ add_task(async function testStaticTheme(
   // Make sure the preview is still set.
   preview = card.querySelector(".card-heading-image");
   ok(preview, "There is a preview");
   is(preview.src, "http://example.com/preview.png", "The preview URL is set");
   is(preview.width, "664", "The width is set");
   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();
   checkLabel(row, "updates");
 
   // Author.
   let author = rows.shift();
   checkLabel(author, "author");
@@ -616,8 +661,66 @@ add_task(async function testPrivateBrows
   // The allowed extension should have a badge on load.
   let card = getAddonCard(doc, "allowed@mochi.test");
   let badge = card.querySelector(".addon-badge-private-browsing-allowed");
   ok(!badge.hidden, "The PB badge is shown for the allowed add-on");
 
   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", ["<all_urls>", "tabs", "webNavigation"]);
+
+  info("Check permissions for add-on without permission messages");
+  await runTest("addon2@mochi.test");
+});
--- a/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
@@ -213,18 +213,17 @@ function assertUpdateState({
   ok(installButton.hidden != shown,
      `The install button is ${shown ? "hidden" : "shown"}`);
   if (expanded) {
     let updateCheckButton = card.querySelector('button[action="update-check"]');
     ok(updateCheckButton.hidden == shown,
       `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"}`);
   }
 }
 
 add_task(async function testUpdateAvailable() {
   let id = "update@mochi.test";