Bug 874050, Make Nofification's click event cancelable, r=wchen
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Wed, 15 Oct 2014 17:48:04 +0300
changeset 210606 71c4f958c4aae2113a8d5ead8fb002ed3f7a15ce
parent 210557 a280a03c9f3cc6207cd17a7c76081e4e7ffd4eea
child 210607 cbb710fac8064c79936b297889be4c3788ee00bd
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerswchen
bugs874050
milestone36.0a1
Bug 874050, Make Nofification's click event cancelable, r=wchen
browser/base/content/test/general/browser_notification_tab_switching.js
browser/base/content/test/general/file_dom_notifications.html
dom/events/Event.cpp
dom/events/Event.h
dom/events/EventListenerManager.cpp
dom/notification/Notification.cpp
--- a/browser/base/content/test/general/browser_notification_tab_switching.js
+++ b/browser/base/content/test/general/browser_notification_tab_switching.js
@@ -1,58 +1,95 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- */
-
-"use strict";
-
-let tab;
-let notification;
-let notificationURL = "http://example.org/browser/browser/base/content/test/general/file_dom_notifications.html";
-
-function test () {
-  waitForExplicitFinish();
-
-  let pm = Services.perms;
-  registerCleanupFunction(function() {
-    pm.remove(notificationURL, "desktop-notification");
-    gBrowser.removeTab(tab);
-    window.restore();
-  });
-
-  pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION);
-
-  tab = gBrowser.addTab(notificationURL);
-  tab.linkedBrowser.addEventListener("load", onLoad, true);
-}
-
-function onLoad() {
-  isnot(gBrowser.selectedTab, tab, "Notification page loaded as a background tab");
-  tab.linkedBrowser.removeEventListener("load", onLoad, true);
-  let win = tab.linkedBrowser.contentWindow.wrappedJSObject;
-  notification = win.showNotification();
-  notification.addEventListener("show", onAlertShowing);
-}
-
-function onAlertShowing() {
-  info("Notification alert showing");
-  notification.removeEventListener("show", onAlertShowing);
-
-  let alertWindow = findChromeWindowByURI("chrome://global/content/alerts/alert.xul");
-  if (!alertWindow) {
-    todo(false, "Notifications don't use XUL windows on all platforms.");
-    notification.close();
-    finish();
-    return;
-  }
-  gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect);
-  EventUtils.synthesizeMouseAtCenter(alertWindow.document.getElementById("alertTitleLabel"), {}, alertWindow);
-  info("Clicked on notification");
-  alertWindow.close();
-}
-
-function onTabSelect() {
-  gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect);
-  is(gBrowser.selectedTab.linkedBrowser.contentWindow.location.href, notificationURL,
-     "Notification tab should be selected.");
-
-  finish();
-}
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+let tab;
+let notification;
+let notificationURL = "http://example.org/browser/browser/base/content/test/general/file_dom_notifications.html";
+let newWindowOpenedFromTab;
+
+function test () {
+  waitForExplicitFinish();
+
+  let pm = Services.perms;
+  registerCleanupFunction(function() {
+    pm.remove(notificationURL, "desktop-notification");
+    gBrowser.removeTab(tab);
+    window.restore();
+  });
+
+  pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION);
+
+  tab = gBrowser.addTab(notificationURL);
+  tab.linkedBrowser.addEventListener("load", onLoad, true);
+}
+
+function onLoad() {
+  isnot(gBrowser.selectedTab, tab, "Notification page loaded as a background tab");
+  tab.linkedBrowser.removeEventListener("load", onLoad, true);
+  let win = tab.linkedBrowser.contentWindow.wrappedJSObject;
+  win.newWindow = win.open("about:blank", "", "height=100,width=100");
+  newWindowOpenedFromTab = win.newWindow;
+  win.newWindow.addEventListener("load", function() {
+    info("new window loaded");
+    win.newWindow.addEventListener("blur", function b() {
+      info("new window got blur");
+      win.newWindow.removeEventListener("blur", b);
+      notification = win.showNotification1();
+      win.newWindow.addEventListener("focus", onNewWindowFocused);
+      notification.addEventListener("show", onAlertShowing);
+    });
+
+    function waitUntilNewWindowHasFocus() {
+      if (!win.newWindow.document.hasFocus()) {
+        setTimeout(waitUntilNewWindowHasFocus, 50);
+      } else {
+        // Focus another window so that new window gets blur event.
+        gBrowser.selectedTab.linkedBrowser.contentWindow.focus();
+      }
+    }
+    win.newWindow.focus();
+    waitUntilNewWindowHasFocus();
+  });
+}
+
+function onAlertShowing() {
+  info("Notification alert showing");
+  notification.removeEventListener("show", onAlertShowing);
+
+  let alertWindow = findChromeWindowByURI("chrome://global/content/alerts/alert.xul");
+  if (!alertWindow) {
+    todo(false, "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");
+  alertWindow.close();
+}
+
+function onNewWindowFocused(event) {
+  event.target.close();
+  isnot(gBrowser.selectedTab, tab, "Notification page loaded as a background tab");
+  // Using timeout to test that something do *not* happen!
+  setTimeout(openSecondNotification, 50);
+}
+
+function openSecondNotification() {
+  isnot(gBrowser.selectedTab, tab, "Notification page loaded as a background tab");
+  let win = tab.linkedBrowser.contentWindow.wrappedJSObject;
+  notification = win.showNotification2();
+  notification.addEventListener("show", onAlertShowing);
+}
+
+function onTabSelect() {
+  gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect);
+  is(gBrowser.selectedTab.linkedBrowser.contentWindow.location.href, notificationURL,
+     "Notification tab should be selected.");
+
+  finish();
+}
--- a/browser/base/content/test/general/file_dom_notifications.html
+++ b/browser/base/content/test/general/file_dom_notifications.html
@@ -1,23 +1,40 @@
-<html>
-<head>
-<script>
-"use strict";
-
-function showNotification() {
-  var options = {
-      dir: undefined,
-      lang: undefined,
-      body: "Test body",
-      tag: "Test tag",
-      icon: undefined,
-  };
-  return new Notification("Test title", options);
-}
-</script>
-</head>
-<body>
-<form id="notificationForm" onsubmit="showNotification();">
-  <input type="submit" value="Show notification" id="submit"/>
-</form>
-</body>
-</html>
+<html>
+<head>
+<script>
+"use strict";
+
+function showNotification1() {
+  var options = {
+      dir: undefined,
+      lang: undefined,
+      body: "Test body",
+      tag: "Test tag",
+      icon: undefined,
+  };
+  var n = new Notification("Test title", options);
+  n.addEventListener("click", function(event) {
+    event.preventDefault();
+    dump("Should focus new window.");
+    newWindow.focus();
+  });
+  return n;
+}
+
+function showNotification2() {
+  var options = {
+      dir: undefined,
+      lang: undefined,
+      body: "Test body",
+      tag: "Test tag",
+      icon: undefined,
+  };
+  return new Notification("Test title", options);
+}
+</script>
+</head>
+<body>
+<form id="notificationForm" onsubmit="showNotification();">
+  <input type="submit" value="Show notification" id="submit"/>
+</form>
+</body>
+</html>
--- a/dom/events/Event.cpp
+++ b/dom/events/Event.cpp
@@ -71,16 +71,17 @@ Event::ConstructorInit(EventTarget* aOwn
   if (mIsMainThreadEvent && !sReturnHighResTimeStampIsSet) {
     Preferences::AddBoolVarCache(&sReturnHighResTimeStamp,
                                  "dom.event.highrestimestamp.enabled",
                                  sReturnHighResTimeStamp);
     sReturnHighResTimeStampIsSet = true;
   }
 
   mPrivateDataDuplicated = false;
+  mWantsPopupControlCheck = false;
 
   if (aEvent) {
     mEvent = aEvent;
     mEventIsInternal = false;
   }
   else {
     mEventIsInternal = true;
     /*
@@ -650,22 +651,30 @@ PopupAllowedForEvent(const char *eventNa
     startiter = enditer;
   }
 
   return false;
 }
 
 // static
 PopupControlState
-Event::GetEventPopupControlState(WidgetEvent* aEvent)
+Event::GetEventPopupControlState(WidgetEvent* aEvent, nsIDOMEvent* aDOMEvent)
 {
   // generally if an event handler is running, new windows are disallowed.
   // check for exceptions:
   PopupControlState abuse = openAbused;
 
+  if (aDOMEvent && aDOMEvent->InternalDOMEvent()->GetWantsPopupControlCheck()) {
+    nsAutoString type;
+    aDOMEvent->GetType(type);
+    if (PopupAllowedForEvent(NS_ConvertUTF16toUTF8(type).get())) {
+      return openAllowed;
+    }
+  }
+
   switch(aEvent->mClass) {
   case eBasicEventClass:
     // For these following events only allow popups if they're
     // triggered while handling user input. See
     // nsPresShell::HandleEventInternal() for details.
     if (EventStateManager::IsHandlingUserInput()) {
       switch(aEvent->message) {
       case NS_FORM_SELECTED :
--- a/dom/events/Event.h
+++ b/dom/events/Event.h
@@ -25,16 +25,17 @@ class nsIDOMEventTarget;
 class nsPresContext;
 
 namespace mozilla {
 namespace dom {
 
 class EventTarget;
 class ErrorEvent;
 class ProgressEvent;
+class WantsPopupControlCheck;
 
 // Dummy class so we can cast through it to get from nsISupports to
 // Event subclasses with only two non-ambiguous static casts.
 class EventBase : public nsIDOMEvent
 {
 };
 
 class Event : public EventBase,
@@ -108,17 +109,18 @@ public:
   // nsIDOMEvent Interface
   NS_DECL_NSIDOMEVENT
 
   void InitPresContextData(nsPresContext* aPresContext);
 
   // Returns true if the event should be trusted.
   bool Init(EventTarget* aGlobal);
 
-  static PopupControlState GetEventPopupControlState(WidgetEvent* aEvent);
+  static PopupControlState GetEventPopupControlState(WidgetEvent* aEvent,
+                                                     nsIDOMEvent* aDOMEvent = nullptr);
 
   static void PopupAllowedEventsChanged();
 
   static void Shutdown();
 
   static const char* GetEventName(uint32_t aEventType);
   static CSSIntPoint GetClientCoords(nsPresContext* aPresContext,
                                      WidgetEvent* aEvent,
@@ -230,29 +232,62 @@ public:
                                             nsIContent* aRelatedTarget);
 
 protected:
 
   // Internal helper functions
   void SetEventType(const nsAString& aEventTypeArg);
   already_AddRefed<nsIContent> GetTargetFromFrame();
 
+  friend class WantsPopupControlCheck;
+  void SetWantsPopupControlCheck(bool aCheck)
+  {
+    mWantsPopupControlCheck = aCheck;
+  }
+
+  bool GetWantsPopupControlCheck()
+  {
+    return IsTrusted() && mWantsPopupControlCheck;
+  }
+
   /**
    * IsChrome() returns true if aCx is chrome context or the event is created
    * in chrome's thread.  Otherwise, false.
    */
   bool IsChrome(JSContext* aCx) const;
 
   mozilla::WidgetEvent*       mEvent;
   nsRefPtr<nsPresContext>     mPresContext;
   nsCOMPtr<EventTarget>       mExplicitOriginalTarget;
   nsCOMPtr<nsPIDOMWindow>     mOwner; // nsPIDOMWindow for now.
   bool                        mEventIsInternal;
   bool                        mPrivateDataDuplicated;
   bool                        mIsMainThreadEvent;
+  // True when popup control check should rely on event.type, not
+  // WidgetEvent.message.
+  bool                        mWantsPopupControlCheck;
+};
+
+class MOZ_STACK_CLASS WantsPopupControlCheck
+{
+public:
+  WantsPopupControlCheck(nsIDOMEvent* aEvent) : mEvent(aEvent->InternalDOMEvent())
+  {
+    mOriginalWantsPopupControlCheck = mEvent->GetWantsPopupControlCheck();
+    mEvent->SetWantsPopupControlCheck(mEvent->IsTrusted());
+  }
+
+  ~WantsPopupControlCheck()
+  {
+    mEvent->SetWantsPopupControlCheck(mOriginalWantsPopupControlCheck);
+  }
+
+private:
+  Event* mEvent;
+  bool mOriginalWantsPopupControlCheck;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #define NS_FORWARD_TO_EVENT \
   NS_FORWARD_NSIDOMEVENT(Event::)
 
--- a/dom/events/EventListenerManager.cpp
+++ b/dom/events/EventListenerManager.cpp
@@ -971,17 +971,17 @@ EventListenerManager::HandleEventInterna
   //Set the value of the internal PreventDefault flag properly based on aEventStatus
   if (*aEventStatus == nsEventStatus_eConsumeNoDefault) {
     aEvent->mFlags.mDefaultPrevented = true;
   }
 
   nsAutoTObserverArray<Listener, 2>::EndLimitedIterator iter(mListeners);
   Maybe<nsAutoPopupStatePusher> popupStatePusher;
   if (mIsMainThreadELM) {
-    popupStatePusher.emplace(Event::GetEventPopupControlState(aEvent));
+    popupStatePusher.emplace(Event::GetEventPopupControlState(aEvent, *aDOMEvent));
   }
 
   bool hasListener = false;
   while (iter.HasMore()) {
     if (aEvent->mFlags.mImmediatePropagationStopped) {
       break;
     }
     Listener* listener = &iter.GetNext();
--- a/dom/notification/Notification.cpp
+++ b/dom/notification/Notification.cpp
@@ -19,16 +19,17 @@
 #include "nsServiceManagerUtils.h"
 #include "nsStructuredCloneContainer.h"
 #include "nsToolkitCompsCID.h"
 #include "nsGlobalWindow.h"
 #include "nsDOMJSUtils.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsIXPConnect.h"
 #include "mozilla/dom/PermissionMessageUtils.h"
+#include "mozilla/dom/Event.h"
 #include "mozilla/Services.h"
 #include "nsContentPermissionHelper.h"
 #ifdef MOZ_B2G
 #include "nsIDOMDesktopNotification.h"
 #endif
 
 namespace mozilla {
 namespace dom {
@@ -355,29 +356,41 @@ NotificationTask::Run()
 
 NS_IMPL_ISUPPORTS(NotificationObserver, nsIObserver)
 
 NS_IMETHODIMP
 NotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
                               const char16_t* aData)
 {
   nsCOMPtr<nsPIDOMWindow> window = mNotification->GetOwner();
-  if (!window) {
+  if (!window || !window->IsCurrentInnerWindow()) {
     // Window has been closed, this observer is not valid anymore
     return NS_ERROR_FAILURE;
   }
 
   if (!strcmp("alertclickcallback", aTopic)) {
-    nsIDocument* doc = window ? window->GetExtantDoc() : nullptr;
-    if (doc) {
-      nsContentUtils::DispatchChromeEvent(doc, window,
-                                          NS_LITERAL_STRING("DOMWebNotificationClicked"),
-                                          true, true);
+
+    nsCOMPtr<nsIDOMEvent> event;
+    NS_NewDOMEvent(getter_AddRefs(event), mNotification, nullptr, nullptr);
+    nsresult rv = event->InitEvent(NS_LITERAL_STRING("click"), false, true);
+    NS_ENSURE_SUCCESS(rv, rv);
+    event->SetTrusted(true);
+    WantsPopupControlCheck popupControlCheck(event);
+    bool doDefaultAction = true;
+    mNotification->DispatchEvent(event, &doDefaultAction);
+    if (doDefaultAction) {
+      nsIDocument* doc = window ? window->GetExtantDoc() : nullptr;
+      if (doc) {
+        // Browser UI may use DOMWebNotificationClicked to focus the tab
+        // from which the event was dispatched.
+        nsContentUtils::DispatchChromeEvent(doc, window->GetOuterWindow(),
+                                            NS_LITERAL_STRING("DOMWebNotificationClicked"),
+                                            true, true);
+      }
     }
-    mNotification->DispatchTrustedEvent(NS_LITERAL_STRING("click"));
   } else if (!strcmp("alertfinished", aTopic)) {
     nsCOMPtr<nsINotificationStorage> notificationStorage =
       do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID);
     if (notificationStorage && mNotification->IsStored()) {
       nsString origin;
       nsresult rv = Notification::GetOrigin(mNotification->GetOwner(), origin);
       if (NS_SUCCEEDED(rv)) {
         nsString id;