Bug 1209602 - XUL: Implement disabling notifications for a site. r=MattN
authorJared Wein <jwein@mozilla.com>
Thu, 15 Oct 2015 22:58:56 -0400
changeset 268508 1a8291d0b02662a9c2bbc82c8c43a2ee0bc548b0
parent 268507 399f9492ad3c3d7f9ffc4859dfeeeacb5b555ceb
child 268509 caaa8c13b98f458d1ea7424881f92196bfd99aac
push id15766
push userjwein@mozilla.com
push dateTue, 20 Oct 2015 16:10:33 +0000
treeherderfx-team@1a8291d0b026 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1209602
milestone44.0a1
Bug 1209602 - XUL: Implement disabling notifications for a site. r=MattN
browser/base/content/test/alerts/browser.ini
browser/base/content/test/alerts/browser_notification_remove_permission.js
browser/base/content/test/alerts/browser_notification_tab_switching.js
toolkit/components/alerts/nsAlertsUtils.cpp
toolkit/components/alerts/nsAlertsUtils.h
toolkit/components/alerts/nsXULAlerts.cpp
toolkit/components/alerts/resources/content/alert.js
toolkit/components/alerts/resources/content/alert.xul
toolkit/locales/en-US/chrome/alerts/alert.properties
toolkit/themes/shared/alert-common.css
widget/cocoa/OSXNotificationCenter.mm
--- a/browser/base/content/test/alerts/browser.ini
+++ b/browser/base/content/test/alerts/browser.ini
@@ -1,7 +1,9 @@
 [DEFAULT]
 support-files =
   file_dom_notifications.html
 
 [browser_notification_open_settings.js]
+[browser_notification_remove_permission.js]
+skip-if = e10s
 [browser_notification_tab_switching.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1100662 - content access causing uncaught exception - Error: cannot ipc non-cpow object at chrome://mochitests/content/browser/browser/base/content/test/general/browser_notification_tab_switching.js:32 (or in RemoteAddonsChild.jsm)
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_remove_permission.js
@@ -0,0 +1,79 @@
+"use strict";
+
+var tab;
+var notification;
+var notificationURL = "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
+var alertWindowClosed = false;
+var permRemoved = false;
+
+function test () {
+  waitForExplicitFinish();
+
+  let pm = Services.perms;
+  registerCleanupFunction(function() {
+    pm.remove(makeURI(notificationURL), "desktop-notification");
+    gBrowser.removeTab(tab);
+    window.restore();
+  });
+
+  pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION);
+
+  tab = gBrowser.addTab(notificationURL);
+  gBrowser.selectedTab = tab;
+  tab.linkedBrowser.addEventListener("load", onLoad, true);
+}
+
+function onLoad() {
+  tab.linkedBrowser.removeEventListener("load", onLoad, true);
+  let win = tab.linkedBrowser.contentWindow.wrappedJSObject;
+  notification = win.showNotification2();
+  notification.addEventListener("show", onAlertShowing);
+}
+
+function onAlertShowing() {
+  info("Notification alert showing");
+  notification.removeEventListener("show", onAlertShowing);
+
+  let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
+  if (!alertWindow) {
+    ok(true, "Notifications don't use XUL windows on all platforms.");
+    notification.close();
+    finish();
+    return;
+  }
+  ok(Services.perms.testExactPermission(makeURI(notificationURL), "desktop-notification"),
+     "Permission should exist prior to removal");
+  let disableForOriginMenuItem = alertWindow.document.getElementById("disableForOriginMenuItem");
+  is(disableForOriginMenuItem.localName, "menuitem", "menuitem found");
+  Services.obs.addObserver(permObserver, "perm-changed", false);
+  alertWindow.addEventListener("beforeunload", onAlertClosing);
+  disableForOriginMenuItem.click();
+  info("Clicked on disable-for-origin menuitem")
+}
+
+function permObserver(subject, topic, data) {
+  if (topic != "perm-changed") {
+    return;
+  }
+
+  let permission = subject.QueryInterface(Ci.nsIPermission);
+  is(permission.type, "desktop-notification", "desktop-notification permission changed");
+  is(data, "deleted", "desktop-notification permission deleted");
+
+  Services.obs.removeObserver(permObserver, "perm-changed");
+  permRemoved = true;
+  if (alertWindowClosed) {
+    finish();
+  }
+}
+
+function onAlertClosing(event) {
+  event.target.removeEventListener("beforeunload", onAlertClosing);
+
+  alertWindowClosed = true;
+  if (permRemoved) {
+    finish();
+  }
+}
+
+
--- a/browser/base/content/test/alerts/browser_notification_tab_switching.js
+++ b/browser/base/content/test/alerts/browser_notification_tab_switching.js
@@ -55,17 +55,17 @@ function onLoad() {
 }
 
 function onAlertShowing() {
   info("Notification alert showing");
   notification.removeEventListener("show", onAlertShowing);
 
   let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
   if (!alertWindow) {
-    todo(false, "Notifications don't use XUL windows on all platforms.");
+    ok(true, "Notifications don't use XUL windows on all platforms.");
     notification.close();
     newWindowOpenedFromTab.close();
     finish();
     return;
   }
   gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect);
   EventUtils.synthesizeMouseAtCenter(alertWindow.document.getElementById("alertTitleLabel"), {}, alertWindow);
   info("Clicked on notification");
