Bug 1525174 - Plugin active states for HTML about:addons r=kmag,flod
authorMark Striemer <mstriemer@mozilla.com>
Sat, 27 Apr 2019 00:30:48 +0000
changeset 530431 571327e1a5e49aeffb9f054380e767b831165f8d
parent 530430 2fd7f8b748612b07075365154685cadf6f0cb29d
child 530432 fbc57fee258e6b0e055301f576ce39d1e9fb4200
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag, flod
bugs1525174
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 1525174 - Plugin active states for HTML about:addons r=kmag,flod Differential Revision: https://phabricator.services.mozilla.com/D27967
toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
toolkit/mozapps/extensions/content/aboutaddons.html
toolkit/mozapps/extensions/content/aboutaddons.js
toolkit/mozapps/extensions/content/panel-item.css
toolkit/mozapps/extensions/test/browser/browser.ini
toolkit/mozapps/extensions/test/browser/browser_html_plugins.js
--- a/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
+++ b/toolkit/locales/en-US/toolkit/about/aboutAddons.ftl
@@ -347,16 +347,20 @@ go-back-button =
 remove-addon-button = Remove
 disable-addon-button = Disable
 enable-addon-button = Enable
 expand-addon-button = More Options
 
 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
+
 addon-detail-author-label = Author
 addon-detail-version-label = Version
 addon-detail-last-updated-label = Last Updated
 addon-detail-homepage-label = Homepage
 addon-detail-rating-label = Rating
 
 # This string is used to show that an add-on is disabled.
 # Variables:
--- a/toolkit/mozapps/extensions/content/aboutaddons.html
+++ b/toolkit/mozapps/extensions/content/aboutaddons.html
@@ -9,39 +9,52 @@
 
     <script src="chrome://global/content/contentAreaUtils.js"></script>
     <script src="chrome://mozapps/content/extensions/aboutaddons.js"></script>
   </head>
   <body>
     <div id="main">
     </div>
 
+    <template name="addon-options">
+      <panel-list>
+        <panel-item action="toggle-disabled"></panel-item>
+        <panel-item data-l10n-id="remove-addon-button" action="remove"></panel-item>
+        <panel-item data-l10n-id="install-update-button" action="install-update" badged></panel-item>
+        <panel-item-separator></panel-item-separator>
+        <panel-item data-l10n-id="expand-addon-button" action="expand"></panel-item>
+      </panel-list>
+    </template>
+
+    <template name="plugin-options">
+      <panel-list>
+        <panel-item data-l10n-id="ask-to-activate-button" action="ask-to-activate"></panel-item>
+        <panel-item data-l10n-id="always-activate-button" action="always-activate"></panel-item>
+        <panel-item data-l10n-id="never-activate-button" action="never-activate"></panel-item>
+        <panel-item-separator></panel-item-separator>
+        <panel-item data-l10n-id="expand-addon-button" action="expand"></panel-item>
+      </panel-list>
+    </template>
+
     <template name="card">
       <div class="card addon">
         <img class="card-heading-image">
         <div class="addon-card-collapsed">
           <img class="card-heading-icon addon-icon">
           <div class="card-contents">
             <div class="addon-name-container">
               <span class="addon-name"></span>
               <div class="addon-badge addon-badge-private-browsing-allowed"
                     data-l10n-id="addon-badge-private-browsing-allowed"
                     hidden></div>
             </div>
             <span class="addon-description"></span>
           </div>
           <div class="more-options-menu">
             <button class="more-options-button ghost-button" action="more-options"></button>
-            <panel-list>
-              <panel-item action="toggle-disabled"></panel-item>
-              <panel-item data-l10n-id="remove-addon-button" action="remove"></panel-item>
-              <panel-item data-l10n-id="install-update-button" action="install-update" badged></panel-item>
-              <panel-item-separator></panel-item-separator>
-              <panel-item data-l10n-id="expand-addon-button" action="expand"></panel-item>
-            </panel-list>
           </div>
         </div>
       </div>
     </template>
 
     <template name="addon-details">
       <div class="addon-detail-description"></div>
       <div class="addon-detail-contribute">
