Bug 1398972 - Replace usage of plugin doorhanger XBL binding r=Felipe
authorDoug Thayer <dothayer@mozilla.com>
Tue, 31 Oct 2017 11:40:04 -0700
changeset 389385 91806d4db644
parent 389384 08b6c9db493b
child 389386 1bbe0eb92a75
push id32786
push userncsoregi@mozilla.com
push dateWed, 01 Nov 2017 10:10:44 +0000
treeherdermozilla-central@7651a53b3faf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersFelipe
bugs1398972
milestone58.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 1398972 - Replace usage of plugin doorhanger XBL binding r=Felipe Migrated to simply use PopupNotifications.jsm. Additionally, this changes the behavior to always have two buttons and a remember checkbox. When selecting allow with remember, it will behave like the always allow option previously, but when selecting block with remember, it will move that page into a quiet mode with respect to Flash - i.e., no plugin overlays will show anymore, and instead you will just see the plugin icon in the URL bar, which you can continue to interact with as before. MozReview-Commit-ID: EUFlI7nM09t
browser/base/content/browser-plugins.js
browser/base/content/browser.css
browser/locales/en-US/chrome/browser/browser.properties
browser/modules/PluginContent.jsm
dom/base/nsIObjectLoadingContent.idl
dom/base/nsObjectLoadingContent.cpp
dom/base/nsObjectLoadingContent.h
toolkit/pluginproblem/content/pluginProblemContent.css
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -137,84 +137,103 @@ var gPluginHandler = {
     }
   },
 
   /**
    * Called from the plugin doorhanger to set the new permissions for a plugin
    * and activate plugins if necessary.
    * aNewState should be either "allownow" "allowalways" or "block"
    */
