Bug 932854 - When a site uses a hidden plugin, show a notification bar to aid discoverability. This patch, suitable for trunk and branch, uses existing strings which are not ideal. r=jaws, a=lsblakk
authorBenjamin Smedberg <benjamin@smedbergs.us>
Mon, 11 Nov 2013 16:11:12 -0500
changeset 166551 b1a341fb814fd160ab7b82aba8871a797ef519fa
parent 166550 0896f9e5c3f76eb0d66b5815f346bef04f2f7cbc
child 166552 598daa4e76e13e1a5393d14b57c293cc458a4838
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws, lsblakk
bugs932854
milestone27.0a2
Bug 932854 - When a site uses a hidden plugin, show a notification bar to aid discoverability. This patch, suitable for trunk and branch, uses existing strings which are not ideal. r=jaws, a=lsblakk
browser/base/content/browser-plugins.js
browser/base/content/test/general/browser_pluginnotification.js
browser/themes/shared/plugin-doorhanger.inc.css
toolkit/themes/shared/plugins/pluginProblem.css
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -872,34 +872,36 @@ var gPluginHandler = {
     if (!aBrowser.docShell || !aBrowser.contentWindow) {
       return;
     }
 
     let notification = PopupNotifications.getNotification("click-to-play-plugins", aBrowser);
     if (!notification)
       return;
 
-    let iconClasses = document.getElementById("plugins-notification-icon").classList;
-
-    // Make a copy so we can remove visible plugins
-    let actions = new Map(notification.options.centerActions);
+    // Make a copy of the actions, removing active plugins and checking for
+    // outdated plugins.
+    let haveInsecure = false;
+    let actions = new Map();
+    for (let action of notification.options.centerActions.values()) {
+      switch (action.fallbackType) {
+        // haveInsecure will trigger the red flashing icon and the infobar
+        // styling below
+        case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
+        case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
+          haveInsecure = true;
+          // fall through
 
-    for (let pluginInfo of actions.values()) {
-      let fallbackType = pluginInfo.fallbackType;
-      if (fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE ||
-          fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE ||
-          fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED) {
-        iconClasses.add("plugin-blocked");
-        iconClasses.remove("plugin-hidden");
-        return;
+        case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
+          actions.set(action.permissionString, action);
+          continue;
       }
     }
 
-    iconClasses.remove("plugin-blocked");
-
+    // check for hidden plugins
     let contentWindow = aBrowser.contentWindow;
     let contentDoc = aBrowser.contentDocument;
     let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils);
     for (let plugin of cwu.plugins) {
       let info = this._getPluginInfo(plugin);
       if (!actions.has(info.permissionString)) {
         continue;
@@ -907,32 +909,143 @@ var gPluginHandler = {
       let fallbackType = info.fallbackType;
       if (fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
         actions.delete(info.permissionString);
         if (actions.size == 0) {
           break;
         }
         continue;
       }
-      if (fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY) {
+      if (fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY &&
+          fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE &&
+          fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE) {
         continue;
       }
       let overlay = this.getPluginUI(plugin, "main");
       if (!overlay) {
         continue;
       }
       if (!this.isTooSmall(plugin, overlay)) {
         actions.delete(info.permissionString);
         if (actions.size == 0) {
           break;
         }
       }
     }
 
-    iconClasses.toggle("plugin-hidden", actions.size != 0);
+    // Set up the icon
+    document.getElementById("plugins-notification-icon").classList.
+      toggle("plugin-blocked", haveInsecure);
+
+    // Now configure the notification bar
+
+    let notificationBox = gBrowser.getNotificationBox(aBrowser);
+
+    function hideNotification() {
+      let n = notificationBox.getNotificationWithValue("plugin-hidden");
+      if (n) {
+        notificationBox.removeNotification(n, true);
+      }
+    }
+
+    // There are three different cases when showing an infobar:
+    // 1.  A single type of plugin is hidden on the page. Show the UI for that
+    //     plugin.
+    // 2a. Multiple types of plugins are hidden on the page. Show the multi-UI
+    //     with the vulnerable styling.
+    // 2b. Multiple types of plugins are hidden on the page, but none are
+    //     vulnerable. Show the nonvulnerable multi-UI.
+    function showNotification() {
+      let n = notificationBox.getNotificationWithValue("plugin-hidden");
+      if (n) {
+        // If something is already shown, just keep it
+        return;
+      }
+
+      let message;
+      // Icons set directly cannot be manipulated using moz-image-region, so
+      // we use CSS classes instead.
+      let host = gPluginHandler._getHostFromPrincipal(aBrowser.contentDocument.nodePrincipal);
+      let brand = document.getElementById("bundle_brand").getString("brandShortName");
+
+      if (actions.size == 1) {
+        let pluginInfo = [...actions.values()][0];
+        let pluginName = pluginInfo.pluginName;
+
+        switch (pluginInfo.fallbackType) {
+          case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
+            message = gNavigatorBundle.getFormattedString(
+              "pluginActivateNew.message",
+              [pluginName, host]);
+            break;
+          case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
+            message = gNavigatorBundle.getFormattedString(
+              "pluginActivateOutdated.message",
+              [pluginName, host, brand]);
+            break;
+          case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
+            message = gNavigatorBundle.getFormattedString(
+              "pluginActivateVulnerable.message",
+              [pluginName, host, brand]);
+        }
+      } else {
+        // Multi-plugin
+        message = gNavigatorBundle.getFormattedString(
+          "pluginActivateMultiple.message", [host]);
+
+        for (let action of actions.values()) {
+          if (action.fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY) {
+            break;
+          }
+        }
+      }
+
+      // These strings are temporary no-string-change for branch uplift
+      let buttons = [
+        {
+          label: gNavigatorBundle.getString("pluginBlockNow.label"),
+          accessKey: gNavigatorBundle.getString("pluginBlockNow.accesskey"),
+          callback: function() {
+            Services.perms.addFromPrincipal(aBrowser.contentDocument.nodePrincipal,
+                                            "plugin-hidden-notification",
+                                            Services.perms.DENY_ACTION);
+          }
+        },
+        {
+          label: gNavigatorBundle.getString("offlineApps.allow"),
+          accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"),
+          callback: function() {
+            let curNotification =
+              PopupNotifications.getNotification("click-to-play-plugins",
+                                                 aBrowser);
+            if (curNotification) {
+              curNotification.reshow();
+            }
+          }
+        }
+      ];
+      n = notificationBox.
+        appendNotification(message, "plugin-hidden", null,
+                           notificationBox.PRIORITY_INFO_HIGH, buttons);
+      if (haveInsecure) {
+        n.classList.add('pluginVulnerable');
+      }
+    }
+
+    if (actions.size == 0) {
+      hideNotification();
+    } else {
+      let notificationPermission = Services.perms.testPermissionFromPrincipal(
+        aBrowser.contentDocument.nodePrincipal, "plugin-hidden-notification");
+      if (notificationPermission == Ci.nsIPermissionManager.DENY_ACTION) {
+        hideNotification();
+      } else {
+        showNotification();
+      }
+    }
   },
 
   // Crashed-plugin observer. Notified once per plugin crash, before events
   // are dispatched to individual plugin instances.
   pluginCrashed : function(subject, topic, data) {
     let propertyBag = subject;
     if (!(propertyBag instanceof Ci.nsIPropertyBag2) ||
         !(propertyBag instanceof Ci.nsIWritablePropertyBag2))
--- a/browser/base/content/test/general/browser_pluginnotification.js
+++ b/browser/base/content/test/general/browser_pluginnotification.js
@@ -846,30 +846,30 @@ function test25() {
 
   prepareTest(() => executeSoon(test26), gTestRoot + "plugin_small.html");
 }
 
 function test26() {
   let notification = PopupNotifications.getNotification("click-to-play-plugins");
   ok(notification, "Test 26: There should be a plugin notification");
 
-  waitForCondition(() => gBrowser.ownerDocument.
-      getElementById("plugins-notification-icon").classList.
-      contains("plugin-hidden"),
+  let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+
+  waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") !== null,
     () => {
       // Don't use setTestPluginEnabledState here because we already saved the
       // prior value
       getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED;
       prepareTest(test27, gTestRoot + "plugin_small.html");
     },
     "Test 26, expected the plugin notification icon to be highlighted");
 }
 
 function test27() {
   let notification = PopupNotifications.getNotification("click-to-play-plugins");
   ok(notification, "Test 27: There should be a plugin notification");
 
-  waitForCondition(() => !gBrowser.ownerDocument.
-      getElementById("plugins-notification-icon").classList.
-      contains("plugin-hidden"),
+  let notificationBox = gBrowser.getNotificationBox(gTestBrowser);
+
+  waitForCondition(() => notificationBox.getNotificationWithValue("plugin-hidden") === null,
     finishTest,
     "Test 27, expected the plugin notification icon to not be highlighted");
 }
--- a/browser/themes/shared/plugin-doorhanger.inc.css
+++ b/browser/themes/shared/plugin-doorhanger.inc.css
@@ -42,8 +42,24 @@
 .click-to-play-plugins-outer-description {
   margin-top: 8px;
 }
 
 .click-to-play-plugins-notification-link,
 .center-item-link {
   margin: 0;
 }
+
+.messageImage[value="plugin-hidden"] {
+  list-style-image: url("chrome://browser/skin/notification-pluginNormal.png");
+  -moz-image-region: rect(0, 16px, 16px, 0);
+}
+
+/* Keep any changes to this style in sync with pluginProblem.css */
+notification.pluginVulnerable {
+  background-color: rgb(72,72,72);
+  background-image: url(chrome://mozapps/skin/plugins/contentPluginStripe.png);
+  color: white;
+}
+
+notification.pluginVulnerable .messageImage {
+  list-style-image: url("chrome://browser/skin/notification-pluginBlocked.png");
+}
--- a/toolkit/themes/shared/plugins/pluginProblem.css
+++ b/toolkit/themes/shared/plugins/pluginProblem.css
@@ -1,16 +1,16 @@
 /* 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/. */
 
 @namespace html url(http://www.w3.org/1999/xhtml);
 
 /* These styles affect only the bound element, not other page content. */
-
+/* Keep any changes to these styles in sync with plugin-doorhanger.inc.css */
 .mainBox {
   font: message-box;
   font-size: 12px;
   text-align: center;
   display: table;
   width: 100%;
   height: 100%;
   background-color: rgb(72,72,72);