--- a/toolkit/mozapps/extensions/content/aboutaddons.js
+++ b/toolkit/mozapps/extensions/content/aboutaddons.js
@@ -17,18 +17,21 @@ XPCOMUtils.defineLazyPreferenceGetter(
   this, "allowPrivateBrowsingByDefault",
   "extensions.allowPrivateBrowsingByDefault", true);
 XPCOMUtils.defineLazyPreferenceGetter(
   this, "SUPPORT_URL", "app.support.baseURL",
   "", null, val => Services.urlFormatter.formatURL(val));
 
 const PLUGIN_ICON_URL = "chrome://global/skin/plugins/pluginGeneric.svg";
 const PERMISSION_MASKS = {
+  "ask-to-activate": AddonManager.PERM_CAN_ASK_TO_ACTIVATE,
   enable: AddonManager.PERM_CAN_ENABLE,
+  "always-activate": AddonManager.PERM_CAN_ENABLE,
   disable: AddonManager.PERM_CAN_DISABLE,
+  "never-activate": AddonManager.PERM_CAN_DISABLE,
   uninstall: AddonManager.PERM_CAN_UNINSTALL,
   upgrade: AddonManager.PERM_CAN_UPGRADE,
 };
 
 const PRIVATE_BROWSING_PERM_NAME = "internal:privateBrowsingAllowed";
 const PRIVATE_BROWSING_PERMS =
   {permissions: [PRIVATE_BROWSING_PERM_NAME], origins: []};
 
@@ -315,20 +318,114 @@ class PanelList extends HTMLElement {
 }
 customElements.define("panel-list", PanelList);
 
 class PanelItem extends HTMLElement {
   constructor() {
     super();
     this.attachShadow({mode: "open"});
     this.shadowRoot.appendChild(importTemplate("panel-item"));
+    this.button = this.shadowRoot.querySelector("button");
+  }
+
+  get disabled() {
+    return this.button.hasAttribute("disabled");
+  }
+
+  set disabled(val) {
+    if (val) {
+      this.button.setAttribute("disabled", "");
+    } else {
+      this.button.removeAttribute("disabled");
+    }
+  }
+
+  get checked() {
+    return this.hasAttribute("checked");
+  }
+
+  set checked(val) {
+    if (val) {
+      this.setAttribute("checked", "");
+    } else {
+      this.removeAttribute("checked");
+    }
   }
 }
 customElements.define("panel-item", PanelItem);
 
