Bug 889788 - Plugin doorhanger breaks when plugins are dynamically removed from a page. This implements the short-term solution of hiding the plugin doorhanger when no more plugins are on the page (the better long-term solution will keep showing the UI and track all the plugins a page ever uses). This also implements a short-term solution when a user enables a plugin. Also bug 887088 - Short-term UI solution: when a user loads multiple tabs from a site and enables a plugin on one of them, the plugins are not enabled on other tabs but the "continue allowing" button does nothing. This patch makes the "Continue Allowing" button enable existing plugins of that type. r=jaws r=jschoenick
authorBenjamin Smedberg <benjamin@smedbergs.us>
Fri, 19 Jul 2013 10:02:48 -0400
changeset 139619 678e1cb9f5879777f96aab5c4d46467c8180fc7c
parent 139618 62429a874f06cc824474be1869f74afe7a44b72c
child 139620 3f3a0ed44893e8f3d87399551dd04216825ba9da
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersjaws, jschoenick
bugs889788, 887088
milestone25.0a1
Bug 889788 - Plugin doorhanger breaks when plugins are dynamically removed from a page. This implements the short-term solution of hiding the plugin doorhanger when no more plugins are on the page (the better long-term solution will keep showing the UI and track all the plugins a page ever uses). This also implements a short-term solution when a user enables a plugin. Also bug 887088 - Short-term UI solution: when a user loads multiple tabs from a site and enables a plugin on one of them, the plugins are not enabled on other tabs but the "continue allowing" button does nothing. This patch makes the "Continue Allowing" button enable existing plugins of that type. r=jaws r=jschoenick
browser/base/content/browser-plugins.js
browser/base/content/browser.js
browser/base/content/urlbarBindings.xml
content/base/src/nsObjectLoadingContent.cpp
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -212,24 +212,31 @@ var gPluginHandler = {
     let pluginName = this.nameForSupportedPlugin(aMimeType);
     if (pluginName && "install" + os in this.supportedPlugins.plugins[pluginName]) {
       return true;
     }
     return false;
   },
 
   handleEvent : function(event) {
-    let plugin = event.target;
-    let doc = plugin.ownerDocument;
-
-    // We're expecting the target to be a plugin.
-    if (!(plugin instanceof Ci.nsIObjectLoadingContent))
-      return;
+    let plugin;
+    let doc;
 
     let eventType = event.type;
+    if (eventType === "PluginRemoved") {
+      doc = event.target;
+    }
+    else {
+      plugin = event.target;
+      doc = plugin.ownerDocument;
+
+      if (!(plugin instanceof Ci.nsIObjectLoadingContent))
+        return;
+    }
+
     if (eventType == "PluginBindingAttached") {
       // The plugin binding fires this event when it is created.
       // As an untrusted event, ensure that this object actually has a binding
       // and make sure we don't handle it twice
       let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
       if (!overlay || overlay._bindingHandled) {
         return;
       }
@@ -299,22 +306,23 @@ var gPluginHandler = {
 
       case "PluginDisabled":
         let manageLink = doc.getAnonymousElementByAttribute(plugin, "class", "managePluginsLink");
         this.addLinkClickCallback(manageLink, "managePlugins");
         this._showClickToPlayNotification(browser);
         break;
 
       case "PluginInstantiated":
+      case "PluginRemoved":
         this._showClickToPlayNotification(browser);
         break;
     }
 
     // Hide the in-content UI if it's too big. The crashed plugin handler already did this.
-    if (eventType != "PluginCrashed") {
+    if (eventType != "PluginCrashed" && eventType != "PluginRemoved") {
       let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
       if (overlay != null && this.isTooSmall(plugin, overlay))
         overlay.style.visibility = "hidden";
     }
   },
 
   isKnownPlugin: function PH_isKnownPlugin(objLoadingContent) {
     return (objLoadingContent.getContentTypeForMIMEType(objLoadingContent.actualType) ==
@@ -681,54 +689,51 @@ var gPluginHandler = {
    */
   _updatePluginPermission: function PH_setPermissionForPlugins(aNotification, aPluginInfo, aNewState) {
     let permission;
     let expireType;
     let expireTime;
 
     switch (aNewState) {
       case "allownow":
-        if (aPluginInfo.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
-          return;
-        }
         permission = Ci.nsIPermissionManager.ALLOW_ACTION;
         expireType = Ci.nsIPermissionManager.EXPIRE_SESSION;
         expireTime = Date.now() + Services.prefs.getIntPref(this.PREF_SESSION_PERSIST_MINUTES) * 60 * 1000;
         break;
 
       case "allowalways":
-        if (aPluginInfo.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
-          return;
-        }
         permission = Ci.nsIPermissionManager.ALLOW_ACTION;
         expireType = Ci.nsIPermissionManager.EXPIRE_TIME;
         expireTime = Date.now() +
           Services.prefs.getIntPref(this.PREF_PERSISTENT_DAYS) * 24 * 60 * 60 * 1000;
         break;
 
       case "block":
-        if (aPluginInfo.fallbackType != Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
-          return;
-        }
         permission = Ci.nsIPermissionManager.PROMPT_ACTION;
         expireType = Ci.nsIPermissionManager.EXPIRE_NEVER;
         expireTime = 0;
         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 "continue":
+        break;
       default:
         Cu.reportError(Error("Unexpected plugin state: " + aNewState));
         return;
     }
 
     let browser = aNotification.browser;
-    Services.perms.add(browser.currentURI, aPluginInfo.permissionString,
-                       permission, expireType, expireTime);
+    if (aNewState != "continue") {
+      Services.perms.add(browser.currentURI, aPluginInfo.permissionString,
+                         permission, expireType, expireTime);
 
-    if (aNewState == "block") {
-      return;
+      if (aNewState == "block") {
+        return;
+      }
     }
 
     // Manually activate the plugins that would have been automatically
     // activated.
     let contentWindow = browser.contentWindow;
     let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils);
     let plugins = cwu.plugins;
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -750,16 +750,17 @@ var gBrowserInit = {
 
     gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false);
 
     // Note that the XBL binding is untrusted
     gBrowser.addEventListener("PluginBindingAttached", gPluginHandler, true, true);
     gBrowser.addEventListener("PluginCrashed",         gPluginHandler, true);
     gBrowser.addEventListener("PluginOutdated",        gPluginHandler, true);
     gBrowser.addEventListener("PluginInstantiated",    gPluginHandler, true);
+    gBrowser.addEventListener("PluginRemoved",         gPluginHandler, true);
 
     gBrowser.addEventListener("NewPluginInstalled", gPluginHandler.newPluginInstalled, true);
 
     Services.obs.addObserver(gPluginHandler.pluginCrashed, "plugin-crashed", false);
 
     window.addEventListener("AppCommand", HandleAppCommandEvent, true);
 
     messageManager.loadFrameScript("chrome://browser/content/content.js", true);
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1569,22 +1569,29 @@
         const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
         for (let action of this.notification.options.centerActions) {
           let item = document.createElementNS(XUL_NS, "row");
           item.setAttribute("class", "plugin-popupnotification-centeritem");
           item.action = action;
           this.appendChild(item);
           this._items.push(item);
         }
-        if (this.notification.options.centerActions.length == 1) {
-          this._setState(this._states.SINGLE);
-        } else if (this.notification.options.primaryPlugin) {
-          this._setState(this._states.MULTI_COLLAPSED);
-        } else {
-          this._setState(this._states.MULTI_EXPANDED);
+        switch (this.notification.options.centerActions.length) {
+          case 0:
+            PopupNotifications._dismiss();
+            break;
+          case 1:
+            this._setState(this._states.SINGLE);
+            break;
+          default:
+            if (this.notification.options.primaryPlugin) {
+              this._setState(this._states.MULTI_COLLAPSED);
+            } else {
+              this._setState(this._states.MULTI_EXPANDED);
+            }
         }
       ]]></constructor>
       <method name="_setState">
         <parameter name="state" />
         <body><![CDATA[
           var grid = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-center-box");
 
           if (this._states.SINGLE == state) {
@@ -1640,17 +1647,17 @@
             button1 = {
               label: "pluginBlockNow.label",
               accesskey: "pluginBlockNow.accesskey",
               action: "_singleBlock"
             };
             button2 = {
               label: "pluginContinue.label",
               accesskey: "pluginContinue.accesskey",
-              action: "_cancel",
+              action: "_singleContinue",
               default: true
             };
             switch (action.blocklistState) {
             case Ci.nsIBlocklistService.STATE_NOT_BLOCKED:
               label = "pluginEnabled.message";
               linkLabel = "pluginActivate.learnMore";
               break;
 
@@ -1814,16 +1821,24 @@
       <method name="_singleActivateAlways">
         <body><![CDATA[
           gPluginHandler._updatePluginPermission(this.notification,
             this.notification.options.centerActions[0],
             "allowalways");
           this._cancel();
         ]]></body>
       </method>
+      <method name="_singleContinue">
+        <body><![CDATA[
+          gPluginHandler._updatePluginPermission(this.notification,
+            this.notification.options.centerActions[0],
+            "continue");
+          this._cancel();
+        ]]></body>
+      </method>
       <method name="_multiAccept">
         <body><![CDATA[
           for (let item of this._items) {
             let action = item.action;
             if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED ||
                 action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
               continue;
             }
--- a/content/base/src/nsObjectLoadingContent.cpp
+++ b/content/base/src/nsObjectLoadingContent.cpp
@@ -191,36 +191,46 @@ CheckPluginStopEvent::Run()
   return NS_OK;
 }
 
 /**
  * Helper task for firing simple events
  */
 class nsSimplePluginEvent : public nsRunnable {
 public:
-  nsSimplePluginEvent(nsIContent* aContent, const nsAString &aEvent)
-    : mContent(aContent),
-      mEvent(aEvent)
-  {}
+  nsSimplePluginEvent(nsIContent* aTarget, const nsAString &aEvent)
+    : mTarget(aTarget)
+    , mDocument(aTarget->GetCurrentDoc())
+    , mEvent(aEvent)
+  {
+  }
+
+  nsSimplePluginEvent(nsIDocument* aTarget, const nsAString& aEvent)
+    : mTarget(aTarget)
+    , mDocument(aTarget)
+    , mEvent(aEvent)
+  {
+  }
 
   ~nsSimplePluginEvent() {}
 
   NS_IMETHOD Run();
 
 private:
-  nsCOMPtr<nsIContent> mContent;
+  nsCOMPtr<nsISupports> mTarget;
+  nsCOMPtr<nsIDocument> mDocument;
   nsString mEvent;
 };
 
 NS_IMETHODIMP
 nsSimplePluginEvent::Run()
 {
-  LOG(("OBJLC [%p]: nsSimplePluginEvent firing event \"%s\"", mContent.get(),
+  LOG(("OBJLC [%p]: nsSimplePluginEvent firing event \"%s\"", mTarget.get(),
        mEvent.get()));
-  nsContentUtils::DispatchTrustedEvent(mContent->GetDocument(), mContent,
+  nsContentUtils::DispatchTrustedEvent(mDocument, mTarget,
                                        mEvent, true, true);
   return NS_OK;
 }
 
 /**
  * A task for firing PluginCrashed DOM Events.
  */
 class nsPluginCrashedEvent : public nsRunnable {
@@ -669,17 +679,19 @@ nsObjectLoadingContent::UnbindFromTree(b
     QueueCheckPluginStopEvent();
   } else if (mType != eType_Image) {
     // nsImageLoadingContent handles the image case.
     // Reset state and clear pending events
     /// XXX(johns): The implementation for GenericFrame notes that ideally we
     ///             would keep the docshell around, but trash the frameloader
     UnloadObject();
   }
-
+  nsCOMPtr<nsIRunnable> ev = new nsSimplePluginEvent(thisContent->GetCurrentDoc(),
+                                           NS_LITERAL_STRING("PluginRemoved"));
+  NS_DispatchToCurrentThread(ev);
 }
 
 nsObjectLoadingContent::nsObjectLoadingContent()
   : mType(eType_Loading)
   , mFallbackType(eFallbackAlternate)
   , mChannelLoaded(false)
   , mInstantiating(false)
   , mNetworkCreated(true)