-  _updatePluginPermission(aNotification, aPluginInfo, aNewState) {
+  _updatePluginPermission(aBrowser, aPluginInfo, aNewState) {
     let permission;
     let expireType;
     let expireTime;
     let histogram =
       Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_USER_ACTION");
 
+    let notification = PopupNotifications.getNotification("click-to-play-plugins", aBrowser);
+
     // Update the permission manager.
     // Also update the current state of pluginInfo.fallbackType so that
     // subsequent opening of the notification shows the current state.
     switch (aNewState) {
       case "allownow":
         permission = Ci.nsIPermissionManager.ALLOW_ACTION;
         expireType = Ci.nsIPermissionManager.EXPIRE_SESSION;
         expireTime = Date.now() + Services.prefs.getIntPref(this.PREF_SESSION_PERSIST_MINUTES) * 60 * 1000;
         histogram.add(0);
         aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
-        aNotification.options.extraAttr = "active";
+        notification.options.extraAttr = "active";
         break;
 
       case "allowalways":
         permission = Ci.nsIPermissionManager.ALLOW_ACTION;
         expireType = Ci.nsIPermissionManager.EXPIRE_TIME;
         expireTime = Date.now() +
           Services.prefs.getIntPref(this.PREF_PERSISTENT_DAYS) * 24 * 60 * 60 * 1000;
         histogram.add(1);
         aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
-        aNotification.options.extraAttr = "active";
+        notification.options.extraAttr = "active";
         break;
 
       case "block":
         permission = Ci.nsIPermissionManager.PROMPT_ACTION;
         expireType = Ci.nsIPermissionManager.EXPIRE_NEVER;
         expireTime = 0;
         histogram.add(2);
         switch (aPluginInfo.blocklistState) {
           case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE:
             aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE;
             break;
           case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
             aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE;
             break;
           default:
-            aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY;
+            // PLUGIN_CLICK_TO_PLAY_QUIET will only last until they reload the page, at
+            // which point it will be PLUGIN_CLICK_TO_PLAY (the overlays will appear)
+            aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET;
         }
-        aNotification.options.extraAttr = "inactive";
+        notification.options.extraAttr = "inactive";
         break;
 
-      // In case a plugin has already been allowed in another tab, the "continue allowing" button
-      // shouldn't change any permissions but should run the plugin-enablement code below.
+      case "blockalways":
+        permission = Ci.nsIObjectLoadingContent.PLUGIN_PERMISSION_PROMPT_ACTION_QUIET;
+        expireType = Ci.nsIPermissionManager.EXPIRE_NEVER;
+        expireTime = 0;
+        histogram.add(3);
+        aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET;
+        notification.options.extraAttr = "inactive";
+        break;
+
+      // In case a plugin has already been allowed/disallowed in another tab, the
+      // buttons matching the existing block state shouldn't change any permissions
+      // but should run the plugin-enablement code below.
       case "continue":
         aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
-        aNotification.options.extraAttr = "active";
+        notification.options.extraAttr = "active";
         break;
+
+      case "continueblocking":
+        aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET;
+        notification.options.extraAttr = "inactive";
+        break;
+
       default:
         Cu.reportError(Error("Unexpected plugin state: " + aNewState));
         return;
     }
 
-    let browser = aNotification.browser;
-    if (aNewState != "continue") {
-      let principal = aNotification.options.principal;
+    if (aNewState != "continue" && aNewState != "continueblocking") {
+      let principal = notification.options.principal;
       Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString,
                                       permission, expireType, expireTime);
       aPluginInfo.pluginPermissionType = expireType;
     }
 
-    browser.messageManager.sendAsyncMessage("BrowserPlugins:ActivatePlugins", {
+    aBrowser.messageManager.sendAsyncMessage("BrowserPlugins:ActivatePlugins", {
       pluginInfo: aPluginInfo,
       newState: aNewState,
     });
   },
 
   showClickToPlayNotification(browser, plugins, showNow,
                                         principal, location) {
     // It is possible that we've received a message from the frame script to show
@@ -240,26 +259,21 @@ var gPluginHandler = {
     // If this is a new notification, create a pluginData map, otherwise append
     let pluginData;
     if (notification) {
       pluginData = notification.options.pluginData;
     } else {
       pluginData = new Map();
     }
 
-    let hasInactivePlugins = true;
-    for (var pluginInfo of plugins) {
+    for (let pluginInfo of plugins) {
       if (pluginData.has(pluginInfo.permissionString)) {
         continue;
       }
 
-      if (pluginInfo.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
-        hasInactivePlugins = false;
-      }
-
       // If a block contains an infoURL, we should always prefer that to the default
       // URL that we construct in-product, even for other blocklist types.
       let url = Services.blocklist.getPluginInfoURL(pluginInfo.pluginTag);
 
       if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
         if (!url) {
           url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag);
         }
@@ -282,40 +296,115 @@ var gPluginHandler = {
       if (showNow) {
         notification.options.primaryPlugin = primaryPluginPermission;
         notification.reshow();
         browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown");
       }
       return;
     }
 
-    let options = {
-      dismissed: !showNow,
-      persistent: showNow,
-      eventCallback: this._clickToPlayNotificationEventCallback,
-      primaryPlugin: primaryPluginPermission,
-      pluginData,
-      principal,
-      extraAttr: hasInactivePlugins ? "inactive" : "active",
-    };
+    if (plugins.length == 1) {
+      let pluginInfo = plugins[0];
+      // If a block contains an infoURL, we should always prefer that to the default
+      // URL that we construct in-product, even for other blocklist types.
+      let url = Services.blocklist.getPluginInfoURL(pluginInfo.pluginTag);
+
+      if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
+        if (!url) {
+          url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag);
+        }
+      } else {
+        url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "clicktoplay";
+      }
+      pluginInfo.detailsLink = url;
+
+      let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow);
+      let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin);
+
+      let active = pluginInfo.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE;
+
+      let options = {
+        dismissed: !showNow,
+        hideClose: !Services.prefs.getBoolPref("privacy.permissionPrompts.showCloseButton"),
+        persistent: showNow,
+        eventCallback: this._clickToPlayNotificationEventCallback,
+        primaryPlugin: primaryPluginPermission,
+        popupIconClass: "plugin-icon",
+        extraAttr: active ? "active" : "inactive",
+        pluginData,
+        principal,
+      };
+
+      let description;
+      if (pluginInfo.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE) {
+        description = gNavigatorBundle.getString("flashActivate.outdated.message");
+      } else {
+        description = gNavigatorBundle.getString("flashActivate.message");
+      }
+
+      let badge = document.getElementById("plugin-icon-badge");
+      badge.setAttribute("animate", "true");
+      badge.addEventListener("animationend", function animListener(event) {
+        if (event.animationName == "blink-badge" &&
+            badge.hasAttribute("animate")) {
+          badge.removeAttribute("animate");
+          badge.removeEventListener("animationend", animListener);
+        }
+      });
 
-    let badge = document.getElementById("plugin-icon-badge");
-    badge.setAttribute("animate", "true");
-    badge.addEventListener("animationend", function animListener(event) {
-      if (event.animationName == "blink-badge" &&
-          badge.hasAttribute("animate")) {
-        badge.removeAttribute("animate");
-        badge.removeEventListener("animationend", animListener);
+      let weakBrowser = Cu.getWeakReference(browser);
+      let mainAction = {
+        callback: ({checkboxChecked}) => {
+          let browserRef = weakBrowser.get();
+          if (browserRef) {
+            if (checkboxChecked) {
+              this._updatePluginPermission(browserRef, pluginInfo, "allowalways");
+            } else if (pluginInfo.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
+              this._updatePluginPermission(browserRef, pluginInfo, "continue");
+            } else {
+              this._updatePluginPermission(browserRef, pluginInfo, "allownow");
+            }
+          }
+        },
+        label: gNavigatorBundle.getString("flashActivate.allow"),
+        accessKey: gNavigatorBundle.getString("flashActivate.allow.accesskey"),
+        dismiss: true,
+      };
+
+      let secondaryActions = null;
+      if (!isWindowPrivate) {
+        options.checkbox = {
+          label: gNavigatorBundle.getString("flashActivate.remember"),
+        };
+        secondaryActions = [{
+          callback: ({checkboxChecked}) => {
+            let browserRef = weakBrowser.get();
+            if (browserRef) {
+              if (checkboxChecked) {
+                this._updatePluginPermission(browserRef, pluginInfo, "blockalways");
+              } else if (pluginInfo.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
+                this._updatePluginPermission(browserRef, pluginInfo, "block");
+              } else {
+                this._updatePluginPermission(browserRef, pluginInfo, "continueblocking");
+              }
+            }
+          },
+          label: gNavigatorBundle.getString("flashActivate.noAllow"),
+          accessKey: gNavigatorBundle.getString("flashActivate.noAllow.accesskey"),
+          dismiss: true,
+        }];
       }
-    });
 
-    PopupNotifications.show(browser, "click-to-play-plugins",
-                            "", "plugins-notification-icon",
-                            null, null, options);
-    browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown");
+      PopupNotifications.show(browser, "click-to-play-plugins",
+                                             description, "plugins-notification-icon",
+                                             mainAction, secondaryActions, options);
+      browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown");
+    } else {
+      this.removeNotification(browser, "click-to-play-plugins");
+    }
   },
 
   removeNotification(browser, name) {
     let notification = PopupNotifications.getNotification(name, browser);
     if (notification)
       PopupNotifications.remove(notification);
   },
 
@@ -405,16 +494,17 @@ var gPluginHandler = {
       let brand = document.getElementById("bundle_brand").getString("brandShortName");
 
       if (actions.length == 1) {
         let pluginInfo = actions[0];
         let pluginName = pluginInfo.pluginName;
 
         switch (pluginInfo.fallbackType) {
           case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
+          case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET:
             message = gNavigatorBundle.getFormattedString(
               "pluginActivationWarning.message",
               [brand]);
             break;
           case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
             message = gNavigatorBundle.getFormattedString(
               "pluginActivateOutdated.message",
               [pluginName, origin, brand]);
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -997,25 +997,16 @@ html|*#fullscreen-exit-button {
   pointer-events: none;
   -moz-stack-sizing: ignore;
 }
 
 #addon-progress-notification {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#addon-progress-notification");
 }
 
-#click-to-play-plugins-notification {
-  -moz-binding: url("chrome://browser/content/urlbarBindings.xml#click-to-play-plugins-notification");
-}
-
-
-.plugin-popupnotification-centeritem {
-  -moz-binding: url("chrome://browser/content/urlbarBindings.xml#plugin-popupnotification-center-item");
-}
-
 browser[tabmodalPromptShowing] {
   -moz-user-focus: none !important;
 }
 
 /* Status panel */
 
 statuspanel {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#statuspanel");
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -340,16 +340,25 @@ pluginActivateNow.accesskey=N
 # plugin.
 pluginActivateAlways.label=Allow and Remember
 pluginActivateAlways.accesskey=R
 pluginBlockNow.label=Block Plugin
 pluginBlockNow.accesskey=B
 pluginContinue.label=Continue Allowing
 pluginContinue.accesskey=C
 
+# Flash activation doorhanger UI
+flashActivate.message=Do you want to allow Adobe Flash to run on this site? Only allow Flash on sites you trust.
+flashActivate.outdated.message=Do you want to allow an outdated version of Flash to run on this site? An outdated version can affect browser performance and security.
+flashActivate.remember=Remember this decision
+flashActivate.noAllow=Don’t Allow
+flashActivate.allow=Allow
+flashActivate.noAllow.accesskey=D
+flashActivate.allow.accesskey=A
+
 # in-page UI
 PluginClickToActivate=Activate %S.
 PluginVulnerableUpdatable=This plugin is vulnerable and should be updated.
 PluginVulnerableNoUpdate=This plugin has security vulnerabilities.
 
 # infobar UI
 pluginContinueBlocking.label=Continue Blocking
 pluginContinueBlocking.accesskey=B
--- a/browser/modules/PluginContent.jsm
+++ b/browser/modules/PluginContent.jsm
@@ -25,16 +25,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 this.PluginContent = function(global) {
   this.init(global);
 };
 
 const FLASH_MIME_TYPE = "application/x-shockwave-flash";
 const REPLACEMENT_STYLE_SHEET = Services.io.newURI("chrome://pluginproblem/content/pluginReplaceBinding.css");
 
+const OVERLAY_DISPLAY_HIDDEN = 0;
+const OVERLAY_DISPLAY_VISIBLE = 1;
+const OVERLAY_DISPLAY_MINIMAL = 2;
+
 PluginContent.prototype = {
   init(global) {
     this.global = global;
     // Need to hold onto the content window or else it'll get destroyed
     this.content = this.global.content;
     // Cache of plugin actions for the current page.
     this.pluginData = new Map();
     // Cache of plugin crash information sent from the parent
@@ -279,46 +283,55 @@ PluginContent.prototype = {
              fallbackType: Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY,
              blocklistState,
            };
   },
 
   /**
    * Update the visibility of the plugin overlay.
    */
-  setVisibility(plugin, overlay, shouldShow) {
-    overlay.classList.toggle("visible", shouldShow);
-    if (shouldShow) {
+  setVisibility(plugin, overlay, overlayDisplayState) {
+    overlay.classList.toggle("visible", overlayDisplayState != OVERLAY_DISPLAY_HIDDEN);
+    overlay.classList.toggle("minimal", overlayDisplayState == OVERLAY_DISPLAY_MINIMAL)
+    if (overlayDisplayState == OVERLAY_DISPLAY_VISIBLE) {
       overlay.removeAttribute("dismissed");
     }
   },
 
   /**
    * Check whether the plugin should be visible on the page. A plugin should
    * not be visible if the overlay is too big, or if any other page content
    * overlays it.
    *
    * This function will handle showing or hiding the overlay.
    * @returns true if the plugin is invisible.
    */
-  shouldShowOverlay(plugin, overlay) {
+  computeOverlayDisplayState(plugin, overlay) {
+    let fallbackType = plugin.pluginFallbackType;
+    if (plugin.pluginFallbackTypeOverride !== undefined) {
+      fallbackType = plugin.pluginFallbackTypeOverride;
+    }
+    if (fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET) {
+      return OVERLAY_DISPLAY_HIDDEN;
+    }
+
     // If the overlay size is 0, we haven't done layout yet. Presume that
     // plugins are visible until we know otherwise.
     if (overlay.scrollWidth == 0) {
-      return true;
+      return OVERLAY_DISPLAY_VISIBLE;
     }
 
     // Is the <object>'s size too small to hold what we want to show?
     let pluginRect = plugin.getBoundingClientRect();
     // XXX bug 446693. The text-shadow on the submitted-report text at
     //     the bottom causes scrollHeight to be larger than it should be.
     let overflows = (overlay.scrollWidth > Math.ceil(pluginRect.width)) ||
                     (overlay.scrollHeight - 5 > Math.ceil(pluginRect.height));
     if (overflows) {
-      return false;
+      return OVERLAY_DISPLAY_MINIMAL;
     }
 
     // Is the plugin covered up by other content so that it is not clickable?
     // Floating point can confuse .elementFromPoint, so inset just a bit
     let left = pluginRect.left + 2;
     let right = pluginRect.right - 2;
     let top = pluginRect.top + 2;
     let bottom = pluginRect.bottom - 2;
@@ -335,21 +348,21 @@ PluginContent.prototype = {
                            .getInterface(Ci.nsIDOMWindowUtils);
 
     for (let [x, y] of points) {
       if (x < 0 || y < 0) {
         continue;
       }
       let el = cwu.elementFromPoint(x, y, true, true);
       if (el === plugin) {
-        return true;
+        return OVERLAY_DISPLAY_VISIBLE;
       }
     }
 
-    return false;
+    return OVERLAY_DISPLAY_HIDDEN;
   },
 
   addLinkClickCallback(linkNode, callbackName /* callbackArgs...*/) {
     // XXX just doing (callback)(arg) was giving a same-origin error. bug?
     let self = this;
     let callbackArgs = Array.prototype.slice.call(arguments).slice(2);
     linkNode.addEventListener("click",
                               function(evt) {
@@ -387,16 +400,17 @@ PluginContent.prototype = {
         return "PluginNotFound";
       case Ci.nsIObjectLoadingContent.PLUGIN_DISABLED:
         return "PluginDisabled";
       case Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED:
         return "PluginBlocklisted";
       case Ci.nsIObjectLoadingContent.PLUGIN_OUTDATED:
         return "PluginOutdated";
       case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
+      case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET:
         return "PluginClickToPlay";
       case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE:
         return "PluginVulnerableUpdatable";
       case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE:
         return "PluginVulnerableNoUpdate";
       default:
         // Not all states map to a handler
         return null;
@@ -457,17 +471,17 @@ PluginContent.prototype = {
       }
     }
 
     let plugin = event.target;
 
     if (eventType == "PluginPlaceholderReplaced") {
       plugin.removeAttribute("href");
       let overlay = this.getPluginUI(plugin, "main");
-      this.setVisibility(plugin, overlay, true);
+      this.setVisibility(plugin, overlay, OVERLAY_DISPLAY_VISIBLE);
       let inIDOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
                           .getService(Ci.inIDOMUtils);
       // Add psuedo class so our styling will take effect
       inIDOMUtils.addPseudoClassLock(plugin, "-moz-handler-clicktoplay");
       overlay.addEventListener("click", this, true);
       return;
     }
 
@@ -542,20 +556,20 @@ PluginContent.prototype = {
         break;
     }
 
     // Show the in-content UI if it's not too big. The crashed plugin handler already did this.
     let overlay = this.getPluginUI(plugin, "main");
     if (eventType != "PluginCrashed") {
       if (overlay != null) {
         this.setVisibility(plugin, overlay,
-                           this.shouldShowOverlay(plugin, overlay));
+                           this.computeOverlayDisplayState(plugin, overlay));
         let resizeListener = () => {
           this.setVisibility(plugin, overlay,
-            this.shouldShowOverlay(plugin, overlay));
+            this.computeOverlayDisplayState(plugin, overlay));
           this.updateNotificationUI();
         };
         plugin.addEventListener("overflow", resizeListener);
         plugin.addEventListener("underflow", resizeListener);
       }
     }
 
     let closeIcon = this.getPluginUI(plugin, "closeIcon");
@@ -587,17 +601,17 @@ PluginContent.prototype = {
 
     let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
     let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType);
     let principal = objLoadingContent.ownerGlobal.top.document.nodePrincipal;
     let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString);
 
     let isFallbackTypeValid =
       objLoadingContent.pluginFallbackType >= Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY &&
-      objLoadingContent.pluginFallbackType <= Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE;
+      objLoadingContent.pluginFallbackType <= Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET;
 
     return !objLoadingContent.activated &&
            pluginPermission != Ci.nsIPermissionManager.DENY_ACTION &&
            isFallbackTypeValid;
   },
 
   hideClickToPlayOverlay(plugin) {
     let overlay = this.getPluginUI(plugin, "main");
@@ -660,17 +674,18 @@ PluginContent.prototype = {
       permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType);
     }
 
     let principal = doc.defaultView.top.document.nodePrincipal;
     let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString);
 
     let overlay = this.getPluginUI(plugin, "main");
 
-    if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION) {
+    if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION ||
+        pluginPermission == Ci.nsIObjectLoadingContent.PLUGIN_PERMISSION_PROMPT_ACTION_QUIET) {
       if (overlay) {
         overlay.classList.remove("visible");
       }
       return;
     }
 
     if (overlay) {
       overlay.addEventListener("click", this, true);
@@ -728,35 +743,36 @@ PluginContent.prototype = {
       }
       if (pluginInfo.permissionString == pluginHost.getPermissionStringForType(plugin.actualType)) {
         let overlay = this.getPluginUI(plugin, "main");
         if (ChromeUtils.getClassName(plugin) === "HTMLAnchorElement") {
           placeHolderFound = true;
         } else {
           pluginFound = true;
         }
-        if (newState == "block") {
+        if (newState == "block" || newState == "blockalways" || newState == "continueblocking") {
           if (overlay) {
             overlay.addEventListener("click", this, true);
           }
+          plugin.pluginFallbackTypeOverride = pluginInfo.fallbackType;
           plugin.reload(true);
         } else if (this.canActivatePlugin(plugin)) {
           if (overlay) {
             overlay.removeEventListener("click", this, true);
           }
           plugin.playPlugin();
         }
       }
     }
 
     // If there are no instances of the plugin on the page any more, what the
     // user probably needs is for us to allow and then refresh. Additionally, if
     // this is content that requires HLS or we replaced the placeholder the page
     // needs to be refreshed for it to insert its plugins
-    if (newState != "block" &&
+    if (newState != "block" && newState != "blockalways" && newState != "continueblocking" &&
        (!pluginFound || placeHolderFound || contentWindow.pluginRequiresReload)) {
       this.reloadPage();
     }
     this.updateNotificationUI();
   },
 
   _showClickToPlayNotification(plugin, showNow) {
     let plugins = [];
@@ -849,16 +865,17 @@ PluginContent.prototype = {
       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
 
+        case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET:
         case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
           actions.set(action.permissionString, action);
           continue;
       }
     }
 
     // Remove plugins that are already active, or large enough to show an overlay.
     let cwu = this.content.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -872,27 +889,28 @@ PluginContent.prototype = {
       if (fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
         actions.delete(info.permissionString);
         if (actions.size == 0) {
           break;
         }
         continue;
       }
       if (fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY &&
+          fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY_QUIET &&
           fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE &&
           fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE) {
         continue;
       }
       let overlay = this.getPluginUI(plugin, "main");
       if (!overlay) {
         continue;
       }
-      let shouldShow = this.shouldShowOverlay(plugin, overlay);
-      this.setVisibility(plugin, overlay, shouldShow);
-      if (shouldShow) {
+      let overlayDisplayState = this.computeOverlayDisplayState(plugin, overlay);
+      this.setVisibility(plugin, overlay, overlayDisplayState);
+      if (overlayDisplayState == OVERLAY_DISPLAY_VISIBLE) {
         actions.delete(info.permissionString);
         if (actions.size == 0) {
           break;
         }
       }
     }
 
     // If there are any items remaining in `actions` now, they are hidden
@@ -1066,31 +1084,31 @@ PluginContent.prototype = {
     this.addLinkClickCallback(helpIcon, "openHelpPage");
 
     let crashText = this.getPluginUI(plugin, "crashedText");
     crashText.textContent = message;
 
     let link = this.getPluginUI(plugin, "reloadLink");
     this.addLinkClickCallback(link, "reloadPage");
 
-    let isShowing = this.shouldShowOverlay(plugin, overlay);
+    let overlayDisplayState = this.computeOverlayDisplayState(plugin, overlay);
 
     // Is the <object>'s size too small to hold what we want to show?
-    if (!isShowing) {
+    if (overlayDisplayState != OVERLAY_DISPLAY_VISIBLE) {
       // First try hiding the crash report submission UI.
       statusDiv.removeAttribute("status");
 
-      isShowing = this.shouldShowOverlay(plugin, overlay);
+      overlayDisplayState = this.computeOverlayDisplayState(plugin, overlay);
     }
-    this.setVisibility(plugin, overlay, isShowing);
+    this.setVisibility(plugin, overlay, overlayDisplayState);
 
     let doc = plugin.ownerDocument;
     let runID = plugin.runID;
 
-    if (isShowing) {
+    if (overlayDisplayState == OVERLAY_DISPLAY_VISIBLE) {
       // If a previous plugin on the page was too small and resulted in adding a
       // notification bar, then remove it because this plugin instance it big
       // enough to serve as in-content notification.
       this.hideNotificationBar("plugin-crashed");
       doc.mozNoPluginCrashedNotification = true;
 
       // Notify others that the crash reporter UI is now ready.
       // Currently, this event is only used by tests.
--- a/dom/base/nsIObjectLoadingContent.idl
+++ b/dom/base/nsIObjectLoadingContent.idl
@@ -54,23 +54,32 @@ interface nsIObjectLoadingContent : nsIS
   const unsigned long PLUGIN_CRASHED              = 5;
   // Suppressed by security policy
   const unsigned long PLUGIN_SUPPRESSED           = 6;
   // Blocked by content policy
   const unsigned long PLUGIN_USER_DISABLED        = 7;
   /// ** All values >= PLUGIN_CLICK_TO_PLAY are plugin placeholder types that
   ///    would be replaced by a real plugin if activated (playPlugin())
   /// ** Furthermore, values >= PLUGIN_CLICK_TO_PLAY and
-  ///    <= PLUGIN_VULNERABLE_NO_UPDATE are click-to-play types.
+  ///    <= PLUGIN_CLICK_TO_PLAY_QUIET are click-to-play types.
   // The plugin is disabled until the user clicks on it
   const unsigned long PLUGIN_CLICK_TO_PLAY        = 8;
   // The plugin is vulnerable (update available)
   const unsigned long PLUGIN_VULNERABLE_UPDATABLE = 9;
   // The plugin is vulnerable (no update available)
   const unsigned long PLUGIN_VULNERABLE_NO_UPDATE = 10;
+  // The plugin is click-to-play, but the user won't see overlays
+  const unsigned long PLUGIN_CLICK_TO_PLAY_QUIET  = 11;
+
+  // Plugins-specific permission indicating that we want to prompt the user
+  // to decide whether they want to allow a plugin, but to do so in a less
+  // intrusive way than PROMPT_ACTION would entail. At the time of writing,
+  // this means hiding all in-content plugin overlays, but still showing the
+  // plugin badge in the URL bar.
+  const unsigned long PLUGIN_PERMISSION_PROMPT_ACTION_QUIET = 8;
 
   /**
    * The actual mime type (the one we got back from the network
    * request) for the element.
    */
   readonly attribute ACString actualType;
 
   /**
--- a/dom/base/nsObjectLoadingContent.cpp
+++ b/dom/base/nsObjectLoadingContent.cpp
@@ -1328,16 +1328,17 @@ nsObjectLoadingContent::ObjectState() co
       return EventStates();
     case eType_Null:
       switch (mFallbackType) {
         case eFallbackSuppressed:
           return NS_EVENT_STATE_SUPPRESSED;
         case eFallbackUserDisabled:
           return NS_EVENT_STATE_USERDISABLED;
         case eFallbackClickToPlay:
+        case eFallbackClickToPlayQuiet:
           return NS_EVENT_STATE_TYPE_CLICK_TO_PLAY;
         case eFallbackDisabled:
           return NS_EVENT_STATE_BROKEN | NS_EVENT_STATE_HANDLER_DISABLED;
         case eFallbackBlocklisted:
           return NS_EVENT_STATE_BROKEN | NS_EVENT_STATE_HANDLER_BLOCKED;
         case eFallbackCrashed:
           return NS_EVENT_STATE_BROKEN | NS_EVENT_STATE_HANDLER_CRASHED;
         case eFallbackUnsupported:
@@ -2373,17 +2374,17 @@ nsObjectLoadingContent::LoadObject(bool 
   // If we didn't load anything, handle switching to fallback state
   if (mType == eType_Null) {
     LOG(("OBJLC [%p]: Loading fallback, type %u", this, fallbackType));
     NS_ASSERTION(!mFrameLoader && !mInstanceOwner,
                  "switched to type null but also loaded something");
 
     // Don't fire error events if we're falling back to click-to-play; instead
     // pretend like this is a really slow-loading plug-in instead.
-    if (fallbackType != eFallbackClickToPlay) {
+    if (fallbackType != eFallbackClickToPlay && fallbackType != eFallbackClickToPlayQuiet) {
       MaybeFireErrorEvent();
     }
 
     if (mChannel) {
       // If we were loading with a channel but then failed over, throw it away
       CloseChannel();
     }
 
@@ -3338,16 +3339,24 @@ nsObjectLoadingContent::ShouldPlay(Fallb
         aReason = eFallbackAlternate;
         return false;
       }
 
       return true;
     case nsIPermissionManager::DENY_ACTION:
       aReason = eFallbackDisabled;
       return false;
+    case PLUGIN_PERMISSION_PROMPT_ACTION_QUIET:
+      if (PreferFallback(true /* isPluginClickToPlay */)) {
+        aReason = eFallbackAlternate;
+      } else {
+        aReason = eFallbackClickToPlayQuiet;
+      }
+
+      return false;
     case nsIPermissionManager::PROMPT_ACTION:
       if (PreferFallback(true /* isPluginClickToPlay */)) {
         // False is already returned in this case, but
         // it's important to correctly set aReason too.
         aReason = eFallbackAlternate;
       }
 
       return false;
--- a/dom/base/nsObjectLoadingContent.h
+++ b/dom/base/nsObjectLoadingContent.h
@@ -89,23 +89,25 @@ class nsObjectLoadingContent : public ns
       eFallbackCrashed = nsIObjectLoadingContent::PLUGIN_CRASHED,
       // Suppressed by security policy
       eFallbackSuppressed = nsIObjectLoadingContent::PLUGIN_SUPPRESSED,
       // Blocked by content policy
       eFallbackUserDisabled = nsIObjectLoadingContent::PLUGIN_USER_DISABLED,
       /// ** All values >= eFallbackClickToPlay are plugin placeholder types
       ///    that would be replaced by a real plugin if activated (PlayPlugin())
       /// ** Furthermore, values >= eFallbackClickToPlay and
-      ///    <= eFallbackVulnerableNoUpdate are click-to-play types.
+      ///    <= eFallbackClickToPlayQuiet are click-to-play types.
       // The plugin is disabled until the user clicks on it
       eFallbackClickToPlay = nsIObjectLoadingContent::PLUGIN_CLICK_TO_PLAY,
       // The plugin is vulnerable (update available)
       eFallbackVulnerableUpdatable = nsIObjectLoadingContent::PLUGIN_VULNERABLE_UPDATABLE,
       // The plugin is vulnerable (no update available)
       eFallbackVulnerableNoUpdate = nsIObjectLoadingContent::PLUGIN_VULNERABLE_NO_UPDATE,
+      // The plugin is click-to-play, but the user won't see overlays
+      eFallbackClickToPlayQuiet = nsIObjectLoadingContent::PLUGIN_CLICK_TO_PLAY_QUIET,
     };
 
     nsObjectLoadingContent();
     virtual ~nsObjectLoadingContent();
 
     NS_DECL_NSIREQUESTOBSERVER
     NS_DECL_NSISTREAMLISTENER
     NS_DECL_NSIFRAMELOADEROWNER
--- a/toolkit/pluginproblem/content/pluginProblemContent.css
+++ b/toolkit/pluginproblem/content/pluginProblemContent.css
@@ -44,30 +44,40 @@ a .mainBox:focus,
   /* used to block inherited properties */
   text-transform: none;
   text-indent: 0;
   cursor: initial;
   white-space: initial;
   word-spacing: initial;
   letter-spacing: initial;
   line-height: initial;
+  visibility: hidden;
+}
+
+.visible {
+  visibility: visible;
 }
 
 /* Initialize the overlay with visibility:hidden to prevent flickering if
 * the plugin is too small to show the overlay */
 .mainBox > .hoverBox,
 .mainBox > .closeIcon {
   visibility: hidden;
 }
 
 .visible > .hoverBox,
 .visible > .closeIcon {
   visibility: visible;
 }
 
+.minimal > .hoverBox,
+.minimal > .closeIcon {
+  visibility: hidden;
+}
+
 .mainBox[chromedir="rtl"] {
   direction: rtl;
 }
 
 a .hoverBox,
 :-moz-handler-clicktoplay .hoverBox,
 :-moz-handler-vulnerable-updatable .hoverBox,
 :-moz-handler-vulnerable-no-update .hoverBox {