Bug 1114554 - Patch 3 - Fire notificationclick event on ServiceWorkerGlobalScope. r=wchen,baku
☠☠ backed out by 531f100d2bd8 ☠ ☠
authorNikhil Marathe <nsm.nikhil@gmail.com>
Thu, 25 Jun 2015 15:01:01 -0700
changeset 281048 75324b6862a8545eeeb1e1fddff80f93dc1c5754
parent 281047 ce8a768782f234b8d70c30164c687024c7905dfb
child 281049 f92abe5ec78447a2bcd9c06658dae0df914f68a5
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswchen, baku
bugs1114554, 1162088
milestone41.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 1114554 - Patch 3 - Fire notificationclick event on ServiceWorkerGlobalScope. r=wchen,baku Bug 1114554 - Patch 3.1 - ServiceWorker principal fixes. r=baku Bug 1162088 introduced origin attributes that ServiceWorkerManager callers have to use. This patch updates notificationclick events to work. Folded: Hide NotificationEvent behind pref
dom/interfaces/base/nsIServiceWorkerManager.idl
dom/interfaces/notification/nsINotificationStorage.idl
dom/notification/Notification.cpp
dom/notification/Notification.h
dom/notification/NotificationEvent.cpp
dom/notification/NotificationEvent.h
dom/notification/NotificationStorage.js
dom/notification/moz.build
dom/tests/mochitest/notification/MockServices.js
dom/webidl/NotificationEvent.webidl
dom/webidl/moz.build
dom/workers/ServiceWorkerManager.cpp
dom/workers/test/serviceworkers/mochitest.ini
dom/workers/test/serviceworkers/notificationclick.html
dom/workers/test/serviceworkers/notificationclick.js
dom/workers/test/serviceworkers/test_notificationclick.html
dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
dom/workers/test/test_notification.html
modules/libpref/init/all.js
--- a/dom/interfaces/base/nsIServiceWorkerManager.idl
+++ b/dom/interfaces/base/nsIServiceWorkerManager.idl
@@ -28,17 +28,17 @@ interface nsIServiceWorkerInfo : nsISupp
   readonly attribute DOMString scope;
   readonly attribute DOMString scriptSpec;
   readonly attribute DOMString currentWorkerURL;
 
   readonly attribute DOMString activeCacheName;
   readonly attribute DOMString waitingCacheName;
 };
 
-[scriptable, builtinclass, uuid(e9abb123-0099-4d9e-85db-c8cd0aff19e6)]
+[scriptable, builtinclass, uuid(ed1cbbf2-0400-4caa-8eb2-b09d21a94e20)]
 interface nsIServiceWorkerManager : nsISupports
 {
   /**
    * Registers a ServiceWorker with script loaded from `aScriptURI` to act as
    * the ServiceWorker for aScope.  Requires a valid entry settings object on
    * the stack. This means you must call this from content code 'within'
    * a window.
    *
@@ -121,16 +121,27 @@ interface nsIServiceWorkerManager : nsIS
 
   // Note: This is meant to be used only by about:serviceworkers.
   // It calls unregister() in each child process. The callback is used to
   // inform when unregister() is completed on the current process.
   void propagateUnregister(in nsIPrincipal aPrincipal,
                            in nsIServiceWorkerUnregisterCallback aCallback,
                            in DOMString aScope);
 
+  void sendNotificationClickEvent(in ACString aOriginSuffix,
+                                  in ACString scope,
+                                  in AString aID,
+                                  in AString aTitle,
+                                  in AString aDir,
+                                  in AString aLang,
+                                  in AString aBody,
+                                  in AString aTag,
+                                  in AString aIcon,
+                                  in AString aData,
+                                  in AString aBehavior);
   void sendPushEvent(in ACString aOriginAttributes,
                      in ACString aScope,
                      in DOMString aData);
   void sendPushSubscriptionChangeEvent(in ACString aOriginAttributes,
                                        in ACString scope);
 
   void updateAllRegistrations();
 };
--- a/dom/interfaces/notification/nsINotificationStorage.idl
+++ b/dom/interfaces/notification/nsINotificationStorage.idl
@@ -36,17 +36,17 @@ interface nsINotificationStorageCallback
    */
   [implicit_jscontext]
   void done();
 };
 
 /**
  * Interface for notification persistence layer.
  */