+class AddonOptions extends HTMLElement {
+  connectedCallback() {
+    if (this.children.length == 0) {
+      this.render();
+    }
+  }
+
+  render() {
+    this.appendChild(importTemplate("addon-options"));
+  }
+
+  update(card, addon, updateInstall) {
+    // Hide remove button if not allowed.
+    let removeButton = this.querySelector('[action="remove"]');
+    removeButton.hidden = !hasPermission(addon, "uninstall");
+
+    // Set disable label and hide if not allowed.
+    let toggleDisabledButton = this.querySelector('[action="toggle-disabled"]');
+    let toggleDisabledAction = addon.userDisabled ? "enable" : "disable";
+    document.l10n.setAttributes(
+      toggleDisabledButton, `${toggleDisabledAction}-addon-button`);
+    toggleDisabledButton.hidden = !hasPermission(addon, toggleDisabledAction);
+
+    // Set the update button and badge the menu if there's an update.
+    this.querySelector('[action="install-update"]').hidden = !updateInstall;
+
+    // The separator isn't needed when expanded (nothing under it) or when the
+    // remove and disable buttons are hidden (nothing above it).
+    let separator = this.querySelector("panel-item-separator");
+    separator.hidden = card.expanded ||
+      removeButton.hidden && toggleDisabledButton.hidden;
+
+    // Hide the expand button if we're expanded.
+    this.querySelector('[action="expand"]').hidden = card.expanded;
+  }
+}
+customElements.define("addon-options", AddonOptions);
+
+class PluginOptions extends HTMLElement {
+  connectedCallback() {
+    if (this.children.length == 0) {
+      this.render();
+    }
+  }
+
+  render() {
+    this.appendChild(importTemplate("plugin-options"));
+  }
+
+  update(card, addon) {
+    let actions = [{
+      action: "ask-to-activate",
+      userDisabled: AddonManager.STATE_ASK_TO_ACTIVATE,
+    }, {
+      action: "always-activate",
+      userDisabled: false,
+    }, {
+      action: "never-activate",
+      userDisabled: true,
+    }];
+    for (let {action, userDisabled} of actions) {
+      let el = this.querySelector(`[action="${action}"]`);
+      el.checked = addon.userDisabled === userDisabled;
+      el.disabled = !(el.checked || hasPermission(addon, action));
+    }
+  }
+}
+customElements.define("plugin-options", PluginOptions);
+
 class AddonDetails extends HTMLElement {
   connectedCallback() {
     if (this.children.length == 0) {
       this.render();
     }
   }
 
   setAddon(addon) {
@@ -584,16 +681,27 @@ class AddonCard extends HTMLElement {
           } else {
             await addon.disable();
           }
           if (e.mozInputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) {
             // Refocus the open menu button so it's clear where the focus is.
             this.querySelector('[action="more-options"]').focus();
           }
           break;
+        case "ask-to-activate":
+          if (hasPermission(addon, "ask-to-activate")) {
+            addon.userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE;
+          }
+          break;
+        case "always-activate":
+          addon.userDisabled = false;
+          break;
+        case "never-activate":
+          addon.userDisabled = true;
+          break;
         case "update-check":
           let listener = {
             onUpdateAvailable(addon, install) {
               attachUpdateHandler(install);
             },
             onNoUpdateAvailable: () => {
               this.sendEvent("no-update");
             },
@@ -720,16 +828,18 @@ class AddonCard extends HTMLElement {
 
   onUpdateModeChanged() {
     this.update();
   }
 
   onPropertyChanged(addon, changed) {
     if (this.details && changed.includes("applyBackgroundUpdates")) {
       this.details.update();
+    } else if (addon.type == "plugin" && changed.includes("userDisabled")) {
+      this.update();
     }
   }
 
   /**
    * Update the card's contents based on the previously set add-on. This should
    * be called if there has been a change to the add-on.
    */
   update() {
@@ -762,55 +872,36 @@ class AddonCard extends HTMLElement {
       name.removeAttribute("data-l10n-id");
     } else {
       document.l10n.setAttributes(name, "addon-name-disabled", {
         name: addon.name,
       });
     }
     name.title = `${addon.name} ${addon.version}`;
 
+    // Set the items in the more options menu.
+    this.options.update(this, addon, this.updateInstall);
+
+    // Badge the more options menu if there's an update.
+    card.querySelector(".more-options-button")
+      .classList.toggle("more-options-button-badged", !!this.updateInstall);
+
     // Set the private browsing badge visibility.
     if (!allowPrivateBrowsingByDefault && addon.type == "extension" &&
         addon.incognito != "not_allowed") {
       // Keep update synchronous, the badge can appear later.
       isAllowedInPrivateBrowsing(addon).then(isAllowed => {
         card.querySelector(".addon-badge-private-browsing-allowed")
           .hidden = !isAllowed;
       });
     }
 
     // Update description.
     card.querySelector(".addon-description").textContent = addon.description;
 
-    // Hide remove button if not allowed.
-    let removeButton = card.querySelector('[action="remove"]');
-    removeButton.hidden = !hasPermission(addon, "uninstall");
-
-    // Set disable label and hide if not allowed.
-    let disableButton = card.querySelector('[action="toggle-disabled"]');
-    let disableAction = addon.userDisabled ? "enable" : "disable";
-    document.l10n.setAttributes(
-      disableButton, `${disableAction}-addon-button`);
-    disableButton.hidden = !hasPermission(addon, disableAction);
-
-    // Set the update button and badge the menu if there's an update.
-    card.querySelector('[action="install-update"]').hidden =
-      !this.updateInstall;
-    card.querySelector(".more-options-button")
-      .classList.toggle("more-options-button-badged", !!this.updateInstall);
-
-    // The separator isn't needed when expanded (nothing under it) or when the
-    // remove and disable buttons are hidden (nothing above it).
-    let separator = card.querySelector("panel-item-separator");
-    separator.hidden = this.expanded ||
-      removeButton.hidden && disableButton.hidden;
-
-    // Hide the expand button if we're expanded.
-    card.querySelector('[action="expand"]').hidden = this.expanded;
-
     // Update the details if they're shown.
     if (this.details) {
       this.details.update();
     }
 
     this.sendEvent("update");
   }
 
@@ -828,16 +919,21 @@ class AddonCard extends HTMLElement {
     let {addon} = this;
     if (!addon) {
       throw new Error("addon-card must be initialized with setAddon()");
     }
 
     this.card = importTemplate("card").firstElementChild;
     this.setAttribute("addon-id", addon.id);
 
+    let panelType = addon.type == "plugin" ? "plugin-options" : "addon-options";
+    this.options = document.createElement(panelType);
+    this.options.render();
+    this.card.querySelector(".more-options-menu").appendChild(this.options);
+
     // Set the contents.
     this.update();
 
     let doneRenderPromise = Promise.resolve();
     if (this.expanded) {
       if (!this.details) {
         this.details = document.createElement("addon-details");
       }
--- a/toolkit/mozapps/extensions/content/panel-item.css
+++ b/toolkit/mozapps/extensions/content/panel-item.css
@@ -22,16 +22,20 @@ button {
   height: 5px;
   border-radius: 50%;
   background: var(--blue-50);
   position: absolute;
   top: 4px;
   left: 28px;
 }
 
+:host([checked]) {
+  --icon: url("chrome://global/skin/icons/check.svg");
+}
+
 button:focus,
-button:hover {
+button:not([disabled]):hover {
   background-color: var(--in-content-button-background);
 }
 
 button:hover:active {
   background-color: var(--in-content-button-background-hover);
 }
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -72,16 +72,17 @@ skip-if = os == "linux" && !debug # Bug 
 [browser_extension_sideloading_permission.js]
 [browser_file_xpi_no_process_switch.js]
 skip-if = true # Bug 1449071 - Frequent failures
 [browser_globalwarnings.js]
 [browser_gmpProvider.js]
 skip-if = os == 'linux' && !debug # Bug 1398766
 [browser_html_detail_view.js]
 [browser_html_list_view.js]
+[browser_html_plugins.js]
 [browser_html_updates.js]
 [browser_inlinesettings_browser.js]
 skip-if = os == 'mac' || os == 'linux' # Bug 1483347
 [browser_installssl.js]
 skip-if = verify
 [browser_interaction_telemetry.js]
 [browser_langpack_signing.js]
 [browser_legacy.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_plugins.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PLUGIN_DESCRIPTION = "Flash plug-in for testing purposes.";
+
+add_task(async function enableHtmlViews() {
+  await SpecialPowers.pushPrefEnv({
+    set: [["extensions.htmlaboutaddons.enabled", true]],
+  });
+});
+
+function checkItems(items, checked) {
+  for (let item of items) {
+    let action = item.getAttribute("action");
+    ok(!item.disabled, `${action} is enabled`);
+    if (action == checked) {
+      ok(item.checked, `${action} is checked`);
+    } else {
+      ok(!item.checked, `${action} isn't checked`);
+    }
+  }
+}
+
+add_task(async function testAskToActivate() {
+  let plugins = await AddonManager.getAddonsByTypes(["plugin"]);
+  let flash = plugins.find(
+    plugin => plugin.description == TEST_PLUGIN_DESCRIPTION);
+  let win = await loadInitialView("plugin");
+  let doc = win.document;
+
+  let card = doc.querySelector(`addon-card[addon-id="${flash.id}"]`);
+  let panelItems = Array.from(card.querySelectorAll("panel-item"));
+  let actions = panelItems.map(item => item.getAttribute("action"));
+  Assert.deepEqual(
+    actions, ["ask-to-activate", "always-activate", "never-activate", "expand"],
+    "The panel items are for a plugin");
+
+  checkItems(panelItems, "ask-to-activate");
+
+  is(flash.userDisabled, AddonManager.STATE_ASK_TO_ACTIVATE,
+     "Flash is ask-to-activate");
+  ok(flash.isActive, "Flash is active");
+
+  // Switch the plugin to always activate.
+  let updated = BrowserTestUtils.waitForEvent(card, "update");
+  panelItems[1].click();
+  await updated;
+  checkItems(panelItems, "always-activate");
+  ok(flash.userDisabled != AddonManager.STATE_ASK_TO_ACTIVATE,
+     "Flash isn't ask-to-activate");
+  ok(flash.isActive, "Flash is still active");
+
+  // Switch to never activate.
+  updated = BrowserTestUtils.waitForEvent(card, "update");
+  panelItems[2].click();
+  await updated;
+  checkItems(panelItems, "never-activate");
+  ok(flash.userDisabled, `Flash is not userDisabled... for some reason`);
+  ok(!flash.isActive, "Flash isn't active");
+
+  // Switch it back to ask to activate.
+  updated = BrowserTestUtils.waitForEvent(card, "update");
+  panelItems[0].click();
+  await updated;
+  checkItems(panelItems, "ask-to-activate");
+  is(flash.userDisabled, AddonManager.STATE_ASK_TO_ACTIVATE,
+     "Flash is ask-to-activate");
+  ok(flash.isActive, "Flash is active");
+
+  await closeView(win);
+});