Bug 1398972 - Replace usage of plugin doorhanger XBL binding r?felipe draft
authorDoug Thayer <dothayer@mozilla.com>
Tue, 31 Oct 2017 13:46:49 -0700
changeset 689650 f34ccfc77c79250f9f44427c5fe9bb0bbfaa1a2e
parent 689459 0a7ff6e19bcc229500d92597fac9340d9bdef959
child 689651 1f2d0bb39ea06f3e5e0c4de84f101630f31b407a
push id87073
push userbmo:dothayer@mozilla.com
push dateTue, 31 Oct 2017 20:46:58 +0000
reviewersfelipe
bugs1398972
milestone58.0a1
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 Adobe Flash on sites you trust.
+flashActivate.outdated.message=Do you want to allow an outdated version of Adobe 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 {