-[scriptable, uuid(cac01fb0-c2eb-4252-b2f4-5b1fac933bd4)]
+[scriptable, uuid(2f8f84b7-70b5-4673-98d8-fd3f9f8e0e5c)]
 interface nsINotificationStorage : nsISupports
 {
 
   /**
    * Add/replace a notification to the persistence layer.
    *
    * @param origin: the origin/app of this notification
    * @param id: a uuid for this notification
@@ -82,16 +82,30 @@ interface nsINotificationStorage : nsISu
    * @param callback: nsINotificationStorageCallback, used for
    *                  returning notifications objects
    */
   void get(in DOMString origin,
            in DOMString tag,
            in nsINotificationStorageCallback aCallback);
 
   /**
+   * Retrieve a notification by ID.
+   *
+   * @param origin: the origin/app for which to fetch notifications.
+   * @param id: the id of the notification.
+   * @param callback: nsINotificationStorageCallback whose Handle method will
+   * be called *at most once* if the notification with that ID is found. Not
+   * called if that ID is not found. Done() will be called right after
+   * Handle().
+   */
+  void getByID(in DOMString origin,
+               in DOMString id,
+               in nsINotificationStorageCallback aCallback);
+
+  /**
    * Remove a notification from storage.
    *
    * @param origin: the origin/app to delete the notification from
    * @param id: the uuid for the notification to delete
    */
   void delete(in DOMString origin,
               in DOMString id);
 
--- a/dom/notification/Notification.cpp
+++ b/dom/notification/Notification.cpp
@@ -14,38 +14,41 @@
 #include "mozilla/unused.h"
 #include "nsContentUtils.h"
 #include "nsIAlertsService.h"
 #include "nsIAppsService.h"
 #include "nsIContentPermissionPrompt.h"
 #include "nsIDocument.h"
 #include "nsINotificationStorage.h"
 #include "nsIPermissionManager.h"
+#include "nsIServiceWorkerManager.h"
 #include "nsIUUIDGenerator.h"
 #include "nsServiceManagerUtils.h"
 #include "nsStructuredCloneContainer.h"
 #include "nsToolkitCompsCID.h"
 #include "nsGlobalWindow.h"
 #include "nsDOMJSUtils.h"
 #include "nsProxyRelease.h"
 #include "nsNetUtil.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsIXPConnect.h"
+#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h"
+#include "mozilla/dom/NotificationEvent.h"
 #include "mozilla/dom/PermissionMessageUtils.h"
-#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h"
 #include "mozilla/Services.h"
 #include "nsContentPermissionHelper.h"
 #include "nsILoadContext.h"
 #ifdef MOZ_B2G
 #include "nsIDOMDesktopNotification.h"
 #endif
 
 #include "ServiceWorkerManager.h"
 #include "WorkerPrivate.h"
 #include "WorkerRunnable.h"
+#include "WorkerScope.h"
 
 namespace mozilla {
 namespace dom {
 
 using namespace workers;
 
 class NotificationStorageCallback final : public nsINotificationStorageCallback
 {
@@ -730,16 +733,58 @@ Notification::Constructor(const GlobalOb
     return nullptr;
   }
 
   // This is be ok since we are on the worker thread where this function will
   // run to completion before the Notification has a chance to go away.
   return notification.forget();
 }
 
+// static
+already_AddRefed<Notification>
+Notification::ConstructFromFields(
+    nsIGlobalObject* aGlobal,
+    const nsAString& aID,
+    const nsAString& aTitle,
+    const nsAString& aDir,
+    const nsAString& aLang,
+    const nsAString& aBody,
+    const nsAString& aTag,
+    const nsAString& aIcon,
+    const nsAString& aData,
+    ErrorResult& aRv)
+{
+  MOZ_ASSERT(aGlobal);
+
+  AutoJSAPI jsapi;
+  DebugOnly<bool> ok = jsapi.Init(aGlobal);
+  MOZ_ASSERT(ok);
+
+  RootedDictionary<NotificationOptions> options(jsapi.cx());
+  options.mDir = Notification::StringToDirection(nsString(aDir));
+  options.mLang = aLang;
+  options.mBody = aBody;
+  options.mTag = aTag;
+  options.mIcon = aIcon;
+  nsRefPtr<Notification> notification;
+  notification = Notification::CreateInternal(aID,
+                                              aTitle,
+                                              options);
+
+  if (NS_IsMainThread()) {
+    notification->BindToOwner(aGlobal);
+  }
+
+  notification->InitFromBase64(jsapi.cx(), aData, aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+
+  return notification.forget();
+}
 
 nsresult
 Notification::PersistNotification()
 {
   AssertIsOnMainThread();
   nsresult rv;
   nsCOMPtr<nsINotificationStorage> notificationStorage =
     do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv);
@@ -941,16 +986,73 @@ protected:
     AutoJSAPI jsapi;
     jsapi.Init();
     r->Dispatch(jsapi.cx());
   }
 };
 
 NS_IMPL_ISUPPORTS_INHERITED0(WorkerNotificationObserver, NotificationObserver)
 
+class ServiceWorkerNotificationObserver final : public nsIObserver
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIOBSERVER
+
+  ServiceWorkerNotificationObserver(const nsAString& aScope,
+                                    nsIPrincipal* aPrincipal,
+                                    const nsAString& aID)
+    : mScope(aScope), mID(aID), mPrincipal(aPrincipal)
+  {
+    AssertIsOnMainThread();
+    MOZ_ASSERT(aPrincipal);
+  }
+
+private:
+  ~ServiceWorkerNotificationObserver()
+  {}
+
+  const nsString mScope;
+  const nsString mID;
+  nsCOMPtr<nsIPrincipal> mPrincipal;
+};
+
+NS_IMPL_ISUPPORTS(ServiceWorkerNotificationObserver, nsIObserver)
+
+// For ServiceWorkers.
+bool
+Notification::DispatchNotificationClickEvent()
+{
+  MOZ_ASSERT(mWorkerPrivate);
+  MOZ_ASSERT(mWorkerPrivate->IsServiceWorker());
+  mWorkerPrivate->AssertIsOnWorkerThread();
+
+  NotificationEventInit options;
+  options.mNotification = this;
+
+  ErrorResult result;
+  nsRefPtr<EventTarget> target = mWorkerPrivate->GlobalScope();
+  nsRefPtr<NotificationEvent> event =
+    NotificationEvent::Constructor(target,
+                                   NS_LITERAL_STRING("notificationclick"),
+                                   options,
+                                   result);
+  if (NS_WARN_IF(result.Failed())) {
+    return false;
+  }
+
+  event->SetTrusted(true);
+  WantsPopupControlCheck popupControlCheck(event);
+  target->DispatchDOMEvent(nullptr, event, nullptr, nullptr);
+  // We always return false since in case of dispatching on the serviceworker,
+  // there is no well defined window to focus. The script may use the
+  // Client.focus() API if it wishes.
+  return false;
+}
+
 bool
 Notification::DispatchClickEvent()
 {
   AssertIsOnTargetThread();
   nsCOMPtr<nsIDOMEvent> event;
   NS_NewDOMEvent(getter_AddRefs(event), this, nullptr, nullptr);
   nsresult rv = event->InitEvent(NS_LITERAL_STRING("click"), false, true);
   NS_ENSURE_SUCCESS(rv, false);
@@ -970,22 +1072,25 @@ class NotificationClickWorkerRunnable fi
   // preventDefault()ed.
   nsMainThreadPtrHandle<nsPIDOMWindow> mWindow;
 public:
   NotificationClickWorkerRunnable(Notification* aNotification,
                                   const nsMainThreadPtrHandle<nsPIDOMWindow>& aWindow)
     : NotificationWorkerRunnable(aNotification->mWorkerPrivate)
     , mNotification(aNotification)
     , mWindow(aWindow)
-  {}
+  {
+    MOZ_ASSERT_IF(mWorkerPrivate->IsServiceWorker(), !mWindow);
+  }
 
   void
   WorkerRunInternal() override
   {
     bool doDefaultAction = mNotification->DispatchClickEvent();
+    MOZ_ASSERT_IF(mWorkerPrivate->IsServiceWorker(), !doDefaultAction);
     if (doDefaultAction) {
       nsRefPtr<FocusWindowRunnable> r = new FocusWindowRunnable(mWindow);
       NS_DispatchToMainThread(r);
     }
   }
 };
 
 NS_IMETHODIMP
@@ -1037,25 +1142,28 @@ WorkerNotificationObserver::Observe(nsIS
   if (NS_WARN_IF(!notification)) {
     return NS_ERROR_FAILURE;
   }
 
   MOZ_ASSERT(notification->mWorkerPrivate);
 
   nsRefPtr<WorkerRunnable> r;
   if (!strcmp("alertclickcallback", aTopic)) {
-    WorkerPrivate* top = notification->mWorkerPrivate;
-    while (top->GetParent()) {
-      top = top->GetParent();
-    }
+    nsPIDOMWindow* window = nullptr;
+    if (!notification->mWorkerPrivate->IsServiceWorker()) {
+      WorkerPrivate* top = notification->mWorkerPrivate;
+      while (top->GetParent()) {
+        top = top->GetParent();
+      }
 
-    nsPIDOMWindow* window = top->GetWindow();
-    if (NS_WARN_IF(!window || !window->IsCurrentInnerWindow())) {
-      // Window has been closed, this observer is not valid anymore
-      return NS_ERROR_FAILURE;
+      window = top->GetWindow();
+      if (NS_WARN_IF(!window || !window->IsCurrentInnerWindow())) {
+        // Window has been closed, this observer is not valid anymore
+        return NS_ERROR_FAILURE;
+      }
     }
 
     // Instead of bothering with adding features and other worker lifecycle
     // management, we simply hold strongrefs to the window and document.
     nsMainThreadPtrHandle<nsPIDOMWindow> windowHandle(
       new nsMainThreadPtrHolder<nsPIDOMWindow>(window));
 
     r = new NotificationClickWorkerRunnable(notification, windowHandle);
@@ -1072,16 +1180,117 @@ WorkerNotificationObserver::Observe(nsIS
   MOZ_ASSERT(r);
   AutoSafeJSContext cx;
   if (!r->Dispatch(cx)) {
     NS_WARNING("Could not dispatch event to worker notification");
   }
   return NS_OK;
 }
 
+class NotificationClickEventCallback final : public nsINotificationStorageCallback
+{
+public:
+  NS_DECL_ISUPPORTS
+
+  NotificationClickEventCallback(nsIPrincipal* aPrincipal,
+                                 const nsAString& aScope)
+  : mPrincipal(aPrincipal), mScope(aScope)
+  {
+    MOZ_ASSERT(aPrincipal);
+  }
+
+  NS_IMETHOD Handle(const nsAString& aID,
+                    const nsAString& aTitle,
+                    const nsAString& aDir,
+                    const nsAString& aLang,
+                    const nsAString& aBody,
+                    const nsAString& aTag,
+                    const nsAString& aIcon,
+                    const nsAString& aData,
+                    const nsAString& aBehavior,
+                    JSContext* aCx) override
+  {
+    MOZ_ASSERT(!aID.IsEmpty());
+
+    AssertIsOnMainThread();
+
+    nsAutoCString originSuffix;
+    nsresult rv = mPrincipal->GetOriginSuffix(originSuffix);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    nsCOMPtr<nsIServiceWorkerManager> swm =
+      mozilla::services::GetServiceWorkerManager();
+
+    if (swm) {
+      swm->SendNotificationClickEvent(originSuffix,
+                                      NS_ConvertUTF16toUTF8(mScope),
+                                      aID,
+                                      aTitle,
+                                      aDir,
+                                      aLang,
+                                      aBody,
+                                      aTag,
+                                      aIcon,
+                                      aData,
+                                      aBehavior);
+    }
+    return NS_OK;
+  }
+
+  NS_IMETHOD Done(JSContext* aCx) override
+  {
+    return NS_OK;
+  }
+
+private:
+  ~NotificationClickEventCallback()
+  {
+  }
+
+  nsCOMPtr<nsIPrincipal> mPrincipal;
+  nsString mScope;
+};
+
+NS_IMPL_ISUPPORTS(NotificationClickEventCallback, nsINotificationStorageCallback)
+
+NS_IMETHODIMP
+ServiceWorkerNotificationObserver::Observe(nsISupports* aSubject,
+                                           const char* aTopic,
+                                           const char16_t* aData)
+{
+  AssertIsOnMainThread();
+  // Persistent notifications only care about the click event.
+  if (!strcmp("alertclickcallback", aTopic)) {
+    nsresult rv;
+    nsCOMPtr<nsINotificationStorage> notificationStorage =
+      do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    nsCOMPtr<nsINotificationStorageCallback> callback =
+      new NotificationClickEventCallback(mPrincipal, mScope);
+
+    nsAutoString origin;
+    rv = Notification::GetOrigin(mPrincipal, origin);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    rv = notificationStorage->GetByID(origin, mID, callback);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
+  return NS_OK;
+}
+
 void
 Notification::ShowInternal()
 {
   AssertIsOnMainThread();
   MOZ_ASSERT(mTempRef, "Notification should take ownership of itself before"
                        "calling ShowInternal!");
   // A notification can only have one observer and one call to ShowInternal.
   MOZ_ASSERT(!mObserver);
@@ -1121,26 +1330,37 @@ Notification::ShowInternal()
     }
     return;
   }
 
   nsAutoString iconUrl;
   nsAutoString soundUrl;
   ResolveIconAndSoundURL(iconUrl, soundUrl);
 
-  // Ownership passed to observer.
   nsCOMPtr<nsIObserver> observer;
-  if (mWorkerPrivate) {
-    // Keep a pointer so that the feature can tell the observer not to release
-    // the notification.
-    mObserver = new WorkerNotificationObserver(Move(ownership));
-    observer = mObserver;
+  if (mScope.IsEmpty()) {
+    // Ownership passed to observer.
+    if (mWorkerPrivate) {
+      // Scope better be set on ServiceWorker initiated requests.
+      MOZ_ASSERT(!mWorkerPrivate->IsServiceWorker());
+      // Keep a pointer so that the feature can tell the observer not to release
+      // the notification.
+      mObserver = new WorkerNotificationObserver(Move(ownership));
+      observer = mObserver;
+    } else {
+      observer = new NotificationObserver(Move(ownership));
+    }
   } else {
-    observer = new NotificationObserver(Move(ownership));
+    // This observer does not care about the Notification. It will be released
+    // at the end of this function.
+    //
+    // The observer is wholly owned by the alerts service.
+    observer = new ServiceWorkerNotificationObserver(mScope, GetPrincipal(), mID);
   }
+  MOZ_ASSERT(observer);
 
   // mDataObjectContainer might be uninitialized here because the notification
   // was constructed with an undefined data property.
   nsString dataStr;
   if (mDataObjectContainer) {
     mDataObjectContainer->GetDataAsBase64(dataStr);
   }
 
@@ -1800,16 +2020,18 @@ Notification::ShowPersistentNotification
   p->MaybeResolve(JS::UndefinedHandleValue);
 
   nsRefPtr<Notification> notification =
     CreateAndShow(aGlobal, aTitle, aOptions, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
+  notification->SetScope(aScope);
+
   return p.forget();
 }
 
 /* static */ already_AddRefed<Notification>
 Notification::CreateAndShow(nsIGlobalObject* aGlobal,
                             const nsAString& aTitle,
                             const NotificationOptions& aOptions,
                             ErrorResult& aRv)
--- a/dom/notification/Notification.h
+++ b/dom/notification/Notification.h
@@ -110,16 +110,17 @@ public:
  */
 class Notification : public DOMEventTargetHelper
 {
   friend class CloseNotificationRunnable;
   friend class NotificationTask;
   friend class NotificationPermissionRequest;
   friend class NotificationObserver;
   friend class NotificationStorageCallback;
+  friend class ServiceWorkerNotificationObserver;
   friend class WorkerNotificationObserver;
 
 public:
   IMPL_EVENT_HANDLER(click)
   IMPL_EVENT_HANDLER(show)
   IMPL_EVENT_HANDLER(error)
   IMPL_EVENT_HANDLER(close)
 
@@ -129,16 +130,40 @@ public:
   static bool PrefEnabled(JSContext* aCx, JSObject* aObj);
   // Returns if Notification.get() is allowed for the current global.
   static bool IsGetEnabled(JSContext* aCx, JSObject* aObj);
 
   static already_AddRefed<Notification> Constructor(const GlobalObject& aGlobal,
                                                     const nsAString& aTitle,
                                                     const NotificationOptions& aOption,
                                                     ErrorResult& aRv);
+
+  /**
+   * Used when dispatching the ServiceWorkerEvent.
+   *
+   * Does not initialize the Notification's behavior.
+   * This is because:
+   * 1) The Notification is not shown to the user and so the behavior
+   *    parameters don't matter.
+   * 2) The default binding requires main thread for parsing the JSON from the
+   *    string behavior.
+   */
+  static already_AddRefed<Notification>
+  ConstructFromFields(
+    nsIGlobalObject* aGlobal,
+    const nsAString& aID,
+    const nsAString& aTitle,
+    const nsAString& aDir,
+    const nsAString& aLang,
+    const nsAString& aBody,
+    const nsAString& aTag,
+    const nsAString& aIcon,
+    const nsAString& aData,
+    ErrorResult& aRv);
+
   void GetID(nsAString& aRetval) {
     aRetval = mID;
   }
 
   void GetTitle(nsAString& aRetval)
   {
     aRetval = mTitle;
   }
@@ -244,16 +269,17 @@ public:
 
   static NotificationPermission GetPermission(nsIGlobalObject* aGlobal,
                                               ErrorResult& aRv);
 
   static NotificationPermission GetPermissionInternal(nsIPrincipal* aPrincipal,
                                                       ErrorResult& rv);
 
   bool DispatchClickEvent();
+  bool DispatchNotificationClickEvent();
 protected:
   // Callers MUST bind the Notification to the correct DOMEventTargetHelper parent using
   // BindToOwner().
   Notification(const nsAString& aID, const nsAString& aTitle, const nsAString& aBody,
                NotificationDirection aDir, const nsAString& aLang,
                const nsAString& aTag, const nsAString& aIconUrl,
                const NotificationBehavior& aBehavior);
 
@@ -296,30 +322,43 @@ protected:
   {
     workers::AssertIsOnMainThread();
     if (mAlertName.IsEmpty()) {
       SetAlertName();
     }
     aRetval = mAlertName;
   }
 
+  void GetScope(nsAString& aScope)
+  {
+    aScope = mScope;
+  }
+
+  void
+  SetScope(const nsAString& aScope)
+  {
+    MOZ_ASSERT(mScope.IsEmpty());
+    mScope = aScope;
+  }
+
   const nsString mID;
   const nsString mTitle;
   const nsString mBody;
   const NotificationDirection mDir;
   const nsString mLang;
   const nsString mTag;
   const nsString mIconUrl;
   nsCOMPtr<nsIStructuredCloneContainer> mDataObjectContainer;
   const NotificationBehavior mBehavior;
 
   // It's null until GetData is first called
   nsCOMPtr<nsIVariant> mData;
 
   nsString mAlertName;
+  nsString mScope;
 
   // Main thread only.
   bool mIsClosed;
 
   // We need to make a distinction between the notification being closed i.e.
   // removed from any pending or active lists, and the notification being
   // removed from the database. NotificationDB might fail when trying to remove
   // the notification.
new file mode 100644
--- /dev/null
+++ b/dom/notification/NotificationEvent.cpp
@@ -0,0 +1,26 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "NotificationEvent.h"
+
+using namespace mozilla::dom;
+
+BEGIN_WORKERS_NAMESPACE
+
+NotificationEvent::NotificationEvent(EventTarget* aOwner)
+  : ExtendableEvent(aOwner)
+{
+}
+
+NS_IMPL_ADDREF_INHERITED(NotificationEvent, ExtendableEvent)
+NS_IMPL_RELEASE_INHERITED(NotificationEvent, ExtendableEvent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(NotificationEvent)
+NS_INTERFACE_MAP_END_INHERITING(ExtendableEvent)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(NotificationEvent, ExtendableEvent, mNotification)
+
+END_WORKERS_NAMESPACE
new file mode 100644
--- /dev/null
+++ b/dom/notification/NotificationEvent.h
@@ -0,0 +1,74 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_workers_notificationevent_h__
+#define mozilla_dom_workers_notificationevent_h__
+
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/NotificationEventBinding.h"
+#include "mozilla/dom/ServiceWorkerEvents.h"
+#include "mozilla/dom/workers/Workers.h"
+
+BEGIN_WORKERS_NAMESPACE
+
+class ServiceWorker;
+class ServiceWorkerClient;
+
+class NotificationEvent final : public ExtendableEvent
+{
+protected:
+  explicit NotificationEvent(EventTarget* aOwner);
+  ~NotificationEvent()
+  {}
+
+public:
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(NotificationEvent, ExtendableEvent)
+  NS_FORWARD_TO_EVENT
+
+  virtual JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return NotificationEventBinding::Wrap(aCx, this, aGivenProto);
+  }
+
+  static already_AddRefed<NotificationEvent>
+  Constructor(mozilla::dom::EventTarget* aOwner,
+              const nsAString& aType,
+              const NotificationEventInit& aOptions,
+              ErrorResult& aRv)
+  {
+    nsRefPtr<NotificationEvent> e = new NotificationEvent(aOwner);
+    bool trusted = e->Init(aOwner);
+    e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
+    e->SetTrusted(trusted);
+    e->mNotification = aOptions.mNotification;
+    e->SetWantsPopupControlCheck(e->IsTrusted());
+    return e.forget();
+  }
+
+  static already_AddRefed<NotificationEvent>
+  Constructor(const GlobalObject& aGlobal,
+              const nsAString& aType,
+              const NotificationEventInit& aOptions,
+              ErrorResult& aRv)
+  {
+    nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports());
+    return Constructor(owner, aType, aOptions, aRv);
+  }
+
+  already_AddRefed<Notification>
+  Notification_()
+  {
+    nsRefPtr<Notification> n = mNotification;
+    return n.forget();
+  }
+
+private:
+  nsRefPtr<Notification> mNotification;
+};
+
+END_WORKERS_NAMESPACE
+#endif /* mozilla_dom_workers_notificationevent_h__ */
+
--- a/dom/notification/NotificationStorage.js
+++ b/dom/notification/NotificationStorage.js
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const DEBUG = false;
+const DEBUG = true;
 function debug(s) { dump("-*- NotificationStorage.js: " + s + "\n"); }
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
@@ -80,17 +80,17 @@ NotificationStorage.prototype = {
     if (DEBUG) debug("Querying appService for: " + aOrigin);
     let rv = !!appsService.getAppByManifestURL(aOrigin);
     if (DEBUG) debug("appService returned: " + rv);
     return rv;
   },
 
   put: function(origin, id, title, dir, lang, body, tag, icon, alertName,
                 data, behavior) {
-    if (DEBUG) { debug("PUT: " + id + ": " + title); }
+    if (DEBUG) { debug("PUT: " + origin + " " + id + ": " + title); }
     var notification = {
       id: id,
       title: title,
       dir: dir,
       lang: lang,
       body: body,
       tag: tag,
       icon: icon,
@@ -129,16 +129,35 @@ NotificationStorage.prototype = {
     if (DEBUG) { debug("GET: " + origin + " " + tag); }
     if (this._cached) {
       this._fetchFromCache(origin, tag, callback);
     } else {
       this._fetchFromDB(origin, tag, callback);
     }
   },
 
+  getByID: function(origin, id, callback) {
+    if (DEBUG) { debug("GETBYID: " + origin + " " + id); }
+    var GetByIDProxyCallback = function(id, originalCallback) {
+      this.searchID = id;
+      this.originalCallback = originalCallback;
+      var self = this;
+      this.handle = function(id, title, dir, lang, body, tag, icon, data, behavior) {
+        if (id == this.searchID) {
+          self.originalCallback.handle(id, title, dir, lang, body, tag, icon, data, behavior);
+        }
+      };
+      this.done = function() {
+        self.originalCallback.done();
+      };
+    };
+
+    return this.get(origin, "", new GetByIDProxyCallback(id, callback));
+  },
+
   delete: function(origin, id) {
     if (DEBUG) { debug("DELETE: " + id); }
     var notification = this._notifications[id];
     if (notification) {
       if (notification.tag) {
         delete this._byTag[origin][notification.tag];
       }
       delete this._notifications[id];
--- a/dom/notification/moz.build
+++ b/dom/notification/moz.build
@@ -13,21 +13,23 @@ EXTRA_COMPONENTS += [
 
 EXTRA_JS_MODULES += [
     'NotificationDB.jsm'
 ]
 
 EXPORTS.mozilla.dom += [
     'DesktopNotification.h',
     'Notification.h',
+    'NotificationEvent.h',
 ]
 
 UNIFIED_SOURCES += [
     'DesktopNotification.cpp',
     'Notification.cpp',
+    'NotificationEvent.cpp',
 ]
 
 FAIL_ON_WARNINGS = True
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 LOCAL_INCLUDES += [
--- a/dom/tests/mochitest/notification/MockServices.js
+++ b/dom/tests/mochitest/notification/MockServices.js
@@ -38,19 +38,20 @@ var MockServices = (function () {
         title: title
       };
 
       // fake async alert show event
       if (listener) {
         setTimeout(function () {
           listener.observe(null, "alertshow", cookie);
         }, 100);
+        setTimeout(function () {
+          listener.observe(null, "alertclickcallback", cookie);
+        }, 100);
       }
-
-      // ?? SpecialPowers.wrap(alertListener).observe(null, "alertclickcallback", cookie);
     },
 
     showAppNotification: function(aImageUrl, aTitle, aText, aAlertListener, aDetails) {
       var listener = aAlertListener || (activeAlertNotifications[aDetails.id] ? activeAlertNotifications[aDetails.id].listener : undefined);
       activeAppNotifications[aDetails.id] = {
         observer: listener,
         title: aTitle,
         text: aText,
new file mode 100644
--- /dev/null
+++ b/dom/webidl/NotificationEvent.webidl
@@ -0,0 +1,26 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * http://notifications.spec.whatwg.org/
+ *
+ * Copyright:
+ * To the extent possible under law, the editors have waived all copyright and
+ * related or neighboring rights to this work.
+ */
+
+[Constructor(DOMString type, optional NotificationEventInit eventInitDict),
+ Exposed=ServiceWorker,Func="mozilla::dom::Notification::PrefEnabled"]
+interface NotificationEvent : ExtendableEvent {
+  readonly attribute Notification notification;
+};
+
+dictionary NotificationEventInit : ExtendableEventInit {
+  required Notification notification;
+};
+
+partial interface ServiceWorkerGlobalScope {
+  attribute EventHandler onnotificationclick;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -325,16 +325,17 @@ WEBIDL_FILES = [
     'NetDashboard.webidl',
     'NetworkInformation.webidl',
     'NetworkOptions.webidl',
     'Node.webidl',
     'NodeFilter.webidl',
     'NodeIterator.webidl',
     'NodeList.webidl',
     'Notification.webidl',
+    'NotificationEvent.webidl',
     'NotifyPaintEvent.webidl',
     'OfflineAudioCompletionEvent.webidl',
     'OfflineAudioContext.webidl',
     'OfflineResourceList.webidl',
     'OscillatorNode.webidl',
     'PaintRequest.webidl',
     'PaintRequestList.webidl',
     'PannerNode.webidl',
--- a/dom/workers/ServiceWorkerManager.cpp
+++ b/dom/workers/ServiceWorkerManager.cpp
@@ -34,16 +34,17 @@
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/DOMError.h"
 #include "mozilla/dom/ErrorEvent.h"
 #include "mozilla/dom/Headers.h"
 #include "mozilla/dom/indexedDB/IDBFactory.h"
 #include "mozilla/dom/InternalHeaders.h"
 #include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/NotificationEvent.h"
 #include "mozilla/dom/PromiseNativeHandler.h"
 #include "mozilla/dom/Request.h"
 #include "mozilla/dom/RootedDictionary.h"
 #include "mozilla/ipc/BackgroundChild.h"
 #include "mozilla/ipc/PBackgroundChild.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/unused.h"
 
@@ -2264,16 +2265,127 @@ ServiceWorkerManager::SendPushSubscripti
   if (NS_WARN_IF(!r->Dispatch(jsapi.cx()))) {
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 #endif
 }
 
+class SendNotificationClickEventRunnable final : public WorkerRunnable
+{
+  nsMainThreadPtrHandle<ServiceWorker> mServiceWorker;
+  const nsString mID;
+  const nsString mTitle;
+  const nsString mDir;
+  const nsString mLang;
+  const nsString mBody;
+  const nsString mTag;
+  const nsString mIcon;
+  const nsString mData;
+  const nsString mBehavior;
+
+public:
+  SendNotificationClickEventRunnable(
+    WorkerPrivate* aWorkerPrivate,
+    nsMainThreadPtrHandle<ServiceWorker>& aServiceWorker,
+    const nsAString& aID,
+    const nsAString& aTitle,
+    const nsAString& aDir,
+    const nsAString& aLang,
+    const nsAString& aBody,
+    const nsAString& aTag,
+    const nsAString& aIcon,
+    const nsAString& aData,
+    const nsAString& aBehavior)
+      : WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount)
+      , mServiceWorker(aServiceWorker)
+      , mID(aID)
+      , mTitle(aTitle)
+      , mDir(aDir)
+      , mLang(aLang)
+      , mBody(aBody)
+      , mTag(aTag)
+      , mIcon(aIcon)
+      , mData(aData)
+      , mBehavior(aBehavior)
+  {
+    AssertIsOnMainThread();
+    MOZ_ASSERT(aWorkerPrivate);
+    MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
+  }
+
+  bool
+  WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
+  {
+    MOZ_ASSERT(aWorkerPrivate);
+
+    nsRefPtr<EventTarget> target = do_QueryObject(aWorkerPrivate->GlobalScope());
+
+    ErrorResult result;
+    nsRefPtr<Notification> notification =
+      Notification::ConstructFromFields(aWorkerPrivate->GlobalScope(), mID, mTitle, mDir, mLang, mBody, mTag, mIcon, mData, result);
+    if (NS_WARN_IF(result.Failed())) {
+      return false;
+    }
+
+    NotificationEventInit nei;
+    nei.mNotification = notification;
+    nei.mBubbles = false;
+    nei.mCancelable = true;
+
+    nsRefPtr<NotificationEvent> event =
+      NotificationEvent::Constructor(target, NS_LITERAL_STRING("notificationclick"), nei, result);
+    if (NS_WARN_IF(result.Failed())) {
+      return false;
+    }
+
+    event->SetTrusted(true);
+    target->DispatchDOMEvent(nullptr, event, nullptr, nullptr);
+    return true;
+  }
+};
+
+NS_IMETHODIMP
+ServiceWorkerManager::SendNotificationClickEvent(const nsACString& aOriginSuffix,
+                                                 const nsACString& aScope,
+                                                 const nsAString& aID,
+                                                 const nsAString& aTitle,
+                                                 const nsAString& aDir,
+                                                 const nsAString& aLang,
+                                                 const nsAString& aBody,
+                                                 const nsAString& aTag,
+                                                 const nsAString& aIcon,
+                                                 const nsAString& aData,
+                                                 const nsAString& aBehavior)
+{
+  OriginAttributes attrs;
+  if (!attrs.PopulateFromSuffix(aOriginSuffix)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  nsRefPtr<ServiceWorker> serviceWorker = CreateServiceWorkerForScope(attrs, aScope);
+  if (!serviceWorker) {
+    return NS_ERROR_FAILURE;
+  }
+  nsMainThreadPtrHandle<ServiceWorker> serviceWorkerHandle(
+    new nsMainThreadPtrHolder<ServiceWorker>(serviceWorker));
+
+  nsRefPtr<SendNotificationClickEventRunnable> r =
+    new SendNotificationClickEventRunnable(serviceWorker->GetWorkerPrivate(), serviceWorkerHandle, aID, aTitle, aDir, aLang, aBody, aTag, aIcon, aData, aBehavior);
+
+  AutoJSAPI jsapi;
+  jsapi.Init();
+  if (NS_WARN_IF(!r->Dispatch(jsapi.cx()))) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
 NS_IMETHODIMP
 ServiceWorkerManager::GetReadyPromise(nsIDOMWindow* aWindow,
                                       nsISupports** aPromise)
 {
   AssertIsOnMainThread();
   MOZ_ASSERT(aWindow);
 
   nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(aWindow);
--- a/dom/workers/test/serviceworkers/mochitest.ini
+++ b/dom/workers/test/serviceworkers/mochitest.ini
@@ -84,16 +84,18 @@ support-files =
   source_message_posting_worker.js
   scope/scope_worker.js
   redirect_serviceworker.sjs
   importscript.sjs
   importscript_worker.js
   client_focus_worker.js
   bug1151916_worker.js
   bug1151916_driver.html
+  notificationclick.html
+  notificationclick.js
   worker_updatefoundevent.js
   worker_updatefoundevent2.js
   updatefoundevent.html
   empty.js
   periodic_update_test.js
   periodic.sjs
   periodic/frame.html
   periodic/register.html
@@ -181,16 +183,17 @@ support-files =
 [test_post_message_advanced.html]
 [test_post_message_source.html]
 [test_register_base.html]
 [test_register_https_in_http.html]
 [test_request_context.html]
 skip-if = toolkit == 'android' # Bug 1163410
 [test_scopes.html]
 [test_sandbox_intercept.html]
+[test_notificationclick.html]
 [test_notification_constructor_error.html]
 [test_sanitize.html]
 [test_sanitize_domain.html]
 [test_service_worker_allowed.html]
 [test_serviceworker_interfaces.html]
 [test_serviceworker_not_sharedworker.html]
 [test_skip_waiting.html]
 [test_strict_mode_error.html]
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/notificationclick.html
@@ -0,0 +1,27 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Bug 1114554 - controlled page</title>
+<script class="testbody" type="text/javascript">
+  var testWindow = parent;
+  if (opener) {
+    testWindow = opener;
+  }
+
+  navigator.serviceWorker.ready.then(function(swr) {
+    swr.showNotification("Hi there. The ServiceWorker should receive a click event for this.");
+  });
+
+  navigator.serviceWorker.onmessage = function(msg) {
+    testWindow.callback();
+  };
+</script>
+
+</head>
+<body>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/notificationclick.js
@@ -0,0 +1,15 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+//
+onnotificationclick = function(e) {
+  self.clients.matchAll().then(function(clients) {
+    if (clients.length === 0) {
+      dump("********************* CLIENTS LIST EMPTY! Test will timeout! ***********************\n");
+      return;
+    }
+
+    clients.forEach(function(client) {
+      client.postMessage("done");
+    });
+  });
+}
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/serviceworkers/test_notificationclick.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=916893
+-->
+<head>
+  <title>Bug 1114554 - Test ServiceWorkerGlobalScope.notificationclick event.</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script>
+  <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+<script type="text/javascript">
+  SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events.");
+
+  function testFrame(src) {
+    var iframe = document.createElement("iframe");
+    iframe.src = src;
+    window.callback = function() {
+      window.callback = null;
+      document.body.removeChild(iframe);
+      iframe = null;
+      ok(true, "Got notificationclick event.");
+      MockServices.unregister();
+      SimpleTest.finish();
+    };
+    document.body.appendChild(iframe);
+  }
+
+  function runTest() {
+    MockServices.register();
+    testFrame('notificationclick.html');
+    navigator.serviceWorker.register("notificationclick.js", { scope: "notificationclick.html" }).then(function(reg) {
+    }, function(e) {
+      ok(false, "registration should have passed!");
+    });
+  };
+
+  SimpleTest.waitForExplicitFinish();
+  SpecialPowers.pushPrefEnv({"set": [
+    ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+    ["dom.serviceWorkers.enabled", true],
+    ["dom.serviceWorkers.testing.enabled", true],
+    ["dom.webnotifications.workers.enabled", true],
+    ["notification.prompt.testing", true],
+  ]}, runTest);
+</script>
+</body>
+</html>
--- a/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
+++ b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
@@ -148,16 +148,18 @@ var interfaceNamesInGlobalScope =
     "ImageData",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "MessageChannel",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "MessageEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "MessagePort",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "NotificationEvent",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "Performance",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PerformanceEntry",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PerformanceMark",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PerformanceMeasure",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/workers/test/test_notification.html
+++ b/dom/workers/test/test_notification.html
@@ -13,17 +13,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=916893">Bug 916893</a>
 <p id="display"></p>
 <div id="content" style="display: none">
 </div>
 <pre id="test">
 </pre>
 <script type="text/javascript">
-  SimpleTest.requestFlakyTimeout("Mock alert service dispatches show event.");
+  SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events.");
 
   function runTest() {
     MockServices.register();
     var w = new Worker("notification_worker.js");
     w.onmessage = function(e) {
       if (e.data.type === 'finish') {
         MockServices.unregister();
         SimpleTest.finish();
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1002,17 +1002,17 @@ pref("dom.disable_window_open_feature.sc
 pref("dom.disable_window_open_feature.resizable",   true);
 pref("dom.disable_window_open_feature.minimizable", false);
 pref("dom.disable_window_open_feature.status",      true);
 
 pref("dom.allow_scripts_to_close_windows",          false);
 
 pref("dom.disable_open_during_load",                false);
 pref("dom.popup_maximum",                           20);
-pref("dom.popup_allowed_events", "change click dblclick mouseup reset submit touchend");
+pref("dom.popup_allowed_events", "change click dblclick mouseup notificationclick reset submit touchend");
 pref("dom.disable_open_click_delay", 1000);
 
 pref("dom.storage.enabled", true);
 pref("dom.storage.default_quota",      5120);
 
 pref("dom.send_after_paint_to_content", false);
 
 // Timeout clamp in ms for timeouts we clamp