--- a/toolkit/components/alerts/nsAlertsUtils.cpp
+++ b/toolkit/components/alerts/nsAlertsUtils.cpp
@@ -5,58 +5,27 @@
 #include "nsAlertsUtils.h"
 
 #include "nsCOMPtr.h"
 #include "nsContentUtils.h"
 #include "nsIStringBundle.h"
 #include "nsIURI.h"
 #include "nsXPIDLString.h"
 
-#define ALERTS_BUNDLE "chrome://alerts/locale/alert.properties"
-
 /* static */
 bool
 nsAlertsUtils::IsActionablePrincipal(nsIPrincipal* aPrincipal)
 {
   return aPrincipal &&
          !nsContentUtils::IsSystemOrExpandedPrincipal(aPrincipal) &&
          !aPrincipal->GetIsNullPrincipal();
 }
 
 /* static */
 void
-nsAlertsUtils::GetSource(nsIPrincipal* aPrincipal, nsAString& aSource)
-{
-  nsAutoString hostPort;
-  GetSourceHostPort(aPrincipal, hostPort);
-  if (hostPort.IsEmpty()) {
-    return;
-  }
-  nsCOMPtr<nsIStringBundleService> stringService(
-    mozilla::services::GetStringBundleService());
-  if (!stringService) {
-    return;
-  }
-  nsCOMPtr<nsIStringBundle> alertsBundle;
-  if (NS_WARN_IF(NS_FAILED(stringService->CreateBundle(ALERTS_BUNDLE,
-      getter_AddRefs(alertsBundle))))) {
-    return;
-  }
-  const char16_t* params[1] = { hostPort.get() };
-  nsXPIDLString result;
-  if (NS_WARN_IF(NS_FAILED(
-      alertsBundle->FormatStringFromName(MOZ_UTF16("source.label"), params, 1,
-      getter_Copies(result))))) {
-    return;
-  }
-  aSource = result;
-}
-
-/* static */
-void
 nsAlertsUtils::GetSourceHostPort(nsIPrincipal* aPrincipal,
                                  nsAString& aHostPort)
 {
   if (!IsActionablePrincipal(aPrincipal)) {
     return;
   }
   nsCOMPtr<nsIURI> principalURI;
   if (NS_WARN_IF(NS_FAILED(
--- a/toolkit/components/alerts/nsAlertsUtils.h
+++ b/toolkit/components/alerts/nsAlertsUtils.h
@@ -18,22 +18,15 @@ public:
    * Indicates whether an alert from |aPrincipal| should include the source
    * string and action buttons. Returns false if |aPrincipal| is |nullptr|, or
    * a system, expanded, or null principal.
    */
   static bool
   IsActionablePrincipal(nsIPrincipal* aPrincipal);
 
   /**
-   * Sets |aSource| to the localized notification source string, or an empty
-   * string if |aPrincipal| is not actionable.
-   */
-  static void
-  GetSource(nsIPrincipal* aPrincipal, nsAString& aSource);
-
-  /**
    * Sets |aHostPort| to the host and port from |aPrincipal|'s URI, or an
    * empty string if |aPrincipal| is not actionable.
    */
   static void
   GetSourceHostPort(nsIPrincipal* aPrincipal, nsAString& aHostPort);
 };
 #endif /* nsAlertsUtils_h */
--- a/toolkit/components/alerts/nsXULAlerts.cpp
+++ b/toolkit/components/alerts/nsXULAlerts.cpp
@@ -140,17 +140,17 @@ nsXULAlerts::ShowAlertNotification(const
   rv = argsArray->AppendElement(ifptr);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // The source contains the host and port of the site that sent the
   // notification. It is empty for system alerts.
   nsCOMPtr<nsISupportsString> scriptableAlertSource (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
   NS_ENSURE_TRUE(scriptableAlertSource, NS_ERROR_FAILURE);
   nsAutoString source;
-  nsAlertsUtils::GetSource(aPrincipal, source);
+  nsAlertsUtils::GetSourceHostPort(aPrincipal, source);
   scriptableAlertSource->SetData(source);
   rv = argsArray->AppendElement(scriptableAlertSource);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIDOMWindow> newWindow;
   nsAutoCString features("chrome,dialog=yes,titlebar=no,popup=yes");
   if (aInPrivateBrowsing) {
     features.AppendLiteral(",private");
--- a/toolkit/components/alerts/resources/content/alert.js
+++ b/toolkit/components/alerts/resources/content/alert.js
@@ -27,24 +27,38 @@ function prefillAlertInfo() {
   // arguments[2] --> the alert text
   // arguments[3] --> is the text clickable?
   // arguments[4] --> the alert cookie to be passed back to the listener
   // arguments[5] --> the alert origin reported by the look and feel
   // arguments[6] --> bidi
   // arguments[7] --> lang
   // arguments[8] --> replaced alert window (nsIDOMWindow)
   // arguments[9] --> an optional callback listener (nsIObserver)
-  // arguments[10] -> the localized alert source string
+  // arguments[10] -> the nsIURI.hostPort of the origin, optional
 
   switch (window.arguments.length) {
     default:
     case 11: {
       if (window.arguments[10]) {
-        document.getElementById('alertBox').setAttribute('hasOrigin', true);
-        document.getElementById('alertSourceLabel').setAttribute('value', window.arguments[10]);
+        let alertBox = document.getElementById("alertBox");
+        alertBox.setAttribute("hasOrigin", true);
+
+        let hostPort = window.arguments[10];
+        const ALERT_BUNDLE = Services.strings.createBundle(
+          "chrome://alerts/locale/alert.properties");
+        let label = document.getElementById("alertSourceLabel");
+        label.setAttribute("value",
+          ALERT_BUNDLE.formatStringFromName("source.label",
+                                            [hostPort],
+                                            1));
+        let disableForOrigin = document.getElementById("disableForOriginMenuItem");
+        disableForOrigin.setAttribute("label",
+          ALERT_BUNDLE.formatStringFromName("webActions.disableForOrigin.label",
+                                            [hostPort],
+                                            1));
       }
     }
     case 10:
       gAlertListener = window.arguments[9];
     case 9:
       gReplacedWindow = window.arguments[8];
     case 8:
       if (window.arguments[7]) {
@@ -212,16 +226,21 @@ function onAlertClick() {
   if (alertBox.getAttribute("animate") == "true") {
     // Closed when the animation ends.
     alertBox.setAttribute("clicked", "true");
   } else {
     window.close();
   }
 }
 
+function disableForOrigin() {
+  gAlertListener.observe(null, "alertdisablecallback", gAlertCookie);
+  onAlertClose();
+}
+
 function onAlertClose() {
   let alertBox = document.getElementById("alertBox");
   if (alertBox.getAttribute("animate") == "true") {
     // Closed when the animation ends.
     alertBox.setAttribute("closing", "true");
   } else {
     window.close();
   }
--- a/toolkit/components/alerts/resources/content/alert.xul
+++ b/toolkit/components/alerts/resources/content/alert.xul
@@ -38,17 +38,23 @@
         <image id="alertImage"/>
       </hbox>
 
       <vbox id="alertTextBox" class="alertTextBox">
         <label id="alertTextLabel" class="alertText plain"/>
         <spacer flex="1"/>
         <box id="alertFooter">
           <label id="alertSourceLabel" class="alertSource plain"/>
-          <button type="menu" id="alertSettings" tooltiptext="&settings.label;"/>
+          <button type="menu" id="alertSettings" tooltiptext="&settings.label;"
+                  onclick="event.stopPropagation();">
+            <menupopup position="after_end">
+              <menuitem id="disableForOriginMenuItem"
+                        oncommand="disableForOrigin();"/>
+            </menupopup>
+          </button>
         </box>
       </vbox>
     </box>
   </vbox>
 
   <!-- This method is called inline because we want to make sure we establish the width
        and height of the alert before we fire the onload handler. -->
   <script type="application/javascript">prefillAlertInfo();</script>
--- a/toolkit/locales/en-US/chrome/alerts/alert.properties
+++ b/toolkit/locales/en-US/chrome/alerts/alert.properties
@@ -3,15 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 # LOCALIZATION NOTE(closeButton.title): Used as the close button text for web notifications on OS X.
 # This should ideally match the string that OS X uses for the close button on alert-type
 # notifications. OS X will truncate the value if it's too long.
 closeButton.title = Close
 # LOCALIZATION NOTE(actionButton.label): Used as the button label to provide more actions on OS X notifications. OS X will truncate this if it's too long.
 actionButton.label = …
-webActions.disable.label = Disable notifications from this site
+# LOCALIZATION NOTE(webActions.disableForOrigin.label): %S is replaced
+# with the hostname origin of the notification.
+webActions.disableForOrigin.label = Disable notifications from %S
 
 # LOCALIZATION NOTE(source.label): Used to show the URL of the site that
 # sent the notification (e.g., "via mozilla.org"). "%1$S" is the source host
 # and port.
 source.label=via %1$S
 webActions.settings.label = Notification settings
--- a/toolkit/themes/shared/alert-common.css
+++ b/toolkit/themes/shared/alert-common.css
@@ -60,25 +60,24 @@ label {
 }
 
 #alertSettings {
   -moz-appearance: none;
   background-color: transparent;
   border-width: 0;
   min-width: 0;
   list-style-image: url("chrome://mozapps/skin/extensions/utilities.svg#utilities");
-  visibility: hidden; /* Temporary until bug 1209602 or bug 1205172 is fixed. */
 }
 
 #alertSettings:hover {
   background-color: rgba(107,107,107,.2);
   border-radius: 20px;
 }
 
-#alertSettings["open"],
+#alertSettings[open],
 #alertSettings:hover:active {
   background-color: rgba(107,107,107,.4);
 }
 
 #alertSettings > .button-box > .button-menu-dropmarker,
 #alertSettings > .button-box > .box-inherit > .button-text {
   display: none;
 }
--- a/widget/cocoa/OSXNotificationCenter.mm
+++ b/widget/cocoa/OSXNotificationCenter.mm
@@ -240,37 +240,49 @@ OSXNotificationCenter::ShowAlertNotifica
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
   Class unClass = NSClassFromString(@"NSUserNotification");
   id<FakeNSUserNotification> notification = [[unClass alloc] init];
   notification.title = nsCocoaUtils::ToNSString(aAlertTitle);
 
   nsAutoString hostPort;
   nsAlertsUtils::GetSourceHostPort(aPrincipal, hostPort);
+  nsCOMPtr<nsIStringBundle> bundle;
+  nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+  nsresult rv = sbs->CreateBundle("chrome://alerts/locale/alert.properties", getter_AddRefs(bundle));
+
   if (!hostPort.IsEmpty()) {
-    notification.subtitle = nsCocoaUtils::ToNSString(hostPort);
+    const char16_t* formatStrings[] = { hostPort.get() };
+    nsXPIDLString notificationSource;
+    bundle->FormatStringFromName(NS_LITERAL_STRING("source.label").get(),
+                                 formatStrings,
+                                 ArrayLength(formatStrings),
+                                 getter_Copies(notificationSource));
+    notification.subtitle = nsCocoaUtils::ToNSString(notificationSource);
   }
 
   notification.informativeText = nsCocoaUtils::ToNSString(aAlertText);
   notification.soundName = NSUserNotificationDefaultSoundName;
   notification.hasActionButton = NO;
 
   // If this is not an application/extension alert, show additional actions dealing with permissions.
   if (nsAlertsUtils::IsActionablePrincipal(aPrincipal)) {
-    nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
-    nsCOMPtr<nsIStringBundle> bundle;
-    nsresult rv = sbs->CreateBundle("chrome://alerts/locale/alert.properties", getter_AddRefs(bundle));
     if (NS_SUCCEEDED(rv)) {
       nsXPIDLString closeButtonTitle, actionButtonTitle, disableButtonTitle, settingsButtonTitle;
       bundle->GetStringFromName(NS_LITERAL_STRING("closeButton.title").get(),
                                 getter_Copies(closeButtonTitle));
       bundle->GetStringFromName(NS_LITERAL_STRING("actionButton.label").get(),
                                 getter_Copies(actionButtonTitle));
-      bundle->GetStringFromName(NS_LITERAL_STRING("webActions.disable.label").get(),
-                                getter_Copies(disableButtonTitle));
+      if (!hostPort.IsEmpty()) {
+        const char16_t* formatStrings[] = { hostPort.get() };
+        bundle->FormatStringFromName(NS_LITERAL_STRING("webActions.disableForOrigin.label").get(),
+                                     formatStrings,
+                                     ArrayLength(formatStrings),
+                                     getter_Copies(disableButtonTitle));
+      }
       bundle->GetStringFromName(NS_LITERAL_STRING("webActions.settings.label").get(),
                                 getter_Copies(settingsButtonTitle));
 
       notification.hasActionButton = YES;
       notification.otherButtonTitle = nsCocoaUtils::ToNSString(closeButtonTitle);
       notification.actionButtonTitle = nsCocoaUtils::ToNSString(actionButtonTitle);
       [(NSObject*)notification setValue:@(YES) forKey:@"_showsButtons"];
       [(NSObject*)notification setValue:@(YES) forKey:@"_alwaysShowAlternateActionMenu"];