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 303717 1a8291d0b02662a9c2bbc82c8c43a2ee0bc548b0
parent 303716 399f9492ad3c3d7f9ffc4859dfeeeacb5b555ceb
child 303718 caaa8c13b98f458d1ea7424881f92196bfd99aac
push id1001
push userraliiev@mozilla.com
push dateMon, 18 Jan 2016 19:06:03 +0000
treeherdermozilla-release@8b89261f3ac4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1209602
milestone44.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 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"];