summaryrefslogtreecommitdiffstats
path: root/dom/notification
diff options
context:
space:
mode:
Diffstat (limited to 'dom/notification')
-rw-r--r--dom/notification/DesktopNotification.cpp319
-rw-r--r--dom/notification/DesktopNotification.h180
-rw-r--r--dom/notification/Notification.cpp2769
-rw-r--r--dom/notification/Notification.h471
-rw-r--r--dom/notification/NotificationDB.jsm360
-rw-r--r--dom/notification/NotificationEvent.cpp26
-rw-r--r--dom/notification/NotificationEvent.h75
-rw-r--r--dom/notification/NotificationStorage.js274
-rw-r--r--dom/notification/NotificationStorage.manifest3
-rw-r--r--dom/notification/moz.build41
-rw-r--r--dom/notification/test/browser/browser.ini2
-rw-r--r--dom/notification/test/browser/browser_permission_dismiss.js113
-rw-r--r--dom/notification/test/browser/notification.html11
-rw-r--r--dom/notification/test/unit/common_test_notificationdb.js60
-rw-r--r--dom/notification/test/unit/test_notificationdb.js310
-rw-r--r--dom/notification/test/unit/test_notificationdb_bug1024090.js56
-rw-r--r--dom/notification/test/unit/xpcshell.ini7
17 files changed, 5077 insertions, 0 deletions
diff --git a/dom/notification/DesktopNotification.cpp b/dom/notification/DesktopNotification.cpp
new file mode 100644
index 000000000..76f1c5afb
--- /dev/null
+++ b/dom/notification/DesktopNotification.cpp
@@ -0,0 +1,319 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=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 "mozilla/dom/DesktopNotification.h"
+#include "mozilla/dom/DesktopNotificationBinding.h"
+#include "mozilla/dom/AppNotificationServiceOptionsBinding.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentPermissionHelper.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/dom/PBrowserChild.h"
+#include "mozilla/Preferences.h"
+#include "nsGlobalWindow.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsServiceManagerUtils.h"
+#include "PermissionMessageUtils.h"
+#include "nsILoadContext.h"
+
+namespace mozilla {
+namespace dom {
+
+/*
+ * Simple Request
+ */
+class DesktopNotificationRequest : public nsIContentPermissionRequest
+ , public Runnable
+{
+ virtual ~DesktopNotificationRequest()
+ {
+ }
+
+ nsCOMPtr<nsIContentPermissionRequester> mRequester;
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSICONTENTPERMISSIONREQUEST
+
+ explicit DesktopNotificationRequest(DesktopNotification* aNotification)
+ : mDesktopNotification(aNotification)
+ {
+ mRequester = new nsContentPermissionRequester(mDesktopNotification->GetOwner());
+ }
+
+ NS_IMETHOD Run() override
+ {
+ nsCOMPtr<nsPIDOMWindowInner> window = mDesktopNotification->GetOwner();
+ nsContentPermissionUtils::AskPermission(this, window);
+ return NS_OK;
+ }
+
+ RefPtr<DesktopNotification> mDesktopNotification;
+};
+
+/* ------------------------------------------------------------------------ */
+/* AlertServiceObserver */
+/* ------------------------------------------------------------------------ */
+
+NS_IMPL_ISUPPORTS(AlertServiceObserver, nsIObserver)
+
+/* ------------------------------------------------------------------------ */
+/* DesktopNotification */
+/* ------------------------------------------------------------------------ */
+
+uint32_t DesktopNotification::sCount = 0;
+
+nsresult
+DesktopNotification::PostDesktopNotification()
+{
+ if (!mObserver) {
+ mObserver = new AlertServiceObserver(this);
+ }
+
+ nsCOMPtr<nsIAlertsService> alerts = do_GetService("@mozilla.org/alerts-service;1");
+ if (!alerts) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ // Generate a unique name (which will also be used as a cookie) because
+ // the nsIAlertsService will coalesce notifications with the same name.
+ // In the case of IPC, the parent process will use the cookie to map
+ // to nsIObservers, thus cookies must be unique to differentiate observers.
+ nsString uniqueName = NS_LITERAL_STRING("desktop-notification:");
+ uniqueName.AppendInt(sCount++);
+ nsCOMPtr<nsPIDOMWindowInner> owner = GetOwner();
+ if (!owner) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIDocument> doc = owner->GetDoc();
+ nsIPrincipal* principal = doc->NodePrincipal();
+ nsCOMPtr<nsILoadContext> loadContext = doc->GetLoadContext();
+ bool inPrivateBrowsing = loadContext && loadContext->UsePrivateBrowsing();
+ nsCOMPtr<nsIAlertNotification> alert =
+ do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE);
+ nsresult rv = alert->Init(uniqueName, mIconURL, mTitle,
+ mDescription,
+ true,
+ uniqueName,
+ NS_LITERAL_STRING("auto"),
+ EmptyString(),
+ EmptyString(),
+ principal,
+ inPrivateBrowsing,
+ false /* requireInteraction */);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return alerts->ShowAlert(alert, mObserver);
+}
+
+DesktopNotification::DesktopNotification(const nsAString & title,
+ const nsAString & description,
+ const nsAString & iconURL,
+ nsPIDOMWindowInner* aWindow,
+ nsIPrincipal* principal)
+ : DOMEventTargetHelper(aWindow)
+ , mTitle(title)
+ , mDescription(description)
+ , mIconURL(iconURL)
+ , mPrincipal(principal)
+ , mAllow(false)
+ , mShowHasBeenCalled(false)
+{
+ if (Preferences::GetBool("notification.disabled", false)) {
+ return;
+ }
+
+ // If we are in testing mode (running mochitests, for example)
+ // and we are suppose to allow requests, then just post an allow event.
+ if (Preferences::GetBool("notification.prompt.testing", false) &&
+ Preferences::GetBool("notification.prompt.testing.allow", true)) {
+ mAllow = true;
+ }
+}
+
+void
+DesktopNotification::Init()
+{
+ RefPtr<DesktopNotificationRequest> request = new DesktopNotificationRequest(this);
+
+ NS_DispatchToMainThread(request);
+}
+
+DesktopNotification::~DesktopNotification()
+{
+ if (mObserver) {
+ mObserver->Disconnect();
+ }
+}
+
+void
+DesktopNotification::DispatchNotificationEvent(const nsString& aName)
+{
+ if (NS_FAILED(CheckInnerWindowCorrectness())) {
+ return;
+ }
+
+ RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
+ // it doesn't bubble, and it isn't cancelable
+ event->InitEvent(aName, false, false);
+ event->SetTrusted(true);
+ DispatchDOMEvent(nullptr, event, nullptr, nullptr);
+}
+
+nsresult
+DesktopNotification::SetAllow(bool aAllow)
+{
+ mAllow = aAllow;
+
+ // if we have called Show() already, lets go ahead and post a notification
+ if (mShowHasBeenCalled && aAllow) {
+ return PostDesktopNotification();
+ }
+
+ return NS_OK;
+}
+
+void
+DesktopNotification::HandleAlertServiceNotification(const char *aTopic)
+{
+ if (NS_FAILED(CheckInnerWindowCorrectness())) {
+ return;
+ }
+
+ if (!strcmp("alertclickcallback", aTopic)) {
+ DispatchNotificationEvent(NS_LITERAL_STRING("click"));
+ } else if (!strcmp("alertfinished", aTopic)) {
+ DispatchNotificationEvent(NS_LITERAL_STRING("close"));
+ }
+}
+
+void
+DesktopNotification::Show(ErrorResult& aRv)
+{
+ mShowHasBeenCalled = true;
+
+ if (!mAllow) {
+ return;
+ }
+
+ aRv = PostDesktopNotification();
+}
+
+JSObject*
+DesktopNotification::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return DesktopNotificationBinding::Wrap(aCx, this, aGivenProto);
+}
+
+/* ------------------------------------------------------------------------ */
+/* DesktopNotificationCenter */
+/* ------------------------------------------------------------------------ */
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(DesktopNotificationCenter)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DesktopNotificationCenter)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DesktopNotificationCenter)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DesktopNotificationCenter)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+already_AddRefed<DesktopNotification>
+DesktopNotificationCenter::CreateNotification(const nsAString& aTitle,
+ const nsAString& aDescription,
+ const nsAString& aIconURL)
+{
+ MOZ_ASSERT(mOwner);
+
+ RefPtr<DesktopNotification> notification =
+ new DesktopNotification(aTitle,
+ aDescription,
+ aIconURL,
+ mOwner,
+ mPrincipal);
+ notification->Init();
+ return notification.forget();
+}
+
+JSObject*
+DesktopNotificationCenter::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return DesktopNotificationCenterBinding::Wrap(aCx, this, aGivenProto);
+}
+
+/* ------------------------------------------------------------------------ */
+/* DesktopNotificationRequest */
+/* ------------------------------------------------------------------------ */
+
+NS_IMPL_ISUPPORTS_INHERITED(DesktopNotificationRequest, Runnable,
+ nsIContentPermissionRequest)
+
+NS_IMETHODIMP
+DesktopNotificationRequest::GetPrincipal(nsIPrincipal * *aRequestingPrincipal)
+{
+ if (!mDesktopNotification) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ NS_IF_ADDREF(*aRequestingPrincipal = mDesktopNotification->mPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DesktopNotificationRequest::GetWindow(mozIDOMWindow** aRequestingWindow)
+{
+ if (!mDesktopNotification) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ NS_IF_ADDREF(*aRequestingWindow = mDesktopNotification->GetOwner());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DesktopNotificationRequest::GetElement(nsIDOMElement * *aElement)
+{
+ NS_ENSURE_ARG_POINTER(aElement);
+ *aElement = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DesktopNotificationRequest::Cancel()
+{
+ nsresult rv = mDesktopNotification->SetAllow(false);
+ mDesktopNotification = nullptr;
+ return rv;
+}
+
+NS_IMETHODIMP
+DesktopNotificationRequest::Allow(JS::HandleValue aChoices)
+{
+ MOZ_ASSERT(aChoices.isUndefined());
+ nsresult rv = mDesktopNotification->SetAllow(true);
+ mDesktopNotification = nullptr;
+ return rv;
+}
+
+NS_IMETHODIMP
+DesktopNotificationRequest::GetRequester(nsIContentPermissionRequester** aRequester)
+{
+ NS_ENSURE_ARG_POINTER(aRequester);
+
+ nsCOMPtr<nsIContentPermissionRequester> requester = mRequester;
+ requester.forget(aRequester);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DesktopNotificationRequest::GetTypes(nsIArray** aTypes)
+{
+ nsTArray<nsString> emptyOptions;
+ return nsContentPermissionUtils::CreatePermissionArray(NS_LITERAL_CSTRING("desktop-notification"),
+ NS_LITERAL_CSTRING("unused"),
+ emptyOptions,
+ aTypes);
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/notification/DesktopNotification.h b/dom/notification/DesktopNotification.h
new file mode 100644
index 000000000..e1cb2efbc
--- /dev/null
+++ b/dom/notification/DesktopNotification.h
@@ -0,0 +1,180 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=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/. */
+
+#ifndef mozilla_dom_DesktopNotification_h
+#define mozilla_dom_DesktopNotification_h
+
+#include "nsIPrincipal.h"
+#include "nsIAlertsService.h"
+#include "nsIContentPermissionPrompt.h"
+
+#include "nsIObserver.h"
+#include "nsString.h"
+#include "nsWeakPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIDOMWindow.h"
+#include "nsIScriptObjectPrincipal.h"
+
+#include "nsIDOMEvent.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/ErrorResult.h"
+#include "nsWrapperCache.h"
+
+
+namespace mozilla {
+namespace dom {
+
+class AlertServiceObserver;
+class DesktopNotification;
+
+/*
+ * DesktopNotificationCenter
+ * Object hangs off of the navigator object and hands out DesktopNotification objects
+ */
+class DesktopNotificationCenter final : public nsISupports,
+ public nsWrapperCache
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DesktopNotificationCenter)
+
+ explicit DesktopNotificationCenter(nsPIDOMWindowInner* aWindow)
+ {
+ MOZ_ASSERT(aWindow);
+ mOwner = aWindow;
+
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
+ MOZ_ASSERT(sop);
+
+ mPrincipal = sop->GetPrincipal();
+ MOZ_ASSERT(mPrincipal);
+ }
+
+ void Shutdown() {
+ mOwner = nullptr;
+ }
+
+ nsPIDOMWindowInner* GetParentObject() const
+ {
+ return mOwner;
+ }
+
+ virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+ already_AddRefed<DesktopNotification>
+ CreateNotification(const nsAString& title,
+ const nsAString& description,
+ const nsAString& iconURL);
+
+private:
+ virtual ~DesktopNotificationCenter()
+ {
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> mOwner;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+};
+
+class DesktopNotificationRequest;
+
+class DesktopNotification final : public DOMEventTargetHelper
+{
+ friend class DesktopNotificationRequest;
+
+public:
+
+ DesktopNotification(const nsAString& aTitle,
+ const nsAString& aDescription,
+ const nsAString& aIconURL,
+ nsPIDOMWindowInner* aWindow,
+ nsIPrincipal* principal);
+
+ virtual ~DesktopNotification();
+
+ void Init();
+
+ /*
+ * PostDesktopNotification
+ * Uses alert service to display a notification
+ */
+ nsresult PostDesktopNotification();
+
+ nsresult SetAllow(bool aAllow);
+
+ /*
+ * Creates and dispatches a dom event of type aName
+ */
+ void DispatchNotificationEvent(const nsString& aName);
+
+ void HandleAlertServiceNotification(const char *aTopic);
+
+ // WebIDL
+
+ nsPIDOMWindowInner* GetParentObject() const
+ {
+ return GetOwner();
+ }
+
+ virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+ void Show(ErrorResult& aRv);
+
+ IMPL_EVENT_HANDLER(click)
+ IMPL_EVENT_HANDLER(close)
+
+protected:
+
+ nsString mTitle;
+ nsString mDescription;
+ nsString mIconURL;
+
+ RefPtr<AlertServiceObserver> mObserver;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ bool mAllow;
+ bool mShowHasBeenCalled;
+
+ static uint32_t sCount;
+};
+
+class AlertServiceObserver: public nsIObserver
+{
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit AlertServiceObserver(DesktopNotification* notification)
+ : mNotification(notification) {}
+
+ void Disconnect() { mNotification = nullptr; }
+
+ NS_IMETHOD
+ Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) override
+ {
+
+ // forward to parent
+ if (mNotification) {
+#ifdef MOZ_B2G
+ if (NS_FAILED(mNotification->CheckInnerWindowCorrectness()))
+ return NS_ERROR_NOT_AVAILABLE;
+#endif
+ mNotification->HandleAlertServiceNotification(aTopic);
+ }
+ return NS_OK;
+ };
+
+ private:
+ virtual ~AlertServiceObserver() {}
+
+ DesktopNotification* mNotification;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_DesktopNotification_h */
diff --git a/dom/notification/Notification.cpp b/dom/notification/Notification.cpp
new file mode 100644
index 000000000..9c0ce2f18
--- /dev/null
+++ b/dom/notification/Notification.cpp
@@ -0,0 +1,2769 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=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 "mozilla/dom/Notification.h"
+
+#include "mozilla/JSONWriter.h"
+#include "mozilla/Move.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+
+#include "mozilla/dom/AppNotificationServiceOptionsBinding.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/NotificationEvent.h"
+#include "mozilla/dom/PermissionMessageUtils.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseWorkerProxy.h"
+#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h"
+
+#include "nsAlertsUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentPermissionHelper.h"
+#include "nsContentUtils.h"
+#include "nsCRTGlue.h"
+#include "nsDOMJSUtils.h"
+#include "nsGlobalWindow.h"
+#include "nsIAlertsService.h"
+#include "nsIContentPermissionPrompt.h"
+#include "nsIDocument.h"
+#include "nsILoadContext.h"
+#include "nsINotificationStorage.h"
+#include "nsIPermissionManager.h"
+#include "nsIPermission.h"
+#include "nsIPushService.h"
+#include "nsIScriptSecurityManager.h"
+#include "nsIServiceWorkerManager.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIUUIDGenerator.h"
+#include "nsIXPConnect.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStructuredCloneContainer.h"
+#include "nsThreadUtils.h"
+#include "nsToolkitCompsCID.h"
+#include "nsXULAppAPI.h"
+#include "ServiceWorkerManager.h"
+#include "WorkerPrivate.h"
+#include "WorkerRunnable.h"
+#include "WorkerScope.h"
+
+namespace mozilla {
+namespace dom {
+
+using namespace workers;
+
+struct NotificationStrings
+{
+ 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;
+ const nsString mServiceWorkerRegistrationScope;
+};
+
+class ScopeCheckingGetCallback : public nsINotificationStorageCallback
+{
+ const nsString mScope;
+public:
+ explicit ScopeCheckingGetCallback(const nsAString& aScope)
+ : mScope(aScope)
+ {}
+
+ 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,
+ const nsAString& aServiceWorkerRegistrationScope) final
+ {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(!aID.IsEmpty());
+
+ // Skip scopes that don't match when called from getNotifications().
+ if (!mScope.IsEmpty() && !mScope.Equals(aServiceWorkerRegistrationScope)) {
+ return NS_OK;
+ }
+
+ NotificationStrings strings = {
+ nsString(aID),
+ nsString(aTitle),
+ nsString(aDir),
+ nsString(aLang),
+ nsString(aBody),
+ nsString(aTag),
+ nsString(aIcon),
+ nsString(aData),
+ nsString(aBehavior),
+ nsString(aServiceWorkerRegistrationScope),
+ };
+
+ mStrings.AppendElement(Move(strings));
+ return NS_OK;
+ }
+
+ NS_IMETHOD Done() override = 0;
+
+protected:
+ virtual ~ScopeCheckingGetCallback()
+ {}
+
+ nsTArray<NotificationStrings> mStrings;
+};
+
+class NotificationStorageCallback final : public ScopeCheckingGetCallback
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(NotificationStorageCallback)
+
+ NotificationStorageCallback(nsIGlobalObject* aWindow, const nsAString& aScope,
+ Promise* aPromise)
+ : ScopeCheckingGetCallback(aScope),
+ mWindow(aWindow),
+ mPromise(aPromise)
+ {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aPromise);
+ }
+
+ NS_IMETHOD Done() final
+ {
+ ErrorResult result;
+ AutoTArray<RefPtr<Notification>, 5> notifications;
+
+ for (uint32_t i = 0; i < mStrings.Length(); ++i) {
+ RefPtr<Notification> n =
+ Notification::ConstructFromFields(mWindow,
+ mStrings[i].mID,
+ mStrings[i].mTitle,
+ mStrings[i].mDir,
+ mStrings[i].mLang,
+ mStrings[i].mBody,
+ mStrings[i].mTag,
+ mStrings[i].mIcon,
+ mStrings[i].mData,
+ /* mStrings[i].mBehavior, not
+ * supported */
+ mStrings[i].mServiceWorkerRegistrationScope,
+ result);
+
+ n->SetStoredState(true);
+ Unused << NS_WARN_IF(result.Failed());
+ if (!result.Failed()) {
+ notifications.AppendElement(n.forget());
+ }
+ }
+
+ mPromise->MaybeResolve(notifications);
+ return NS_OK;
+ }
+
+private:
+ virtual ~NotificationStorageCallback()
+ {}
+
+ nsCOMPtr<nsIGlobalObject> mWindow;
+ RefPtr<Promise> mPromise;
+ const nsString mScope;
+};
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(NotificationStorageCallback)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(NotificationStorageCallback)
+NS_IMPL_CYCLE_COLLECTION(NotificationStorageCallback, mWindow, mPromise);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(NotificationStorageCallback)
+ NS_INTERFACE_MAP_ENTRY(nsINotificationStorageCallback)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+class NotificationGetRunnable final : public Runnable
+{
+ const nsString mOrigin;
+ const nsString mTag;
+ nsCOMPtr<nsINotificationStorageCallback> mCallback;
+public:
+ NotificationGetRunnable(const nsAString& aOrigin,
+ const nsAString& aTag,
+ nsINotificationStorageCallback* aCallback)
+ : mOrigin(aOrigin), mTag(aTag), mCallback(aCallback)
+ {}
+
+ NS_IMETHOD
+ Run() override
+ {
+ nsresult rv;
+ nsCOMPtr<nsINotificationStorage> notificationStorage =
+ do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = notificationStorage->Get(mOrigin, mTag, mCallback);
+ //XXXnsm Is it guaranteed mCallback will be called in case of failure?
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ return rv;
+ }
+};
+
+class NotificationPermissionRequest : public nsIContentPermissionRequest,
+ public nsIRunnable
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_NSICONTENTPERMISSIONREQUEST
+ NS_DECL_NSIRUNNABLE
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(NotificationPermissionRequest,
+ nsIContentPermissionRequest)
+
+ NotificationPermissionRequest(nsIPrincipal* aPrincipal,
+ nsPIDOMWindowInner* aWindow, Promise* aPromise,
+ NotificationPermissionCallback* aCallback)
+ : mPrincipal(aPrincipal), mWindow(aWindow),
+ mPermission(NotificationPermission::Default),
+ mPromise(aPromise),
+ mCallback(aCallback)
+ {
+ MOZ_ASSERT(aPromise);
+ mRequester = new nsContentPermissionRequester(mWindow);
+ }
+
+protected:
+ virtual ~NotificationPermissionRequest() {}
+
+ nsresult ResolvePromise();
+ nsresult DispatchResolvePromise();
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+ NotificationPermission mPermission;
+ RefPtr<Promise> mPromise;
+ RefPtr<NotificationPermissionCallback> mCallback;
+ nsCOMPtr<nsIContentPermissionRequester> mRequester;
+};
+
+namespace {
+class ReleaseNotificationControlRunnable final : public MainThreadWorkerControlRunnable
+{
+ Notification* mNotification;
+
+public:
+ explicit ReleaseNotificationControlRunnable(Notification* aNotification)
+ : MainThreadWorkerControlRunnable(aNotification->mWorkerPrivate)
+ , mNotification(aNotification)
+ { }
+
+ bool
+ WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
+ {
+ mNotification->ReleaseObject();
+ return true;
+ }
+};
+
+class GetPermissionRunnable final : public WorkerMainThreadRunnable
+{
+ NotificationPermission mPermission;
+
+public:
+ explicit GetPermissionRunnable(WorkerPrivate* aWorker)
+ : WorkerMainThreadRunnable(aWorker,
+ NS_LITERAL_CSTRING("Notification :: Get Permission"))
+ , mPermission(NotificationPermission::Denied)
+ { }
+
+ bool
+ MainThreadRun() override
+ {
+ ErrorResult result;
+ mPermission =
+ Notification::GetPermissionInternal(mWorkerPrivate->GetPrincipal(),
+ result);
+ return true;
+ }
+
+ NotificationPermission
+ GetPermission()
+ {
+ return mPermission;
+ }
+};
+
+class FocusWindowRunnable final : public Runnable
+{
+ nsMainThreadPtrHandle<nsPIDOMWindowInner> mWindow;
+public:
+ explicit FocusWindowRunnable(const nsMainThreadPtrHandle<nsPIDOMWindowInner>& aWindow)
+ : mWindow(aWindow)
+ { }
+
+ NS_IMETHOD
+ Run() override
+ {
+ AssertIsOnMainThread();
+ if (!mWindow->IsCurrentInnerWindow()) {
+ // Window has been closed, this observer is not valid anymore
+ return NS_OK;
+ }
+
+ nsIDocument* doc = mWindow->GetExtantDoc();
+ if (doc) {
+ // Browser UI may use DOMWebNotificationClicked to focus the tab
+ // from which the event was dispatched.
+ nsContentUtils::DispatchChromeEvent(doc, mWindow->GetOuterWindow(),
+ NS_LITERAL_STRING("DOMWebNotificationClicked"),
+ true, true);
+ }
+
+ return NS_OK;
+ }
+};
+
+nsresult
+CheckScope(nsIPrincipal* aPrincipal, const nsACString& aScope)
+{
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aPrincipal);
+
+ nsCOMPtr<nsIURI> scopeURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return aPrincipal->CheckMayLoad(scopeURI, /* report = */ true,
+ /* allowIfInheritsPrincipal = */ false);
+}
+} // anonymous namespace
+
+// Subclass that can be directly dispatched to child workers from the main
+// thread.
+class NotificationWorkerRunnable : public MainThreadWorkerRunnable
+{
+protected:
+ explicit NotificationWorkerRunnable(WorkerPrivate* aWorkerPrivate)
+ : MainThreadWorkerRunnable(aWorkerPrivate)
+ {
+ }
+
+ bool
+ WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
+ {
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ aWorkerPrivate->ModifyBusyCountFromWorker(true);
+ WorkerRunInternal(aWorkerPrivate);
+ return true;
+ }
+
+ void
+ PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate,
+ bool aRunResult) override
+ {
+ aWorkerPrivate->ModifyBusyCountFromWorker(false);
+ }
+
+ virtual void
+ WorkerRunInternal(WorkerPrivate* aWorkerPrivate) = 0;
+};
+
+// Overrides dispatch and run handlers so we can directly dispatch from main
+// thread to child workers.
+class NotificationEventWorkerRunnable final : public NotificationWorkerRunnable
+{
+ Notification* mNotification;
+ const nsString mEventName;
+public:
+ NotificationEventWorkerRunnable(Notification* aNotification,
+ const nsString& aEventName)
+ : NotificationWorkerRunnable(aNotification->mWorkerPrivate)
+ , mNotification(aNotification)
+ , mEventName(aEventName)
+ {}
+
+ void
+ WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override
+ {
+ mNotification->DispatchTrustedEvent(mEventName);
+ }
+};
+
+class ReleaseNotificationRunnable final : public NotificationWorkerRunnable
+{
+ Notification* mNotification;
+public:
+ explicit ReleaseNotificationRunnable(Notification* aNotification)
+ : NotificationWorkerRunnable(aNotification->mWorkerPrivate)
+ , mNotification(aNotification)
+ {}
+
+ void
+ WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override
+ {
+ mNotification->ReleaseObject();
+ }
+};
+
+// Create one whenever you require ownership of the notification. Use with
+// UniquePtr<>. See Notification.h for details.
+class NotificationRef final {
+ friend class WorkerNotificationObserver;
+
+private:
+ Notification* mNotification;
+ bool mInited;
+
+ // Only useful for workers.
+ void
+ Forget()
+ {
+ mNotification = nullptr;
+ }
+
+public:
+ explicit NotificationRef(Notification* aNotification)
+ : mNotification(aNotification)
+ {
+ MOZ_ASSERT(mNotification);
+ if (mNotification->mWorkerPrivate) {
+ mNotification->mWorkerPrivate->AssertIsOnWorkerThread();
+ } else {
+ AssertIsOnMainThread();
+ }
+
+ mInited = mNotification->AddRefObject();
+ }
+
+ // This is only required because Gecko runs script in a worker's onclose
+ // handler (non-standard, Bug 790919) where calls to HoldWorker() will
+ // fail. Due to non-standardness and added complications if we decide to
+ // support this, attempts to create a Notification in onclose just throw
+ // exceptions.
+ bool
+ Initialized()
+ {
+ return mInited;
+ }
+
+ ~NotificationRef()
+ {
+ if (Initialized() && mNotification) {
+ Notification* notification = mNotification;
+ mNotification = nullptr;
+ if (notification->mWorkerPrivate && NS_IsMainThread()) {
+ // Try to pass ownership back to the worker. If the dispatch succeeds we
+ // are guaranteed this runnable will run, and that it will run after queued
+ // event runnables, so event runnables will have a safe pointer to the
+ // Notification.
+ //
+ // If the dispatch fails, the worker isn't running anymore and the event
+ // runnables have already run or been canceled. We can use a control
+ // runnable to release the reference.
+ RefPtr<ReleaseNotificationRunnable> r =
+ new ReleaseNotificationRunnable(notification);
+
+ if (!r->Dispatch()) {
+ RefPtr<ReleaseNotificationControlRunnable> r =
+ new ReleaseNotificationControlRunnable(notification);
+ MOZ_ALWAYS_TRUE(r->Dispatch());
+ }
+ } else {
+ notification->AssertIsOnTargetThread();
+ notification->ReleaseObject();
+ }
+ }
+ }
+
+ // XXXnsm, is it worth having some sort of WeakPtr like wrapper instead of
+ // a rawptr that the NotificationRef can invalidate?
+ Notification*
+ GetNotification()
+ {
+ MOZ_ASSERT(Initialized());
+ return mNotification;
+ }
+};
+
+class NotificationTask : public Runnable
+{
+public:
+ enum NotificationAction {
+ eShow,
+ eClose
+ };
+
+ NotificationTask(UniquePtr<NotificationRef> aRef, NotificationAction aAction)
+ : mNotificationRef(Move(aRef)), mAction(aAction)
+ {}
+
+ NS_IMETHOD
+ Run() override;
+protected:
+ virtual ~NotificationTask() {}
+
+ UniquePtr<NotificationRef> mNotificationRef;
+ NotificationAction mAction;
+};
+
+uint32_t Notification::sCount = 0;
+
+NS_IMPL_CYCLE_COLLECTION(NotificationPermissionRequest, mWindow, mPromise,
+ mCallback)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(NotificationPermissionRequest)
+ NS_INTERFACE_MAP_ENTRY(nsIContentPermissionRequest)
+ NS_INTERFACE_MAP_ENTRY(nsIRunnable)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIContentPermissionRequest)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(NotificationPermissionRequest)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(NotificationPermissionRequest)
+
+NS_IMETHODIMP
+NotificationPermissionRequest::Run()
+{
+ if (nsContentUtils::IsSystemPrincipal(mPrincipal)) {
+ mPermission = NotificationPermission::Granted;
+ } else {
+ // File are automatically granted permission.
+ nsCOMPtr<nsIURI> uri;
+ mPrincipal->GetURI(getter_AddRefs(uri));
+
+ if (uri) {
+ bool isFile;
+ uri->SchemeIs("file", &isFile);
+ if (isFile) {
+ mPermission = NotificationPermission::Granted;
+ }
+ }
+ }
+
+ // Grant permission if pref'ed on.
+ if (Preferences::GetBool("notification.prompt.testing", false)) {
+ if (Preferences::GetBool("notification.prompt.testing.allow", true)) {
+ mPermission = NotificationPermission::Granted;
+ } else {
+ mPermission = NotificationPermission::Denied;
+ }
+ }
+
+ if (mPermission != NotificationPermission::Default) {
+ return DispatchResolvePromise();
+ }
+
+ return nsContentPermissionUtils::AskPermission(this, mWindow);
+}
+
+NS_IMETHODIMP
+NotificationPermissionRequest::GetPrincipal(nsIPrincipal** aRequestingPrincipal)
+{
+ NS_ADDREF(*aRequestingPrincipal = mPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+NotificationPermissionRequest::GetWindow(mozIDOMWindow** aRequestingWindow)
+{
+ NS_ADDREF(*aRequestingWindow = mWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+NotificationPermissionRequest::GetElement(nsIDOMElement** aElement)
+{
+ NS_ENSURE_ARG_POINTER(aElement);
+ *aElement = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+NotificationPermissionRequest::Cancel()
+{
+ // `Cancel` is called if the user denied permission or dismissed the
+ // permission request. To distinguish between the two, we set the
+ // permission to "default" and query the permission manager in
+ // `ResolvePromise`.
+ mPermission = NotificationPermission::Default;
+ return DispatchResolvePromise();
+}
+
+NS_IMETHODIMP
+NotificationPermissionRequest::Allow(JS::HandleValue aChoices)
+{
+ MOZ_ASSERT(aChoices.isUndefined());
+
+ mPermission = NotificationPermission::Granted;
+ return DispatchResolvePromise();
+}
+
+NS_IMETHODIMP
+NotificationPermissionRequest::GetRequester(nsIContentPermissionRequester** aRequester)
+{
+ NS_ENSURE_ARG_POINTER(aRequester);
+
+ nsCOMPtr<nsIContentPermissionRequester> requester = mRequester;
+ requester.forget(aRequester);
+ return NS_OK;
+}
+
+inline nsresult
+NotificationPermissionRequest::DispatchResolvePromise()
+{
+ return NS_DispatchToMainThread(NewRunnableMethod(this,
+ &NotificationPermissionRequest::ResolvePromise));
+}
+
+nsresult
+NotificationPermissionRequest::ResolvePromise()
+{
+ nsresult rv = NS_OK;
+ if (mPermission == NotificationPermission::Default) {
+ // This will still be "default" if the user dismissed the doorhanger,
+ // or "denied" otherwise.
+ mPermission = Notification::TestPermission(mPrincipal);
+ }
+ if (mCallback) {
+ ErrorResult error;
+ mCallback->Call(mPermission, error);
+ rv = error.StealNSResult();
+ }
+ Telemetry::Accumulate(
+ Telemetry::WEB_NOTIFICATION_REQUEST_PERMISSION_CALLBACK, !!mCallback);
+ mPromise->MaybeResolve(mPermission);
+ return rv;
+}
+
+NS_IMETHODIMP
+NotificationPermissionRequest::GetTypes(nsIArray** aTypes)
+{
+ nsTArray<nsString> emptyOptions;
+ return nsContentPermissionUtils::CreatePermissionArray(NS_LITERAL_CSTRING("desktop-notification"),
+ NS_LITERAL_CSTRING("unused"),
+ emptyOptions,
+ aTypes);
+}
+
+NS_IMPL_ISUPPORTS(NotificationTelemetryService, nsIObserver)
+
+NotificationTelemetryService::NotificationTelemetryService()
+ : mDNDRecorded(false)
+{}
+
+NotificationTelemetryService::~NotificationTelemetryService()
+{
+ Unused << NS_WARN_IF(NS_FAILED(RemovePermissionChangeObserver()));
+}
+
+/* static */ already_AddRefed<NotificationTelemetryService>
+NotificationTelemetryService::GetInstance()
+{
+ nsCOMPtr<nsISupports> telemetrySupports =
+ do_GetService(NOTIFICATIONTELEMETRYSERVICE_CONTRACTID);
+ if (!telemetrySupports) {
+ return nullptr;
+ }
+ RefPtr<NotificationTelemetryService> telemetry =
+ static_cast<NotificationTelemetryService*>(telemetrySupports.get());
+ return telemetry.forget();
+}
+
+nsresult
+NotificationTelemetryService::Init()
+{
+ nsresult rv = AddPermissionChangeObserver();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RecordPermissions();
+
+ return NS_OK;
+}
+
+nsresult
+NotificationTelemetryService::RemovePermissionChangeObserver()
+{
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (!obs) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return obs->RemoveObserver(this, "perm-changed");
+}
+
+nsresult
+NotificationTelemetryService::AddPermissionChangeObserver()
+{
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (!obs) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return obs->AddObserver(this, "perm-changed", false);
+}
+
+void
+NotificationTelemetryService::RecordPermissions()
+{
+ if (!Telemetry::CanRecordBase() || !Telemetry::CanRecordExtended()) {
+ return;
+ }
+
+ nsCOMPtr<nsIPermissionManager> permissionManager =
+ services::GetPermissionManager();
+ if (!permissionManager) {
+ return;
+ }
+
+ nsCOMPtr<nsISimpleEnumerator> enumerator;
+ nsresult rv = permissionManager->GetEnumerator(getter_AddRefs(enumerator));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ for (;;) {
+ bool hasMoreElements;
+ nsresult rv = enumerator->HasMoreElements(&hasMoreElements);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ if (!hasMoreElements) {
+ break;
+ }
+ nsCOMPtr<nsISupports> supportsPermission;
+ rv = enumerator->GetNext(getter_AddRefs(supportsPermission));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ uint32_t capability;
+ if (!GetNotificationPermission(supportsPermission, &capability)) {
+ continue;
+ }
+ if (capability == nsIPermissionManager::DENY_ACTION) {
+ Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_PERMISSIONS, 0);
+ } else if (capability == nsIPermissionManager::ALLOW_ACTION) {
+ Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_PERMISSIONS, 1);
+ }
+ }
+}
+
+bool
+NotificationTelemetryService::GetNotificationPermission(nsISupports* aSupports,
+ uint32_t* aCapability)
+{
+ nsCOMPtr<nsIPermission> permission = do_QueryInterface(aSupports);
+ if (!permission) {
+ return false;
+ }
+ nsAutoCString type;
+ permission->GetType(type);
+ if (!type.Equals("desktop-notification")) {
+ return false;
+ }
+ permission->GetCapability(aCapability);
+ return true;
+}
+
+void
+NotificationTelemetryService::RecordDNDSupported()
+{
+ if (mDNDRecorded) {
+ return;
+ }
+
+ nsCOMPtr<nsIAlertsService> alertService =
+ do_GetService(NS_ALERTSERVICE_CONTRACTID);
+ if (!alertService) {
+ return;
+ }
+
+ nsCOMPtr<nsIAlertsDoNotDisturb> alertServiceDND =
+ do_QueryInterface(alertService);
+ if (!alertServiceDND) {
+ return;
+ }
+
+ mDNDRecorded = true;
+ bool isEnabled;
+ nsresult rv = alertServiceDND->GetManualDoNotDisturb(&isEnabled);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ Telemetry::Accumulate(
+ Telemetry::ALERTS_SERVICE_DND_SUPPORTED_FLAG, true);
+}
+
+nsresult
+NotificationTelemetryService::RecordSender(nsIPrincipal* aPrincipal)
+{
+ if (!Telemetry::CanRecordBase() || !Telemetry::CanRecordExtended() ||
+ !nsAlertsUtils::IsActionablePrincipal(aPrincipal)) {
+ return NS_OK;
+ }
+ nsAutoString origin;
+ nsresult rv = Notification::GetOrigin(aPrincipal, origin);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!mOrigins.Contains(origin)) {
+ mOrigins.PutEntry(origin);
+ Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_SENDERS, 1);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+NotificationTelemetryService::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData)
+{
+ uint32_t capability;
+ if (strcmp("perm-changed", aTopic) ||
+ !NS_strcmp(u"cleared", aData) ||
+ !GetNotificationPermission(aSubject, &capability)) {
+ return NS_OK;
+ }
+ if (!NS_strcmp(u"deleted", aData)) {
+ if (capability == nsIPermissionManager::DENY_ACTION) {
+ Telemetry::Accumulate(
+ Telemetry::WEB_NOTIFICATION_PERMISSION_REMOVED, 0);
+ } else if (capability == nsIPermissionManager::ALLOW_ACTION) {
+ Telemetry::Accumulate(
+ Telemetry::WEB_NOTIFICATION_PERMISSION_REMOVED, 1);
+ }
+ }
+ return NS_OK;
+}
+
+// Observer that the alert service calls to do common tasks and/or dispatch to the
+// specific observer for the context e.g. main thread, worker, or service worker.
+class NotificationObserver final : public nsIObserver
+{
+public:
+ nsCOMPtr<nsIObserver> mObserver;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ bool mInPrivateBrowsing;
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ NotificationObserver(nsIObserver* aObserver, nsIPrincipal* aPrincipal,
+ bool aInPrivateBrowsing)
+ : mObserver(aObserver), mPrincipal(aPrincipal),
+ mInPrivateBrowsing(aInPrivateBrowsing)
+ {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mObserver);
+ MOZ_ASSERT(mPrincipal);
+ }
+
+protected:
+ virtual ~NotificationObserver()
+ {
+ AssertIsOnMainThread();
+ }
+
+ nsresult AdjustPushQuota(const char* aTopic);
+};
+
+NS_IMPL_ISUPPORTS(NotificationObserver, nsIObserver)
+
+class MainThreadNotificationObserver : public nsIObserver
+{
+public:
+ UniquePtr<NotificationRef> mNotificationRef;
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ explicit MainThreadNotificationObserver(UniquePtr<NotificationRef> aRef)
+ : mNotificationRef(Move(aRef))
+ {
+ AssertIsOnMainThread();
+ }
+
+protected:
+ virtual ~MainThreadNotificationObserver()
+ {
+ AssertIsOnMainThread();
+ }
+};
+
+NS_IMPL_ISUPPORTS(MainThreadNotificationObserver, nsIObserver)
+
+NS_IMETHODIMP
+NotificationTask::Run()
+{
+ AssertIsOnMainThread();
+
+ // Get a pointer to notification before the notification takes ownership of
+ // the ref (it owns itself temporarily, with ShowInternal() and
+ // CloseInternal() passing on the ownership appropriately.)
+ Notification* notif = mNotificationRef->GetNotification();
+ notif->mTempRef.swap(mNotificationRef);
+ if (mAction == eShow) {
+ notif->ShowInternal();
+ } else if (mAction == eClose) {
+ notif->CloseInternal();
+ } else {
+ MOZ_CRASH("Invalid action");
+ }
+
+ MOZ_ASSERT(!mNotificationRef);
+ return NS_OK;
+}
+
+bool
+Notification::RequireInteractionEnabled(JSContext* aCx, JSObject* aOjb)
+{
+ if (NS_IsMainThread()) {
+ return Preferences::GetBool("dom.webnotifications.requireinteraction.enabled", false);
+ }
+
+ WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx);
+ if (!workerPrivate) {
+ return false;
+ }
+
+ return workerPrivate->DOMWorkerNotificationRIEnabled();
+}
+
+// static
+bool
+Notification::PrefEnabled(JSContext* aCx, JSObject* aObj)
+{
+ if (NS_IsMainThread()) {
+ return Preferences::GetBool("dom.webnotifications.enabled", false);
+ }
+
+ WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx);
+ if (!workerPrivate) {
+ return false;
+ }
+
+ if (workerPrivate->IsServiceWorker()) {
+ return workerPrivate->DOMServiceWorkerNotificationEnabled();
+ }
+
+ return workerPrivate->DOMWorkerNotificationEnabled();
+}
+
+// static
+bool
+Notification::IsGetEnabled(JSContext* aCx, JSObject* aObj)
+{
+ return NS_IsMainThread();
+}
+
+Notification::Notification(nsIGlobalObject* aGlobal, const nsAString& aID,
+ const nsAString& aTitle, const nsAString& aBody,
+ NotificationDirection aDir, const nsAString& aLang,
+ const nsAString& aTag, const nsAString& aIconUrl,
+ bool aRequireInteraction,
+ const NotificationBehavior& aBehavior)
+ : DOMEventTargetHelper(),
+ mWorkerPrivate(nullptr), mObserver(nullptr),
+ mID(aID), mTitle(aTitle), mBody(aBody), mDir(aDir), mLang(aLang),
+ mTag(aTag), mIconUrl(aIconUrl), mRequireInteraction(aRequireInteraction),
+ mBehavior(aBehavior), mData(JS::NullValue()),
+ mIsClosed(false), mIsStored(false), mTaskCount(0)
+{
+ if (NS_IsMainThread()) {
+ // We can only call this on the main thread because
+ // Event::SetEventType() called down the call chain when dispatching events
+ // using DOMEventTargetHelper::DispatchTrustedEvent() will assume the event
+ // is a main thread event if it has a valid owner. It will then attempt to
+ // fetch the atom for the event name which asserts main thread only.
+ BindToOwner(aGlobal);
+ } else {
+ mWorkerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(mWorkerPrivate);
+ }
+}
+
+nsresult
+Notification::Init()
+{
+ if (!mWorkerPrivate) {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE);
+
+ nsresult rv = obs->AddObserver(this, DOM_WINDOW_DESTROYED_TOPIC, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = obs->AddObserver(this, DOM_WINDOW_FROZEN_TOPIC, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+void
+Notification::SetAlertName()
+{
+ AssertIsOnMainThread();
+ if (!mAlertName.IsEmpty()) {
+ return;
+ }
+
+ nsAutoString alertName;
+ nsresult rv = GetOrigin(GetPrincipal(), alertName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ // Get the notification name that is unique per origin + tag/ID.
+ // The name of the alert is of the form origin#tag/ID.
+ alertName.Append('#');
+ if (!mTag.IsEmpty()) {
+ alertName.AppendLiteral("tag:");
+ alertName.Append(mTag);
+ } else {
+ alertName.AppendLiteral("notag:");
+ alertName.Append(mID);
+ }
+
+ mAlertName = alertName;
+}
+
+// May be called on any thread.
+// static
+already_AddRefed<Notification>
+Notification::Constructor(const GlobalObject& aGlobal,
+ const nsAString& aTitle,
+ const NotificationOptions& aOptions,
+ ErrorResult& aRv)
+{
+ // FIXME(nsm): If the sticky flag is set, throw an error.
+ RefPtr<ServiceWorkerGlobalScope> scope;
+ UNWRAP_OBJECT(ServiceWorkerGlobalScope, aGlobal.Get(), scope);
+ if (scope) {
+ aRv.ThrowTypeError<MSG_NOTIFICATION_NO_CONSTRUCTOR_IN_SERVICEWORKER>();
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<Notification> notification =
+ CreateAndShow(aGlobal.Context(), global, aTitle, aOptions,
+ EmptyString(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ 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,
+ const nsAString& aServiceWorkerRegistrationScope,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aGlobal);
+
+ RootedDictionary<NotificationOptions> options(RootingCx());
+ options.mDir = Notification::StringToDirection(nsString(aDir));
+ options.mLang = aLang;
+ options.mBody = aBody;
+ options.mTag = aTag;
+ options.mIcon = aIcon;
+ RefPtr<Notification> notification = CreateInternal(aGlobal, aID, aTitle,
+ options);
+
+ notification->InitFromBase64(aData, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ notification->SetScope(aServiceWorkerRegistrationScope);
+
+ return notification.forget();
+}
+
+nsresult
+Notification::PersistNotification()
+{
+ AssertIsOnMainThread();
+ nsresult rv;
+ nsCOMPtr<nsINotificationStorage> notificationStorage =
+ do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsString origin;
+ rv = GetOrigin(GetPrincipal(), origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsString id;
+ GetID(id);
+
+ nsString alertName;
+ GetAlertName(alertName);
+
+ nsAutoString behavior;
+ if (!mBehavior.ToJSON(behavior)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = notificationStorage->Put(origin,
+ id,
+ mTitle,
+ DirectionToString(mDir),
+ mLang,
+ mBody,
+ mTag,
+ mIconUrl,
+ alertName,
+ mDataAsBase64,
+ behavior,
+ mScope);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ SetStoredState(true);
+ return NS_OK;
+}
+
+void
+Notification::UnpersistNotification()
+{
+ AssertIsOnMainThread();
+ if (IsStored()) {
+ nsCOMPtr<nsINotificationStorage> notificationStorage =
+ do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID);
+ if (notificationStorage) {
+ nsString origin;
+ nsresult rv = GetOrigin(GetPrincipal(), origin);
+ if (NS_SUCCEEDED(rv)) {
+ notificationStorage->Delete(origin, mID);
+ }
+ }
+ SetStoredState(false);
+ }
+}
+
+already_AddRefed<Notification>
+Notification::CreateInternal(nsIGlobalObject* aGlobal,
+ const nsAString& aID,
+ const nsAString& aTitle,
+ const NotificationOptions& aOptions)
+{
+ nsresult rv;
+ nsString id;
+ if (!aID.IsEmpty()) {
+ id = aID;
+ } else {
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ do_GetService("@mozilla.org/uuid-generator;1");
+ NS_ENSURE_TRUE(uuidgen, nullptr);
+ nsID uuid;
+ rv = uuidgen->GenerateUUIDInPlace(&uuid);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ char buffer[NSID_LENGTH];
+ uuid.ToProvidedString(buffer);
+ NS_ConvertASCIItoUTF16 convertedID(buffer);
+ id = convertedID;
+ }
+
+ RefPtr<Notification> notification = new Notification(aGlobal, id, aTitle,
+ aOptions.mBody,
+ aOptions.mDir,
+ aOptions.mLang,
+ aOptions.mTag,
+ aOptions.mIcon,
+ aOptions.mRequireInteraction,
+ aOptions.mMozbehavior);
+ rv = notification->Init();
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ return notification.forget();
+}
+
+Notification::~Notification()
+{
+ mData.setUndefined();
+ mozilla::DropJSObjects(this);
+ AssertIsOnTargetThread();
+ MOZ_ASSERT(!mWorkerHolder);
+ MOZ_ASSERT(!mTempRef);
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(Notification)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Notification, DOMEventTargetHelper)
+ tmp->mData.setUndefined();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Notification, DOMEventTargetHelper)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(Notification, DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mData)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_ADDREF_INHERITED(Notification, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(Notification, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(Notification)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+nsIPrincipal*
+Notification::GetPrincipal()
+{
+ AssertIsOnMainThread();
+ if (mWorkerPrivate) {
+ return mWorkerPrivate->GetPrincipal();
+ } else {
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(GetOwner());
+ NS_ENSURE_TRUE(sop, nullptr);
+ return sop->GetPrincipal();
+ }
+}
+
+class WorkerNotificationObserver final : public MainThreadNotificationObserver
+{
+public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIOBSERVER
+
+ explicit WorkerNotificationObserver(UniquePtr<NotificationRef> aRef)
+ : MainThreadNotificationObserver(Move(aRef))
+ {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mNotificationRef->GetNotification()->mWorkerPrivate);
+ }
+
+ void
+ ForgetNotification()
+ {
+ AssertIsOnMainThread();
+ mNotificationRef->Forget();
+ }
+
+protected:
+ virtual ~WorkerNotificationObserver()
+ {
+ AssertIsOnMainThread();
+
+ MOZ_ASSERT(mNotificationRef);
+ Notification* notification = mNotificationRef->GetNotification();
+ if (notification) {
+ notification->mObserver = nullptr;
+ }
+ }
+};
+
+NS_IMPL_ISUPPORTS_INHERITED0(WorkerNotificationObserver, MainThreadNotificationObserver)
+
+class ServiceWorkerNotificationObserver final : public nsIObserver
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ ServiceWorkerNotificationObserver(const nsAString& aScope,
+ nsIPrincipal* aPrincipal,
+ 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)
+ : mScope(aScope), mID(aID), mPrincipal(aPrincipal), mTitle(aTitle)
+ , mDir(aDir), mLang(aLang), mBody(aBody), mTag(aTag), mIcon(aIcon)
+ , mData(aData), mBehavior(aBehavior)
+ {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aPrincipal);
+ }
+
+private:
+ ~ServiceWorkerNotificationObserver()
+ {}
+
+ const nsString mScope;
+ const nsString mID;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ const nsString mTitle;
+ const nsString mDir;
+ const nsString mLang;
+ const nsString mBody;
+ const nsString mTag;
+ const nsString mIcon;
+ const nsString mData;
+ const nsString mBehavior;
+};
+
+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;
+ RefPtr<EventTarget> target = mWorkerPrivate->GlobalScope();
+ RefPtr<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();
+ RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
+ event->InitEvent(NS_LITERAL_STRING("click"), false, true);
+ event->SetTrusted(true);
+ WantsPopupControlCheck popupControlCheck(event);
+ bool doDefaultAction = true;
+ DispatchEvent(event, &doDefaultAction);
+ return doDefaultAction;
+}
+
+// Overrides dispatch and run handlers so we can directly dispatch from main
+// thread to child workers.
+class NotificationClickWorkerRunnable final : public NotificationWorkerRunnable
+{
+ Notification* mNotification;
+ // Optional window that gets focused if click event is not
+ // preventDefault()ed.
+ nsMainThreadPtrHandle<nsPIDOMWindowInner> mWindow;
+public:
+ NotificationClickWorkerRunnable(Notification* aNotification,
+ const nsMainThreadPtrHandle<nsPIDOMWindowInner>& aWindow)
+ : NotificationWorkerRunnable(aNotification->mWorkerPrivate)
+ , mNotification(aNotification)
+ , mWindow(aWindow)
+ {
+ MOZ_ASSERT_IF(mWorkerPrivate->IsServiceWorker(), !mWindow);
+ }
+
+ void
+ WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override
+ {
+ bool doDefaultAction = mNotification->DispatchClickEvent();
+ MOZ_ASSERT_IF(mWorkerPrivate->IsServiceWorker(), !doDefaultAction);
+ if (doDefaultAction) {
+ RefPtr<FocusWindowRunnable> r = new FocusWindowRunnable(mWindow);
+ NS_DispatchToMainThread(r);
+ }
+ }
+};
+
+NS_IMETHODIMP
+NotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData)
+{
+ AssertIsOnMainThread();
+
+ if (!strcmp("alertdisablecallback", aTopic)) {
+ Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_MENU, 1);
+ if (XRE_IsParentProcess()) {
+ return Notification::RemovePermission(mPrincipal);
+ }
+ // Permissions can't be removed from the content process. Send a message
+ // to the parent; `ContentParent::RecvDisableNotifications` will call
+ // `RemovePermission`.
+ ContentChild::GetSingleton()->SendDisableNotifications(
+ IPC::Principal(mPrincipal));
+ return NS_OK;
+ } else if (!strcmp("alertclickcallback", aTopic)) {
+ Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_CLICKED, 1);
+ } else if (!strcmp("alertsettingscallback", aTopic)) {
+ Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_MENU, 2);
+ if (XRE_IsParentProcess()) {
+ return Notification::OpenSettings(mPrincipal);
+ }
+ // `ContentParent::RecvOpenNotificationSettings` notifies observers in the
+ // parent process.
+ ContentChild::GetSingleton()->SendOpenNotificationSettings(
+ IPC::Principal(mPrincipal));
+ return NS_OK;
+ } else if (!strcmp("alertshow", aTopic) ||
+ !strcmp("alertfinished", aTopic)) {
+ RefPtr<NotificationTelemetryService> telemetry =
+ NotificationTelemetryService::GetInstance();
+ if (telemetry) {
+ // Record whether "do not disturb" is supported after the first
+ // notification, to account for falling back to XUL alerts.
+ telemetry->RecordDNDSupported();
+ if (!mInPrivateBrowsing) {
+ // Ignore senders in private windows.
+ Unused << NS_WARN_IF(NS_FAILED(telemetry->RecordSender(mPrincipal)));
+ }
+ }
+ Unused << NS_WARN_IF(NS_FAILED(AdjustPushQuota(aTopic)));
+
+ if (!strcmp("alertshow", aTopic)) {
+ // Record notifications actually shown (e.g. don't count if DND is on).
+ Telemetry::Accumulate(Telemetry::WEB_NOTIFICATION_SHOWN, 1);
+ }
+ }
+
+ return mObserver->Observe(aSubject, aTopic, aData);
+}
+
+nsresult
+NotificationObserver::AdjustPushQuota(const char* aTopic)
+{
+ nsCOMPtr<nsIPushQuotaManager> pushQuotaManager =
+ do_GetService("@mozilla.org/push/Service;1");
+ if (!pushQuotaManager) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString origin;
+ nsresult rv = mPrincipal->GetOrigin(origin);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!strcmp("alertshow", aTopic)) {
+ return pushQuotaManager->NotificationForOriginShown(origin.get());
+ }
+ return pushQuotaManager->NotificationForOriginClosed(origin.get());
+}
+
+NS_IMETHODIMP
+MainThreadNotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData)
+{
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mNotificationRef);
+ Notification* notification = mNotificationRef->GetNotification();
+ MOZ_ASSERT(notification);
+ if (!strcmp("alertclickcallback", aTopic)) {
+ nsCOMPtr<nsPIDOMWindowInner> window = notification->GetOwner();
+ if (NS_WARN_IF(!window || !window->IsCurrentInnerWindow())) {
+ // Window has been closed, this observer is not valid anymore
+ return NS_ERROR_FAILURE;
+ }
+
+ bool doDefaultAction = notification->DispatchClickEvent();
+ 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);
+ }
+ }
+ } else if (!strcmp("alertfinished", aTopic)) {
+ notification->UnpersistNotification();
+ notification->mIsClosed = true;
+ notification->DispatchTrustedEvent(NS_LITERAL_STRING("close"));
+ } else if (!strcmp("alertshow", aTopic)) {
+ notification->DispatchTrustedEvent(NS_LITERAL_STRING("show"));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WorkerNotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData)
+{
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mNotificationRef);
+ // For an explanation of why it is OK to pass this rawptr to the event
+ // runnables, see the Notification class comment.
+ Notification* notification = mNotificationRef->GetNotification();
+ // We can't assert notification here since the feature could've unset it.
+ if (NS_WARN_IF(!notification)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(notification->mWorkerPrivate);
+
+ RefPtr<WorkerRunnable> r;
+ if (!strcmp("alertclickcallback", aTopic)) {
+ nsPIDOMWindowInner* window = nullptr;
+ if (!notification->mWorkerPrivate->IsServiceWorker()) {
+ WorkerPrivate* top = notification->mWorkerPrivate;
+ while (top->GetParent()) {
+ top = top->GetParent();
+ }
+
+ 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<nsPIDOMWindowInner> windowHandle(
+ new nsMainThreadPtrHolder<nsPIDOMWindowInner>(window));
+
+ r = new NotificationClickWorkerRunnable(notification, windowHandle);
+ } else if (!strcmp("alertfinished", aTopic)) {
+ notification->UnpersistNotification();
+ notification->mIsClosed = true;
+ r = new NotificationEventWorkerRunnable(notification,
+ NS_LITERAL_STRING("close"));
+ } else if (!strcmp("alertshow", aTopic)) {
+ r = new NotificationEventWorkerRunnable(notification,
+ NS_LITERAL_STRING("show"));
+ }
+
+ MOZ_ASSERT(r);
+ if (!r->Dispatch()) {
+ NS_WARNING("Could not dispatch event to worker notification");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ServiceWorkerNotificationObserver::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData)
+{
+ AssertIsOnMainThread();
+
+ nsAutoCString originSuffix;
+ nsresult rv = mPrincipal->GetOriginSuffix(originSuffix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIServiceWorkerManager> swm =
+ mozilla::services::GetServiceWorkerManager();
+ if (NS_WARN_IF(!swm)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!strcmp("alertclickcallback", aTopic)) {
+ rv = swm->SendNotificationClickEvent(originSuffix,
+ NS_ConvertUTF16toUTF8(mScope),
+ mID,
+ mTitle,
+ mDir,
+ mLang,
+ mBody,
+ mTag,
+ mIcon,
+ mData,
+ mBehavior);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ return NS_OK;
+ }
+
+ if (!strcmp("alertfinished", aTopic)) {
+ nsString origin;
+ nsresult rv = Notification::GetOrigin(mPrincipal, origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Remove closed or dismissed persistent notifications.
+ nsCOMPtr<nsINotificationStorage> notificationStorage =
+ do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID);
+ if (notificationStorage) {
+ notificationStorage->Delete(origin, mID);
+ }
+
+ rv = swm->SendNotificationCloseEvent(originSuffix,
+ NS_ConvertUTF16toUTF8(mScope),
+ mID,
+ mTitle,
+ mDir,
+ mLang,
+ mBody,
+ mTag,
+ mIcon,
+ mData,
+ mBehavior);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+bool
+Notification::IsInPrivateBrowsing()
+{
+ AssertIsOnMainThread();
+
+ nsIDocument* doc = nullptr;
+
+ if (mWorkerPrivate) {
+ doc = mWorkerPrivate->GetDocument();
+ } else if (GetOwner()) {
+ doc = GetOwner()->GetExtantDoc();
+ }
+
+ if (doc) {
+ nsCOMPtr<nsILoadContext> loadContext = doc->GetLoadContext();
+ return loadContext && loadContext->UsePrivateBrowsing();
+ }
+
+ if (mWorkerPrivate) {
+ // Not all workers may have a document, but with Bug 1107516 fixed, they
+ // should all have a loadcontext.
+ nsCOMPtr<nsILoadGroup> loadGroup = mWorkerPrivate->GetLoadGroup();
+ nsCOMPtr<nsILoadContext> loadContext;
+ NS_QueryNotificationCallbacks(nullptr, loadGroup, NS_GET_IID(nsILoadContext),
+ getter_AddRefs(loadContext));
+ return loadContext && loadContext->UsePrivateBrowsing();
+ }
+
+ //XXXnsm Should this default to true?
+ return false;
+}
+
+namespace {
+ struct StringWriteFunc : public JSONWriteFunc
+ {
+ nsAString& mBuffer; // This struct must not outlive this buffer
+ explicit StringWriteFunc(nsAString& buffer) : mBuffer(buffer) {}
+
+ void Write(const char* aStr)
+ {
+ mBuffer.Append(NS_ConvertUTF8toUTF16(aStr));
+ }
+ };
+}
+
+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);
+
+ // Transfer ownership to local scope so we can either release it at the end
+ // of this function or transfer it to the observer.
+ UniquePtr<NotificationRef> ownership;
+ mozilla::Swap(ownership, mTempRef);
+ MOZ_ASSERT(ownership->GetNotification() == this);
+
+ nsresult rv = PersistNotification();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not persist Notification");
+ }
+
+ nsCOMPtr<nsIAlertsService> alertService =
+ do_GetService(NS_ALERTSERVICE_CONTRACTID);
+
+ ErrorResult result;
+ NotificationPermission permission = NotificationPermission::Denied;
+ if (mWorkerPrivate) {
+ permission = GetPermissionInternal(mWorkerPrivate->GetPrincipal(), result);
+ } else {
+ permission = GetPermissionInternal(GetOwner(), result);
+ }
+ // We rely on GetPermissionInternal returning Denied on all failure codepaths.
+ MOZ_ASSERT_IF(result.Failed(), permission == NotificationPermission::Denied);
+ result.SuppressException();
+ if (permission != NotificationPermission::Granted || !alertService) {
+ if (mWorkerPrivate) {
+ RefPtr<NotificationEventWorkerRunnable> r =
+ new NotificationEventWorkerRunnable(this,
+ NS_LITERAL_STRING("error"));
+ if (!r->Dispatch()) {
+ NS_WARNING("Could not dispatch event to worker notification");
+ }
+ } else {
+ DispatchTrustedEvent(NS_LITERAL_STRING("error"));
+ }
+ return;
+ }
+
+ nsAutoString iconUrl;
+ nsAutoString soundUrl;
+ ResolveIconAndSoundURL(iconUrl, soundUrl);
+
+ bool isPersistent = false;
+ nsCOMPtr<nsIObserver> observer;
+ 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 MainThreadNotificationObserver(Move(ownership));
+ }
+ } else {
+ isPersistent = true;
+ // 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 NotificationObserver passed to the alert service.
+ nsAutoString behavior;
+ if (NS_WARN_IF(!mBehavior.ToJSON(behavior))) {
+ behavior.Truncate();
+ }
+ observer = new ServiceWorkerNotificationObserver(mScope,
+ GetPrincipal(),
+ mID,
+ mTitle,
+ DirectionToString(mDir),
+ mLang,
+ mBody,
+ mTag,
+ iconUrl,
+ mDataAsBase64,
+ behavior);
+ }
+ MOZ_ASSERT(observer);
+ nsCOMPtr<nsIObserver> alertObserver = new NotificationObserver(observer,
+ GetPrincipal(),
+ IsInPrivateBrowsing());
+
+
+ // In the case of IPC, the parent process uses the cookie to map to
+ // nsIObserver. Thus the cookie must be unique to differentiate observers.
+ nsString uniqueCookie = NS_LITERAL_STRING("notification:");
+ uniqueCookie.AppendInt(sCount++);
+ bool inPrivateBrowsing = IsInPrivateBrowsing();
+
+ bool requireInteraction = mRequireInteraction;
+ if (!Preferences::GetBool("dom.webnotifications.requireinteraction.enabled", false)) {
+ requireInteraction = false;
+ }
+
+ nsAutoString alertName;
+ GetAlertName(alertName);
+ nsCOMPtr<nsIAlertNotification> alert =
+ do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ NS_ENSURE_TRUE_VOID(alert);
+ nsIPrincipal* principal = GetPrincipal();
+ rv = alert->Init(alertName, iconUrl, mTitle, mBody,
+ true,
+ uniqueCookie,
+ DirectionToString(mDir),
+ mLang,
+ mDataAsBase64,
+ GetPrincipal(),
+ inPrivateBrowsing,
+ requireInteraction);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ if (isPersistent) {
+ nsAutoString persistentData;
+
+ JSONWriter w(MakeUnique<StringWriteFunc>(persistentData));
+ w.Start();
+
+ nsAutoString origin;
+ Notification::GetOrigin(principal, origin);
+ w.StringProperty("origin", NS_ConvertUTF16toUTF8(origin).get());
+
+ w.StringProperty("id", NS_ConvertUTF16toUTF8(mID).get());
+
+ nsAutoCString originSuffix;
+ principal->GetOriginSuffix(originSuffix);
+ w.StringProperty("originSuffix", originSuffix.get());
+
+ w.End();
+
+ alertService->ShowPersistentNotification(persistentData, alert, alertObserver);
+ } else {
+ alertService->ShowAlert(alert, alertObserver);
+ }
+}
+
+/* static */ bool
+Notification::RequestPermissionEnabledForScope(JSContext* aCx, JSObject* /* unused */)
+{
+ // requestPermission() is not allowed on workers. The calling page should ask
+ // for permission on the worker's behalf. This is to prevent 'which window
+ // should show the browser pop-up'. See discussion:
+ // http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2013-October/041272.html
+ return NS_IsMainThread();
+}
+
+already_AddRefed<Promise>
+Notification::RequestPermission(const GlobalObject& aGlobal,
+ const Optional<OwningNonNull<NotificationPermissionCallback> >& aCallback,
+ ErrorResult& aRv)
+{
+ // Get principal from global to make permission request for notifications.
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!sop) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(window);
+ RefPtr<Promise> promise = Promise::Create(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ NotificationPermissionCallback* permissionCallback = nullptr;
+ if (aCallback.WasPassed()) {
+ permissionCallback = &aCallback.Value();
+ }
+ nsCOMPtr<nsIRunnable> request =
+ new NotificationPermissionRequest(principal, window, promise, permissionCallback);
+
+ NS_DispatchToMainThread(request);
+ return promise.forget();
+}
+
+// static
+NotificationPermission
+Notification::GetPermission(const GlobalObject& aGlobal, ErrorResult& aRv)
+{
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ return GetPermission(global, aRv);
+}
+
+// static
+NotificationPermission
+Notification::GetPermission(nsIGlobalObject* aGlobal, ErrorResult& aRv)
+{
+ if (NS_IsMainThread()) {
+ return GetPermissionInternal(aGlobal, aRv);
+ } else {
+ WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(worker);
+ RefPtr<GetPermissionRunnable> r =
+ new GetPermissionRunnable(worker);
+ r->Dispatch(aRv);
+ if (aRv.Failed()) {
+ return NotificationPermission::Denied;
+ }
+
+ return r->GetPermission();
+ }
+}
+
+/* static */ NotificationPermission
+Notification::GetPermissionInternal(nsISupports* aGlobal, ErrorResult& aRv)
+{
+ // Get principal from global to check permission for notifications.
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aGlobal);
+ if (!sop) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return NotificationPermission::Denied;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
+ return GetPermissionInternal(principal, aRv);
+}
+
+/* static */ NotificationPermission
+Notification::GetPermissionInternal(nsIPrincipal* aPrincipal,
+ ErrorResult& aRv)
+{
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aPrincipal);
+
+ if (nsContentUtils::IsSystemPrincipal(aPrincipal)) {
+ return NotificationPermission::Granted;
+ } else {
+ // Allow files to show notifications by default.
+ nsCOMPtr<nsIURI> uri;
+ aPrincipal->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ bool isFile;
+ uri->SchemeIs("file", &isFile);
+ if (isFile) {
+ return NotificationPermission::Granted;
+ }
+ }
+ }
+
+ // We also allow notifications is they are pref'ed on.
+ if (Preferences::GetBool("notification.prompt.testing", false)) {
+ if (Preferences::GetBool("notification.prompt.testing.allow", true)) {
+ return NotificationPermission::Granted;
+ } else {
+ return NotificationPermission::Denied;
+ }
+ }
+
+ return TestPermission(aPrincipal);
+}
+
+/* static */ NotificationPermission
+Notification::TestPermission(nsIPrincipal* aPrincipal)
+{
+ AssertIsOnMainThread();
+
+ uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
+
+ nsCOMPtr<nsIPermissionManager> permissionManager =
+ services::GetPermissionManager();
+ if (!permissionManager) {
+ return NotificationPermission::Default;
+ }
+
+ permissionManager->TestExactPermissionFromPrincipal(aPrincipal,
+ "desktop-notification",
+ &permission);
+
+ // Convert the result to one of the enum types.
+ switch (permission) {
+ case nsIPermissionManager::ALLOW_ACTION:
+ return NotificationPermission::Granted;
+ case nsIPermissionManager::DENY_ACTION:
+ return NotificationPermission::Denied;
+ default:
+ return NotificationPermission::Default;
+ }
+}
+
+nsresult
+Notification::ResolveIconAndSoundURL(nsString& iconUrl, nsString& soundUrl)
+{
+ AssertIsOnMainThread();
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIURI> baseUri;
+
+ // XXXnsm If I understand correctly, the character encoding for resolving
+ // URIs in new specs is dictated by the URL spec, which states that unless
+ // the URL parser is passed an override encoding, the charset to be used is
+ // UTF-8. The new Notification icon/sound specification just says to use the
+ // Fetch API, where the Request constructor defers to URL parsing specifying
+ // the API base URL and no override encoding. So we've to use UTF-8 on
+ // workers, but for backwards compat keeping it document charset on main
+ // thread.
+ const char* charset = "UTF-8";
+
+ if (mWorkerPrivate) {
+ baseUri = mWorkerPrivate->GetBaseURI();
+ } else {
+ nsIDocument* doc = GetOwner() ? GetOwner()->GetExtantDoc() : nullptr;
+ if (doc) {
+ baseUri = doc->GetBaseURI();
+ charset = doc->GetDocumentCharacterSet().get();
+ } else {
+ NS_WARNING("No document found for main thread notification!");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (baseUri) {
+ if (mIconUrl.Length() > 0) {
+ nsCOMPtr<nsIURI> srcUri;
+ rv = NS_NewURI(getter_AddRefs(srcUri), mIconUrl, charset, baseUri);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString src;
+ srcUri->GetSpec(src);
+ iconUrl = NS_ConvertUTF8toUTF16(src);
+ }
+ }
+ if (mBehavior.mSoundFile.Length() > 0) {
+ nsCOMPtr<nsIURI> srcUri;
+ rv = NS_NewURI(getter_AddRefs(srcUri), mBehavior.mSoundFile, charset, baseUri);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString src;
+ srcUri->GetSpec(src);
+ soundUrl = NS_ConvertUTF8toUTF16(src);
+ }
+ }
+ }
+
+ return rv;
+}
+
+already_AddRefed<Promise>
+Notification::Get(nsPIDOMWindowInner* aWindow,
+ const GetNotificationOptions& aFilter,
+ const nsAString& aScope,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aWindow);
+
+ nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc();
+ if (!doc) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ nsString origin;
+ aRv = GetOrigin(doc->NodePrincipal(), origin);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aWindow);
+ RefPtr<Promise> promise = Promise::Create(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsINotificationStorageCallback> callback =
+ new NotificationStorageCallback(global, aScope, promise);
+
+ RefPtr<NotificationGetRunnable> r =
+ new NotificationGetRunnable(origin, aFilter.mTag, callback);
+
+ aRv = NS_DispatchToMainThread(r);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise>
+Notification::Get(const GlobalObject& aGlobal,
+ const GetNotificationOptions& aFilter,
+ ErrorResult& aRv)
+{
+ AssertIsOnMainThread();
+ nsCOMPtr<nsIGlobalObject> global =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ MOZ_ASSERT(global);
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global);
+
+ return Get(window, aFilter, EmptyString(), aRv);
+}
+
+class WorkerGetResultRunnable final : public NotificationWorkerRunnable
+{
+ RefPtr<PromiseWorkerProxy> mPromiseProxy;
+ const nsTArray<NotificationStrings> mStrings;
+public:
+ WorkerGetResultRunnable(WorkerPrivate* aWorkerPrivate,
+ PromiseWorkerProxy* aPromiseProxy,
+ const nsTArray<NotificationStrings>&& aStrings)
+ : NotificationWorkerRunnable(aWorkerPrivate)
+ , mPromiseProxy(aPromiseProxy)
+ , mStrings(Move(aStrings))
+ {
+ }
+
+ void
+ WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override
+ {
+ RefPtr<Promise> workerPromise = mPromiseProxy->WorkerPromise();
+
+ ErrorResult result;
+ AutoTArray<RefPtr<Notification>, 5> notifications;
+ for (uint32_t i = 0; i < mStrings.Length(); ++i) {
+ RefPtr<Notification> n =
+ Notification::ConstructFromFields(aWorkerPrivate->GlobalScope(),
+ mStrings[i].mID,
+ mStrings[i].mTitle,
+ mStrings[i].mDir,
+ mStrings[i].mLang,
+ mStrings[i].mBody,
+ mStrings[i].mTag,
+ mStrings[i].mIcon,
+ mStrings[i].mData,
+ /* mStrings[i].mBehavior, not
+ * supported */
+ mStrings[i].mServiceWorkerRegistrationScope,
+ result);
+
+ n->SetStoredState(true);
+ Unused << NS_WARN_IF(result.Failed());
+ if (!result.Failed()) {
+ notifications.AppendElement(n.forget());
+ }
+ }
+
+ workerPromise->MaybeResolve(notifications);
+ mPromiseProxy->CleanUp();
+ }
+};
+
+class WorkerGetCallback final : public ScopeCheckingGetCallback
+{
+ RefPtr<PromiseWorkerProxy> mPromiseProxy;
+public:
+ NS_DECL_ISUPPORTS
+
+ WorkerGetCallback(PromiseWorkerProxy* aProxy, const nsAString& aScope)
+ : ScopeCheckingGetCallback(aScope), mPromiseProxy(aProxy)
+ {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(aProxy);
+ }
+
+ NS_IMETHOD Done() final
+ {
+ AssertIsOnMainThread();
+ MOZ_ASSERT(mPromiseProxy, "Was Done() called twice?");
+
+ RefPtr<PromiseWorkerProxy> proxy = mPromiseProxy.forget();
+ MutexAutoLock lock(proxy->Lock());
+ if (proxy->CleanedUp()) {
+ return NS_OK;
+ }
+
+ RefPtr<WorkerGetResultRunnable> r =
+ new WorkerGetResultRunnable(proxy->GetWorkerPrivate(),
+ proxy,
+ Move(mStrings));
+
+ r->Dispatch();
+ return NS_OK;
+ }
+
+private:
+ ~WorkerGetCallback()
+ {}
+};
+
+NS_IMPL_ISUPPORTS(WorkerGetCallback, nsINotificationStorageCallback)
+
+class WorkerGetRunnable final : public Runnable
+{
+ RefPtr<PromiseWorkerProxy> mPromiseProxy;
+ const nsString mTag;
+ const nsString mScope;
+public:
+ WorkerGetRunnable(PromiseWorkerProxy* aProxy,
+ const nsAString& aTag,
+ const nsAString& aScope)
+ : mPromiseProxy(aProxy), mTag(aTag), mScope(aScope)
+ {
+ MOZ_ASSERT(mPromiseProxy);
+ }
+
+ NS_IMETHOD
+ Run() override
+ {
+ AssertIsOnMainThread();
+ nsCOMPtr<nsINotificationStorageCallback> callback =
+ new WorkerGetCallback(mPromiseProxy, mScope);
+
+ nsresult rv;
+ nsCOMPtr<nsINotificationStorage> notificationStorage =
+ do_GetService(NS_NOTIFICATION_STORAGE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ callback->Done();
+ return rv;
+ }
+
+ MutexAutoLock lock(mPromiseProxy->Lock());
+ if (mPromiseProxy->CleanedUp()) {
+ return NS_OK;
+ }
+
+ nsString origin;
+ rv =
+ Notification::GetOrigin(mPromiseProxy->GetWorkerPrivate()->GetPrincipal(),
+ origin);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ callback->Done();
+ return rv;
+ }
+
+ rv = notificationStorage->Get(origin, mTag, callback);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ callback->Done();
+ return rv;
+ }
+
+ return NS_OK;
+ }
+private:
+ ~WorkerGetRunnable()
+ {}
+};
+
+already_AddRefed<Promise>
+Notification::WorkerGet(WorkerPrivate* aWorkerPrivate,
+ const GetNotificationOptions& aFilter,
+ const nsAString& aScope,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ RefPtr<Promise> p = Promise::Create(aWorkerPrivate->GlobalScope(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ RefPtr<PromiseWorkerProxy> proxy =
+ PromiseWorkerProxy::Create(aWorkerPrivate, p);
+ if (!proxy) {
+ aRv.Throw(NS_ERROR_DOM_ABORT_ERR);
+ return nullptr;
+ }
+
+ RefPtr<WorkerGetRunnable> r =
+ new WorkerGetRunnable(proxy, aFilter.mTag, aScope);
+ // Since this is called from script via
+ // ServiceWorkerRegistration::GetNotifications, we can assert dispatch.
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r));
+ return p.forget();
+}
+
+JSObject*
+Notification::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return mozilla::dom::NotificationBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+Notification::Close()
+{
+ AssertIsOnTargetThread();
+ auto ref = MakeUnique<NotificationRef>(this);
+ if (!ref->Initialized()) {
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> closeNotificationTask =
+ new NotificationTask(Move(ref), NotificationTask::eClose);
+ nsresult rv = NS_DispatchToMainThread(closeNotificationTask);
+
+ if (NS_FAILED(rv)) {
+ DispatchTrustedEvent(NS_LITERAL_STRING("error"));
+ // If dispatch fails, NotificationTask will release the ref when it goes
+ // out of scope at the end of this function.
+ }
+}
+
+void
+Notification::CloseInternal()
+{
+ AssertIsOnMainThread();
+ // Transfer ownership (if any) to local scope so we can release it at the end
+ // of this function. This is relevant when the call is from
+ // NotificationTask::Run().
+ UniquePtr<NotificationRef> ownership;
+ mozilla::Swap(ownership, mTempRef);
+
+ SetAlertName();
+ UnpersistNotification();
+ if (!mIsClosed) {
+ nsCOMPtr<nsIAlertsService> alertService =
+ do_GetService(NS_ALERTSERVICE_CONTRACTID);
+ if (alertService) {
+ nsAutoString alertName;
+ GetAlertName(alertName);
+ alertService->CloseAlert(alertName, GetPrincipal());
+ }
+ }
+}
+
+nsresult
+Notification::GetOrigin(nsIPrincipal* aPrincipal, nsString& aOrigin)
+{
+ if (!aPrincipal) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = nsContentUtils::GetUTFOrigin(aPrincipal, aOrigin);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+bool
+Notification::RequireInteraction() const
+{
+ return mRequireInteraction;
+}
+
+void
+Notification::GetData(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetval)
+{
+ if (mData.isNull() && !mDataAsBase64.IsEmpty()) {
+ nsresult rv;
+ RefPtr<nsStructuredCloneContainer> container =
+ new nsStructuredCloneContainer();
+ rv = container->InitFromBase64(mDataAsBase64, JS_STRUCTURED_CLONE_VERSION);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRetval.setNull();
+ return;
+ }
+
+ JS::Rooted<JS::Value> data(aCx);
+ rv = container->DeserializeToJsval(aCx, &data);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRetval.setNull();
+ return;
+ }
+
+ if (data.isGCThing()) {
+ mozilla::HoldJSObjects(this);
+ }
+ mData = data;
+ }
+ if (mData.isNull()) {
+ aRetval.setNull();
+ return;
+ }
+
+ aRetval.set(mData);
+}
+
+void
+Notification::InitFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aData,
+ ErrorResult& aRv)
+{
+ if (!mDataAsBase64.IsEmpty() || aData.isNull()) {
+ return;
+ }
+ RefPtr<nsStructuredCloneContainer> dataObjectContainer =
+ new nsStructuredCloneContainer();
+ aRv = dataObjectContainer->InitFromJSVal(aData, aCx);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ dataObjectContainer->GetDataAsBase64(mDataAsBase64);
+}
+
+void Notification::InitFromBase64(const nsAString& aData, ErrorResult& aRv)
+{
+ if (!mDataAsBase64.IsEmpty() || aData.IsEmpty()) {
+ return;
+ }
+
+ // To and fro to ensure it is valid base64.
+ RefPtr<nsStructuredCloneContainer> container =
+ new nsStructuredCloneContainer();
+ aRv = container->InitFromBase64(aData, JS_STRUCTURED_CLONE_VERSION);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ container->GetDataAsBase64(mDataAsBase64);
+}
+
+bool
+Notification::AddRefObject()
+{
+ AssertIsOnTargetThread();
+ MOZ_ASSERT_IF(mWorkerPrivate && !mWorkerHolder, mTaskCount == 0);
+ MOZ_ASSERT_IF(mWorkerPrivate && mWorkerHolder, mTaskCount > 0);
+ if (mWorkerPrivate && !mWorkerHolder) {
+ if (!RegisterWorkerHolder()) {
+ return false;
+ }
+ }
+ AddRef();
+ ++mTaskCount;
+ return true;
+}
+
+void
+Notification::ReleaseObject()
+{
+ AssertIsOnTargetThread();
+ MOZ_ASSERT(mTaskCount > 0);
+ MOZ_ASSERT_IF(mWorkerPrivate, mWorkerHolder);
+
+ --mTaskCount;
+ if (mWorkerPrivate && mTaskCount == 0) {
+ UnregisterWorkerHolder();
+ }
+ Release();
+}
+
+NotificationWorkerHolder::NotificationWorkerHolder(Notification* aNotification)
+ : mNotification(aNotification)
+{
+ MOZ_ASSERT(mNotification->mWorkerPrivate);
+ mNotification->mWorkerPrivate->AssertIsOnWorkerThread();
+}
+
+/*
+ * Called from the worker, runs on main thread, blocks worker.
+ *
+ * We can freely access mNotification here because the feature supplied it and
+ * the Notification owns the feature.
+ */
+class CloseNotificationRunnable final
+ : public WorkerMainThreadRunnable
+{
+ Notification* mNotification;
+ bool mHadObserver;
+
+ public:
+ explicit CloseNotificationRunnable(Notification* aNotification)
+ : WorkerMainThreadRunnable(aNotification->mWorkerPrivate,
+ NS_LITERAL_CSTRING("Notification :: Close Notification"))
+ , mNotification(aNotification)
+ , mHadObserver(false)
+ {}
+
+ bool
+ MainThreadRun() override
+ {
+ if (mNotification->mObserver) {
+ // The Notify() take's responsibility of releasing the Notification.
+ mNotification->mObserver->ForgetNotification();
+ mNotification->mObserver = nullptr;
+ mHadObserver = true;
+ }
+ mNotification->CloseInternal();
+ return true;
+ }
+
+ bool
+ HadObserver()
+ {
+ return mHadObserver;
+ }
+};
+
+bool
+NotificationWorkerHolder::Notify(Status aStatus)
+{
+ if (aStatus >= Canceling) {
+ // CloseNotificationRunnable blocks the worker by pushing a sync event loop
+ // on the stack. Meanwhile, WorkerControlRunnables dispatched to the worker
+ // can still continue running. One of these is
+ // ReleaseNotificationControlRunnable that releases the notification,
+ // invalidating the notification and this feature. We hold this reference to
+ // keep the notification valid until we are done with it.
+ //
+ // An example of when the control runnable could get dispatched to the
+ // worker is if a Notification is created and the worker is immediately
+ // closed, but there is no permission to show it so that the main thread
+ // immediately drops the NotificationRef. In this case, this function blocks
+ // on the main thread, but the main thread dispatches the control runnable,
+ // invalidating mNotification.
+ RefPtr<Notification> kungFuDeathGrip = mNotification;
+
+ // Dispatched to main thread, blocks on closing the Notification.
+ RefPtr<CloseNotificationRunnable> r =
+ new CloseNotificationRunnable(kungFuDeathGrip);
+ ErrorResult rv;
+ r->Dispatch(rv);
+ // XXXbz I'm told throwing and returning false from here is pointless (and
+ // also that doing sync stuff from here is really weird), so I guess we just
+ // suppress the exception on rv, if any.
+ rv.SuppressException();
+
+ // Only call ReleaseObject() to match the observer's NotificationRef
+ // ownership (since CloseNotificationRunnable asked the observer to drop the
+ // reference to the notification).
+ if (r->HadObserver()) {
+ kungFuDeathGrip->ReleaseObject();
+ }
+
+ // From this point we cannot touch properties of this feature because
+ // ReleaseObject() may have led to the notification going away and the
+ // notification owns this feature!
+ }
+ return true;
+}
+
+bool
+Notification::RegisterWorkerHolder()
+{
+ MOZ_ASSERT(mWorkerPrivate);
+ mWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(!mWorkerHolder);
+ mWorkerHolder = MakeUnique<NotificationWorkerHolder>(this);
+ if (NS_WARN_IF(!mWorkerHolder->HoldWorker(mWorkerPrivate, Canceling))) {
+ return false;
+ }
+
+ return true;
+}
+
+void
+Notification::UnregisterWorkerHolder()
+{
+ MOZ_ASSERT(mWorkerPrivate);
+ mWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(mWorkerHolder);
+ mWorkerHolder = nullptr;
+}
+
+/*
+ * Checks:
+ * 1) Is aWorker allowed to show a notification for scope?
+ * 2) Is aWorker an active worker?
+ *
+ * If it is not an active worker, Result() will be NS_ERROR_NOT_AVAILABLE.
+ */
+class CheckLoadRunnable final : public WorkerMainThreadRunnable
+{
+ nsresult mRv;
+ nsCString mScope;
+
+public:
+ explicit CheckLoadRunnable(WorkerPrivate* aWorker, const nsACString& aScope)
+ : WorkerMainThreadRunnable(aWorker,
+ NS_LITERAL_CSTRING("Notification :: Check Load"))
+ , mRv(NS_ERROR_DOM_SECURITY_ERR)
+ , mScope(aScope)
+ { }
+
+ bool
+ MainThreadRun() override
+ {
+ nsIPrincipal* principal = mWorkerPrivate->GetPrincipal();
+ mRv = CheckScope(principal, mScope);
+
+ if (NS_FAILED(mRv)) {
+ return true;
+ }
+
+ RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+ if (!swm) {
+ // browser shutdown began
+ mRv = NS_ERROR_FAILURE;
+ return true;
+ }
+
+ RefPtr<ServiceWorkerRegistrationInfo> registration =
+ swm->GetRegistration(principal, mScope);
+
+ // This is coming from a ServiceWorkerRegistration.
+ MOZ_ASSERT(registration);
+
+ if (!registration->GetActive() ||
+ registration->GetActive()->ID() != mWorkerPrivate->ServiceWorkerID()) {
+ mRv = NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return true;
+ }
+
+ nsresult
+ Result()
+ {
+ return mRv;
+ }
+
+};
+
+/* static */
+already_AddRefed<Promise>
+Notification::ShowPersistentNotification(JSContext* aCx,
+ nsIGlobalObject *aGlobal,
+ const nsAString& aScope,
+ const nsAString& aTitle,
+ const NotificationOptions& aOptions,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aGlobal);
+
+ // Validate scope.
+ // XXXnsm: This may be slow due to blocking the worker and waiting on the main
+ // thread. On calls from content, we can be sure the scope is valid since
+ // ServiceWorkerRegistrations have their scope set correctly. Can this be made
+ // debug only? The problem is that there would be different semantics in
+ // debug and non-debug builds in such a case.
+ if (NS_IsMainThread()) {
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aGlobal);
+ if (NS_WARN_IF(!sop)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ nsIPrincipal* principal = sop->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ aRv = CheckScope(principal, NS_ConvertUTF16toUTF8(aScope));
+ if (NS_WARN_IF(aRv.Failed())) {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ return nullptr;
+ }
+ } else {
+ WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(worker);
+ worker->AssertIsOnWorkerThread();
+ RefPtr<CheckLoadRunnable> loadChecker =
+ new CheckLoadRunnable(worker, NS_ConvertUTF16toUTF8(aScope));
+ loadChecker->Dispatch(aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(loadChecker->Result()))) {
+ if (loadChecker->Result() == NS_ERROR_NOT_AVAILABLE) {
+ aRv.ThrowTypeError<MSG_NO_ACTIVE_WORKER>(aScope);
+ } else {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ }
+ return nullptr;
+ }
+ }
+
+
+ RefPtr<Promise> p = Promise::Create(aGlobal, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // We check permission here rather than pass the Promise to NotificationTask
+ // which leads to uglier code.
+ NotificationPermission permission = GetPermission(aGlobal, aRv);
+
+ // "If permission for notification's origin is not "granted", reject promise with a TypeError exception, and terminate these substeps."
+ if (NS_WARN_IF(aRv.Failed()) || permission == NotificationPermission::Denied) {
+ ErrorResult result;
+ result.ThrowTypeError<MSG_NOTIFICATION_PERMISSION_DENIED>();
+ p->MaybeReject(result);
+ return p.forget();
+ }
+
+ // "Otherwise, resolve promise with undefined."
+ // The Notification may still not be shown due to other errors, but the spec
+ // is not concerned with those.
+ p->MaybeResolveWithUndefined();
+
+ RefPtr<Notification> notification =
+ CreateAndShow(aCx, aGlobal, aTitle, aOptions, aScope, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return p.forget();
+}
+
+/* static */ already_AddRefed<Notification>
+Notification::CreateAndShow(JSContext* aCx,
+ nsIGlobalObject* aGlobal,
+ const nsAString& aTitle,
+ const NotificationOptions& aOptions,
+ const nsAString& aScope,
+ ErrorResult& aRv)
+{
+ MOZ_ASSERT(aGlobal);
+
+ RefPtr<Notification> notification = CreateInternal(aGlobal, EmptyString(),
+ aTitle, aOptions);
+
+ // Make a structured clone of the aOptions.mData object
+ JS::Rooted<JS::Value> data(aCx, aOptions.mData);
+ notification->InitFromJSVal(aCx, data, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ notification->SetScope(aScope);
+
+ auto ref = MakeUnique<NotificationRef>(notification);
+ if (NS_WARN_IF(!ref->Initialized())) {
+ aRv.Throw(NS_ERROR_DOM_ABORT_ERR);
+ return nullptr;
+ }
+
+ // Queue a task to show the notification.
+ nsCOMPtr<nsIRunnable> showNotificationTask =
+ new NotificationTask(Move(ref), NotificationTask::eShow);
+ nsresult rv = NS_DispatchToMainThread(showNotificationTask);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ notification->DispatchTrustedEvent(NS_LITERAL_STRING("error"));
+ }
+
+ return notification.forget();
+}
+
+/* static */ nsresult
+Notification::RemovePermission(nsIPrincipal* aPrincipal)
+{
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsCOMPtr<nsIPermissionManager> permissionManager =
+ mozilla::services::GetPermissionManager();
+ if (!permissionManager) {
+ return NS_ERROR_FAILURE;
+ }
+ permissionManager->RemoveFromPrincipal(aPrincipal, "desktop-notification");
+ return NS_OK;
+}
+
+/* static */ nsresult
+Notification::OpenSettings(nsIPrincipal* aPrincipal)
+{
+ MOZ_ASSERT(XRE_IsParentProcess());
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (!obs) {
+ return NS_ERROR_FAILURE;
+ }
+ // Notify other observers so they can show settings UI.
+ obs->NotifyObservers(aPrincipal, "notifications-open-settings", nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Notification::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData)
+{
+ AssertIsOnMainThread();
+
+ if (!strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) ||
+ !strcmp(aTopic, DOM_WINDOW_FROZEN_TOPIC)) {
+
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ if (SameCOMIdentity(aSubject, window)) {
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, DOM_WINDOW_DESTROYED_TOPIC);
+ obs->RemoveObserver(this, DOM_WINDOW_FROZEN_TOPIC);
+ }
+
+ CloseInternal();
+ }
+ }
+
+ return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
+
diff --git a/dom/notification/Notification.h b/dom/notification/Notification.h
new file mode 100644
index 000000000..a2c4b5c68
--- /dev/null
+++ b/dom/notification/Notification.h
@@ -0,0 +1,471 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- *//* vim: set ts=8 sts=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/. */
+
+#ifndef mozilla_dom_notification_h__
+#define mozilla_dom_notification_h__
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/NotificationBinding.h"
+#include "mozilla/dom/workers/bindings/WorkerHolder.h"
+
+#include "nsIObserver.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsHashKeys.h"
+#include "nsTHashtable.h"
+#include "nsWeakReference.h"
+
+#define NOTIFICATIONTELEMETRYSERVICE_CONTRACTID \
+ "@mozilla.org/notificationTelemetryService;1"
+
+class nsIPrincipal;
+class nsIVariant;
+
+namespace mozilla {
+namespace dom {
+
+class NotificationRef;
+class WorkerNotificationObserver;
+class Promise;
+
+namespace workers {
+ class WorkerPrivate;
+} // namespace workers
+
+class Notification;
+class NotificationWorkerHolder final : public workers::WorkerHolder
+{
+ // Since the feature is strongly held by a Notification, it is ok to hold
+ // a raw pointer here.
+ Notification* mNotification;
+
+public:
+ explicit NotificationWorkerHolder(Notification* aNotification);
+
+ bool
+ Notify(workers::Status aStatus) override;
+};
+
+// Records telemetry probes at application startup, when a notification is
+// shown, and when the notification permission is revoked for a site.
+class NotificationTelemetryService final : public nsIObserver
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ NotificationTelemetryService();
+
+ static already_AddRefed<NotificationTelemetryService> GetInstance();
+
+ nsresult Init();
+ void RecordDNDSupported();
+ void RecordPermissions();
+ nsresult RecordSender(nsIPrincipal* aPrincipal);
+
+private:
+ virtual ~NotificationTelemetryService();
+
+ nsresult AddPermissionChangeObserver();
+ nsresult RemovePermissionChangeObserver();
+
+ bool GetNotificationPermission(nsISupports* aSupports,
+ uint32_t* aCapability);
+
+ bool mDNDRecorded;
+ nsTHashtable<nsStringHashKey> mOrigins;
+};
+
+/*
+ * Notifications on workers introduce some lifetime issues. The property we
+ * are trying to satisfy is:
+ * Whenever a task is dispatched to the main thread to operate on
+ * a Notification, the Notification should be addrefed on the worker thread
+ * and a feature should be added to observe the worker lifetime. This main
+ * thread owner should ensure it properly releases the reference to the
+ * Notification, additionally removing the feature if necessary.
+ *
+ * To enforce the correct addref and release, along with managing the feature,
+ * we introduce a NotificationRef. Only one object may ever own
+ * a NotificationRef, so UniquePtr<> is used throughout. The NotificationRef
+ * constructor calls AddRefObject(). When it is destroyed (on any thread) it
+ * releases the Notification on the correct thread.
+ *
+ * Code should only access the underlying Notification object when it can
+ * guarantee that it retains ownership of the NotificationRef while doing so.
+ *
+ * The one kink in this mechanism is that the worker feature may be Notify()ed
+ * if the worker stops running script, even if the Notification's corresponding
+ * UI is still visible to the user. We handle this case with the following
+ * steps:
+ * a) Close the notification. This is done by blocking the worker on the main
+ * thread. This ensures that there are no main thread holders when the worker
+ * resumes. This also deals with the case where Notify() runs on the worker
+ * before the observer has been created on the main thread. Even in such
+ * a situation, the CloseNotificationRunnable() will only run after the
+ * Show task that was previously queued. Since the show task is only queued
+ * once when the Notification is created, we can be sure that no new tasks
+ * will follow the Notify().
+ *
+ * b) Ask the observer to let go of its NotificationRef's underlying
+ * Notification without proper cleanup since the feature will handle the
+ * release. This is only OK because every notification has only one
+ * associated observer. The NotificationRef itself is still owned by the
+ * observer and deleted by the UniquePtr, but it doesn't do anything since
+ * the underlying Notification is null.
+ *
+ * To unify code-paths, we use the same NotificationRef in the main
+ * thread implementation too.
+ *
+ * Note that the Notification's JS wrapper does it's standard
+ * AddRef()/Release() and is not affected by any of this.
+ *
+ * Since the worker event queue can have runnables that will dispatch events on
+ * the Notification, the NotificationRef destructor will first try to release
+ * the Notification by dispatching a normal runnable to the worker so that it is
+ * queued after any event runnables. If that dispatch fails, it means the worker
+ * is no longer running and queued WorkerRunnables will be canceled, so we
+ * dispatch a control runnable instead.
+ *
+ */
+class Notification : public DOMEventTargetHelper
+ , public nsIObserver
+ , public nsSupportsWeakReference
+{
+ friend class CloseNotificationRunnable;
+ friend class NotificationTask;
+ friend class NotificationPermissionRequest;
+ friend class MainThreadNotificationObserver;
+ friend class NotificationStorageCallback;
+ friend class ServiceWorkerNotificationObserver;
+ friend class WorkerGetRunnable;
+ friend class WorkerNotificationObserver;
+ friend class NotificationTelemetryService;
+
+public:
+ IMPL_EVENT_HANDLER(click)
+ IMPL_EVENT_HANDLER(show)
+ IMPL_EVENT_HANDLER(error)
+ IMPL_EVENT_HANDLER(close)
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(Notification, DOMEventTargetHelper)
+ NS_DECL_NSIOBSERVER
+
+ static bool RequireInteractionEnabled(JSContext* aCx, JSObject* aObj);
+ 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,
+ const nsAString& aServiceWorkerRegistrationScope,
+ ErrorResult& aRv);
+
+ void GetID(nsAString& aRetval) {
+ aRetval = mID;
+ }
+
+ void GetTitle(nsAString& aRetval)
+ {
+ aRetval = mTitle;
+ }
+
+ NotificationDirection Dir()
+ {
+ return mDir;
+ }
+
+ void GetLang(nsAString& aRetval)
+ {
+ aRetval = mLang;
+ }
+
+ void GetBody(nsAString& aRetval)
+ {
+ aRetval = mBody;
+ }
+
+ void GetTag(nsAString& aRetval)
+ {
+ aRetval = mTag;
+ }
+
+ void GetIcon(nsAString& aRetval)
+ {
+ aRetval = mIconUrl;
+ }
+
+ void SetStoredState(bool val)
+ {
+ mIsStored = val;
+ }
+
+ bool IsStored()
+ {
+ return mIsStored;
+ }
+
+ static bool RequestPermissionEnabledForScope(JSContext* aCx, JSObject* /* unused */);
+
+ static already_AddRefed<Promise>
+ RequestPermission(const GlobalObject& aGlobal,
+ const Optional<OwningNonNull<NotificationPermissionCallback> >& aCallback,
+ ErrorResult& aRv);
+
+ static NotificationPermission GetPermission(const GlobalObject& aGlobal,
+ ErrorResult& aRv);
+
+ static already_AddRefed<Promise>
+ Get(nsPIDOMWindowInner* aWindow,
+ const GetNotificationOptions& aFilter,
+ const nsAString& aScope,
+ ErrorResult& aRv);
+
+ static already_AddRefed<Promise> Get(const GlobalObject& aGlobal,
+ const GetNotificationOptions& aFilter,
+ ErrorResult& aRv);
+
+ static already_AddRefed<Promise> WorkerGet(workers::WorkerPrivate* aWorkerPrivate,
+ const GetNotificationOptions& aFilter,
+ const nsAString& aScope,
+ ErrorResult& aRv);
+
+ // Notification implementation of
+ // ServiceWorkerRegistration.showNotification.
+ //
+ //
+ // Note that aCx may not be in the compartment of aGlobal, but aOptions will
+ // have its JS things in the compartment of aCx.
+ static already_AddRefed<Promise>
+ ShowPersistentNotification(JSContext* aCx,
+ nsIGlobalObject* aGlobal,
+ const nsAString& aScope,
+ const nsAString& aTitle,
+ const NotificationOptions& aOptions,
+ ErrorResult& aRv);
+
+ void Close();
+
+ nsPIDOMWindowInner* GetParentObject()
+ {
+ return GetOwner();
+ }
+
+ virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+ bool RequireInteraction() const;
+
+ void GetData(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval);
+
+ void InitFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aData, ErrorResult& aRv);
+
+ void InitFromBase64(const nsAString& aData, ErrorResult& aRv);
+
+ void AssertIsOnTargetThread() const
+ {
+ MOZ_ASSERT(IsTargetThread());
+ }
+
+ // Initialized on the worker thread, never unset, and always used in
+ // a read-only capacity. Used on any thread.
+ workers::WorkerPrivate* mWorkerPrivate;
+
+ // Main thread only.
+ WorkerNotificationObserver* mObserver;
+
+ // The NotificationTask calls ShowInternal()/CloseInternal() on the
+ // Notification. At this point the task has ownership of the Notification. It
+ // passes this on to the Notification itself via mTempRef so that
+ // ShowInternal()/CloseInternal() may pass it along appropriately (or release
+ // it).
+ //
+ // Main thread only.
+ UniquePtr<NotificationRef> mTempRef;
+
+ // Returns true if addref succeeded.
+ bool AddRefObject();
+ void ReleaseObject();
+
+ static NotificationPermission GetPermission(nsIGlobalObject* aGlobal,
+ ErrorResult& aRv);
+
+ static NotificationPermission GetPermissionInternal(nsIPrincipal* aPrincipal,
+ ErrorResult& rv);
+
+ static NotificationPermission TestPermission(nsIPrincipal* aPrincipal);
+
+ bool DispatchClickEvent();
+ bool DispatchNotificationClickEvent();
+
+ static nsresult RemovePermission(nsIPrincipal* aPrincipal);
+ static nsresult OpenSettings(nsIPrincipal* aPrincipal);
+protected:
+ Notification(nsIGlobalObject* aGlobal, const nsAString& aID,
+ const nsAString& aTitle, const nsAString& aBody,
+ NotificationDirection aDir, const nsAString& aLang,
+ const nsAString& aTag, const nsAString& aIconUrl,
+ bool aRequireNotification,
+ const NotificationBehavior& aBehavior);
+
+ static already_AddRefed<Notification> CreateInternal(nsIGlobalObject* aGlobal,
+ const nsAString& aID,
+ const nsAString& aTitle,
+ const NotificationOptions& aOptions);
+
+ nsresult Init();
+ bool IsInPrivateBrowsing();
+ void ShowInternal();
+ void CloseInternal();
+
+ static NotificationPermission GetPermissionInternal(nsISupports* aGlobal,
+ ErrorResult& rv);
+
+ static const nsString DirectionToString(NotificationDirection aDirection)
+ {
+ switch (aDirection) {
+ case NotificationDirection::Ltr:
+ return NS_LITERAL_STRING("ltr");
+ case NotificationDirection::Rtl:
+ return NS_LITERAL_STRING("rtl");
+ default:
+ return NS_LITERAL_STRING("auto");
+ }
+ }
+
+ static NotificationDirection StringToDirection(const nsAString& aDirection)
+ {
+ if (aDirection.EqualsLiteral("ltr")) {
+ return NotificationDirection::Ltr;
+ }
+ if (aDirection.EqualsLiteral("rtl")) {
+ return NotificationDirection::Rtl;
+ }
+ return NotificationDirection::Auto;
+ }
+
+ static nsresult GetOrigin(nsIPrincipal* aPrincipal, nsString& aOrigin);
+
+ void GetAlertName(nsAString& aRetval)
+ {
+ 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;
+ const bool mRequireInteraction;
+ nsString mDataAsBase64;
+ const NotificationBehavior mBehavior;
+
+ // It's null until GetData is first called
+ JS::Heap<JS::Value> 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.
+ bool mIsStored;
+
+ static uint32_t sCount;
+
+private:
+ virtual ~Notification();
+
+ // Creates a Notification and shows it. Returns a reference to the
+ // Notification if result is NS_OK. The lifetime of this Notification is tied
+ // to an underlying NotificationRef. Do not hold a non-stack raw pointer to
+ // it. Be careful about thread safety if acquiring a strong reference.
+ //
+ // Note that aCx may not be in the compartment of aGlobal, but aOptions will
+ // have its JS things in the compartment of aCx.
+ static already_AddRefed<Notification>
+ CreateAndShow(JSContext* aCx,
+ nsIGlobalObject* aGlobal,
+ const nsAString& aTitle,
+ const NotificationOptions& aOptions,
+ const nsAString& aScope,
+ ErrorResult& aRv);
+
+ nsIPrincipal* GetPrincipal();
+
+ nsresult PersistNotification();
+ void UnpersistNotification();
+
+ void
+ SetAlertName();
+
+ bool IsTargetThread() const
+ {
+ return NS_IsMainThread() == !mWorkerPrivate;
+ }
+
+ bool RegisterWorkerHolder();
+ void UnregisterWorkerHolder();
+
+ nsresult ResolveIconAndSoundURL(nsString&, nsString&);
+
+ // Only used for Notifications on Workers, worker thread only.
+ UniquePtr<NotificationWorkerHolder> mWorkerHolder;
+ // Target thread only.
+ uint32_t mTaskCount;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_notification_h__
+
diff --git a/dom/notification/NotificationDB.jsm b/dom/notification/NotificationDB.jsm
new file mode 100644
index 000000000..863dd2484
--- /dev/null
+++ b/dom/notification/NotificationDB.jsm
@@ -0,0 +1,360 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = [];
+
+const DEBUG = false;
+function debug(s) { dump("-*- NotificationDB component: " + s + "\n"); }
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1",
+ "nsIMessageListenerManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage",
+ "@mozilla.org/notificationStorage;1",
+ "nsINotificationStorage");
+
+const NOTIFICATION_STORE_DIR = OS.Constants.Path.profileDir;
+const NOTIFICATION_STORE_PATH =
+ OS.Path.join(NOTIFICATION_STORE_DIR, "notificationstore.json");
+
+const kMessages = [
+ "Notification:Save",
+ "Notification:Delete",
+ "Notification:GetAll"
+];
+
+var NotificationDB = {
+
+ // Ensure we won't call init() while xpcom-shutdown is performed
+ _shutdownInProgress: false,
+
+ init: function() {
+ if (this._shutdownInProgress) {
+ return;
+ }
+
+ this.notifications = {};
+ this.byTag = {};
+ this.loaded = false;
+
+ this.tasks = []; // read/write operation queue
+ this.runningTask = null;
+
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ this.registerListeners();
+ },
+
+ registerListeners: function() {
+ for (let message of kMessages) {
+ ppmm.addMessageListener(message, this);
+ }
+ },
+
+ unregisterListeners: function() {
+ for (let message of kMessages) {
+ ppmm.removeMessageListener(message, this);
+ }
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (DEBUG) debug("Topic: " + aTopic);
+ if (aTopic == "xpcom-shutdown") {
+ this._shutdownInProgress = true;
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ this.unregisterListeners();
+ }
+ },
+
+ filterNonAppNotifications: function(notifications) {
+ for (let origin in notifications) {
+ let persistentNotificationCount = 0;
+ for (let id in notifications[origin]) {
+ if (notifications[origin][id].serviceWorkerRegistrationScope) {
+ persistentNotificationCount++;
+ } else {
+ delete notifications[origin][id];
+ }
+ }
+ if (persistentNotificationCount == 0) {
+ if (DEBUG) debug("Origin " + origin + " is not linked to an app manifest, deleting.");
+ delete notifications[origin];
+ }
+ }
+
+ return notifications;
+ },
+
+ // Attempt to read notification file, if it's not there we will create it.
+ load: function() {
+ var promise = OS.File.read(NOTIFICATION_STORE_PATH, { encoding: "utf-8"});
+ return promise.then(
+ function onSuccess(data) {
+ if (data.length > 0) {
+ // Preprocessing phase intends to cleanly separate any migration-related
+ // tasks.
+ this.notifications = this.filterNonAppNotifications(JSON.parse(data));
+ }
+
+ // populate the list of notifications by tag
+ if (this.notifications) {
+ for (var origin in this.notifications) {
+ this.byTag[origin] = {};
+ for (var id in this.notifications[origin]) {
+ var curNotification = this.notifications[origin][id];
+ if (curNotification.tag) {
+ this.byTag[origin][curNotification.tag] = curNotification;
+ }
+ }
+ }
+ }
+
+ this.loaded = true;
+ }.bind(this),
+
+ // If read failed, we assume we have no notifications to load.
+ function onFailure(reason) {
+ this.loaded = true;
+ return this.createStore();
+ }.bind(this)
+ );
+ },
+
+ // Creates the notification directory.
+ createStore: function() {
+ var promise = OS.File.makeDir(NOTIFICATION_STORE_DIR, {
+ ignoreExisting: true
+ });
+ return promise.then(
+ this.createFile.bind(this)
+ );
+ },
+
+ // Creates the notification file once the directory is created.
+ createFile: function() {
+ return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, "");
+ },
+
+ // Save current notifications to the file.
+ save: function() {
+ var data = JSON.stringify(this.notifications);
+ return OS.File.writeAtomic(NOTIFICATION_STORE_PATH, data, { encoding: "utf-8"});
+ },
+
+ // Helper function: promise will be resolved once file exists and/or is loaded.
+ ensureLoaded: function() {
+ if (!this.loaded) {
+ return this.load();
+ } else {
+ return Promise.resolve();
+ }
+ },
+
+ receiveMessage: function(message) {
+ if (DEBUG) { debug("Received message:" + message.name); }
+
+ // sendAsyncMessage can fail if the child process exits during a
+ // notification storage operation, so always wrap it in a try/catch.
+ function returnMessage(name, data) {
+ try {
+ message.target.sendAsyncMessage(name, data);
+ } catch (e) {
+ if (DEBUG) { debug("Return message failed, " + name); }
+ }
+ }
+
+ switch (message.name) {
+ case "Notification:GetAll":
+ this.queueTask("getall", message.data).then(function(notifications) {
+ returnMessage("Notification:GetAll:Return:OK", {
+ requestID: message.data.requestID,
+ origin: message.data.origin,
+ notifications: notifications
+ });
+ }).catch(function(error) {
+ returnMessage("Notification:GetAll:Return:KO", {
+ requestID: message.data.requestID,
+ origin: message.data.origin,
+ errorMsg: error
+ });
+ });
+ break;
+
+ case "Notification:Save":
+ this.queueTask("save", message.data).then(function() {
+ returnMessage("Notification:Save:Return:OK", {
+ requestID: message.data.requestID
+ });
+ }).catch(function(error) {
+ returnMessage("Notification:Save:Return:KO", {
+ requestID: message.data.requestID,
+ errorMsg: error
+ });
+ });
+ break;
+
+ case "Notification:Delete":
+ this.queueTask("delete", message.data).then(function() {
+ returnMessage("Notification:Delete:Return:OK", {
+ requestID: message.data.requestID
+ });
+ }).catch(function(error) {
+ returnMessage("Notification:Delete:Return:KO", {
+ requestID: message.data.requestID,
+ errorMsg: error
+ });
+ });
+ break;
+
+ default:
+ if (DEBUG) { debug("Invalid message name" + message.name); }
+ }
+ },
+
+ // We need to make sure any read/write operations are atomic,
+ // so use a queue to run each operation sequentially.
+ queueTask: function(operation, data) {
+ if (DEBUG) { debug("Queueing task: " + operation); }
+
+ var defer = {};
+
+ this.tasks.push({
+ operation: operation,
+ data: data,
+ defer: defer
+ });
+
+ var promise = new Promise(function(resolve, reject) {
+ defer.resolve = resolve;
+ defer.reject = reject;
+ });
+
+ // Only run immediately if we aren't currently running another task.
+ if (!this.runningTask) {
+ if (DEBUG) { debug("Task queue was not running, starting now..."); }
+ this.runNextTask();
+ }
+
+ return promise;
+ },
+
+ runNextTask: function() {
+ if (this.tasks.length === 0) {
+ if (DEBUG) { debug("No more tasks to run, queue depleted"); }
+ this.runningTask = null;
+ return;
+ }
+ this.runningTask = this.tasks.shift();
+
+ // Always make sure we are loaded before performing any read/write tasks.
+ this.ensureLoaded()
+ .then(function() {
+ var task = this.runningTask;
+
+ switch (task.operation) {
+ case "getall":
+ return this.taskGetAll(task.data);
+ break;
+
+ case "save":
+ return this.taskSave(task.data);
+ break;
+
+ case "delete":
+ return this.taskDelete(task.data);
+ break;
+ }
+
+ }.bind(this))
+ .then(function(payload) {
+ if (DEBUG) {
+ debug("Finishing task: " + this.runningTask.operation);
+ }
+ this.runningTask.defer.resolve(payload);
+ }.bind(this))
+ .catch(function(err) {
+ if (DEBUG) {
+ debug("Error while running " + this.runningTask.operation + ": " + err);
+ }
+ this.runningTask.defer.reject(new String(err));
+ }.bind(this))
+ .then(function() {
+ this.runNextTask();
+ }.bind(this));
+ },
+
+ taskGetAll: function(data) {
+ if (DEBUG) { debug("Task, getting all"); }
+ var origin = data.origin;
+ var notifications = [];
+ // Grab only the notifications for specified origin.
+ if (this.notifications[origin]) {
+ for (var i in this.notifications[origin]) {
+ notifications.push(this.notifications[origin][i]);
+ }
+ }
+ return Promise.resolve(notifications);
+ },
+
+ taskSave: function(data) {
+ if (DEBUG) { debug("Task, saving"); }
+ var origin = data.origin;
+ var notification = data.notification;
+ if (!this.notifications[origin]) {
+ this.notifications[origin] = {};
+ this.byTag[origin] = {};
+ }
+
+ // We might have existing notification with this tag,
+ // if so we need to remove it before saving the new one.
+ if (notification.tag) {
+ var oldNotification = this.byTag[origin][notification.tag];
+ if (oldNotification) {
+ delete this.notifications[origin][oldNotification.id];
+ }
+ this.byTag[origin][notification.tag] = notification;
+ }
+
+ this.notifications[origin][notification.id] = notification;
+ return this.save();
+ },
+
+ taskDelete: function(data) {
+ if (DEBUG) { debug("Task, deleting"); }
+ var origin = data.origin;
+ var id = data.id;
+ if (!this.notifications[origin]) {
+ if (DEBUG) { debug("No notifications found for origin: " + origin); }
+ return Promise.resolve();
+ }
+
+ // Make sure we can find the notification to delete.
+ var oldNotification = this.notifications[origin][id];
+ if (!oldNotification) {
+ if (DEBUG) { debug("No notification found with id: " + id); }
+ return Promise.resolve();
+ }
+
+ if (oldNotification.tag) {
+ delete this.byTag[origin][oldNotification.tag];
+ }
+ delete this.notifications[origin][id];
+ return this.save();
+ }
+};
+
+NotificationDB.init();
diff --git a/dom/notification/NotificationEvent.cpp b/dom/notification/NotificationEvent.cpp
new file mode 100644
index 000000000..765dd68cb
--- /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
diff --git a/dom/notification/NotificationEvent.h b/dom/notification/NotificationEvent.h
new file mode 100644
index 000000000..ffea009da
--- /dev/null
+++ b/dom/notification/NotificationEvent.h
@@ -0,0 +1,75 @@
+/* -*- 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)
+ {
+ RefPtr<NotificationEvent> e = new NotificationEvent(aOwner);
+ bool trusted = e->Init(aOwner);
+ e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
+ e->SetTrusted(trusted);
+ e->SetComposed(aOptions.mComposed);
+ 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_()
+ {
+ RefPtr<Notification> n = mNotification;
+ return n.forget();
+ }
+
+private:
+ RefPtr<Notification> mNotification;
+};
+
+END_WORKERS_NAMESPACE
+#endif /* mozilla_dom_workers_notificationevent_h__ */
+
diff --git a/dom/notification/NotificationStorage.js b/dom/notification/NotificationStorage.js
new file mode 100644
index 000000000..8186d61b8
--- /dev/null
+++ b/dom/notification/NotificationStorage.js
@@ -0,0 +1,274 @@
+/* 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;
+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");
+
+const NOTIFICATIONSTORAGE_CID = "{37f819b0-0b5c-11e3-8ffd-0800200c9a66}";
+const NOTIFICATIONSTORAGE_CONTRACTID = "@mozilla.org/notificationStorage;1";
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+
+const kMessageNotificationGetAllOk = "Notification:GetAll:Return:OK";
+const kMessageNotificationGetAllKo = "Notification:GetAll:Return:KO";
+const kMessageNotificationSaveKo = "Notification:Save:Return:KO";
+const kMessageNotificationDeleteKo = "Notification:Delete:Return:KO";
+
+const kMessages = [
+ kMessageNotificationGetAllOk,
+ kMessageNotificationGetAllKo,
+ kMessageNotificationSaveKo,
+ kMessageNotificationDeleteKo
+];
+
+function NotificationStorage() {
+ // cache objects
+ this._notifications = {};
+ this._byTag = {};
+ this._cached = false;
+
+ this._requests = {};
+ this._requestCount = 0;
+
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+
+ // Register for message listeners.
+ this.registerListeners();
+}
+
+NotificationStorage.prototype = {
+
+ registerListeners: function() {
+ for (let message of kMessages) {
+ cpmm.addMessageListener(message, this);
+ }
+ },
+
+ unregisterListeners: function() {
+ for (let message of kMessages) {
+ cpmm.removeMessageListener(message, this);
+ }
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (DEBUG) debug("Topic: " + aTopic);
+ if (aTopic === "xpcom-shutdown") {
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ this.unregisterListeners();
+ }
+ },
+
+ put: function(origin, id, title, dir, lang, body, tag, icon, alertName,
+ data, behavior, serviceWorkerRegistrationScope) {
+ if (DEBUG) { debug("PUT: " + origin + " " + id + ": " + title); }
+ var notification = {
+ id: id,
+ title: title,
+ dir: dir,
+ lang: lang,
+ body: body,
+ tag: tag,
+ icon: icon,
+ alertName: alertName,
+ timestamp: new Date().getTime(),
+ origin: origin,
+ data: data,
+ mozbehavior: behavior,
+ serviceWorkerRegistrationScope: serviceWorkerRegistrationScope,
+ };
+
+ this._notifications[id] = notification;
+ if (tag) {
+ if (!this._byTag[origin]) {
+ this._byTag[origin] = {};
+ }
+
+ // We might have existing notification with this tag,
+ // if so we need to remove it from our cache.
+ if (this._byTag[origin][tag]) {
+ var oldNotification = this._byTag[origin][tag];
+ delete this._notifications[oldNotification.id];
+ }
+
+ this._byTag[origin][tag] = notification;
+ };
+
+ if (serviceWorkerRegistrationScope) {
+ cpmm.sendAsyncMessage("Notification:Save", {
+ origin: origin,
+ notification: notification
+ });
+ }
+ },
+
+ get: function(origin, tag, callback) {
+ 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, serviceWorkerRegistrationScope) {
+ if (id == this.searchID) {
+ self.originalCallback.handle(id, title, dir, lang, body, tag, icon, data, behavior, serviceWorkerRegistrationScope);
+ }
+ };
+ 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];
+ }
+
+ cpmm.sendAsyncMessage("Notification:Delete", {
+ origin: origin,
+ id: id
+ });
+ },
+
+ receiveMessage: function(message) {
+ var request = this._requests[message.data.requestID];
+
+ switch (message.name) {
+ case kMessageNotificationGetAllOk:
+ delete this._requests[message.data.requestID];
+ this._populateCache(message.data.notifications);
+ this._fetchFromCache(request.origin, request.tag, request.callback);
+ break;
+
+ case kMessageNotificationGetAllKo:
+ delete this._requests[message.data.requestID];
+ try {
+ request.callback.done();
+ } catch (e) {
+ debug("Error calling callback done: " + e);
+ }
+ case kMessageNotificationSaveKo:
+ case kMessageNotificationDeleteKo:
+ if (DEBUG) {
+ debug("Error received when treating: '" + message.name +
+ "': " + message.data.errorMsg);
+ }
+ break;
+
+ default:
+ if (DEBUG) debug("Unrecognized message: " + message.name);
+ break;
+ }
+ },
+
+ _fetchFromDB: function(origin, tag, callback) {
+ var request = {
+ origin: origin,
+ tag: tag,
+ callback: callback
+ };
+ var requestID = this._requestCount++;
+ this._requests[requestID] = request;
+ cpmm.sendAsyncMessage("Notification:GetAll", {
+ origin: origin,
+ requestID: requestID
+ });
+ },
+
+ _fetchFromCache: function(origin, tag, callback) {
+ var notifications = [];
+ // If a tag was specified and we have a notification
+ // with this tag, return that. If no tag was specified
+ // simple return all stored notifications.
+ if (tag && this._byTag[origin] && this._byTag[origin][tag]) {
+ notifications.push(this._byTag[origin][tag]);
+ } else if (!tag) {
+ for (var id in this._notifications) {
+ if (this._notifications[id].origin === origin) {
+ notifications.push(this._notifications[id]);
+ }
+ }
+ }
+
+ // Pass each notification back separately.
+ // The callback is called asynchronously to match the behaviour when
+ // fetching from the database.
+ notifications.forEach(function(notification) {
+ try {
+ Services.tm.currentThread.dispatch(
+ callback.handle.bind(callback,
+ notification.id,
+ notification.title,
+ notification.dir,
+ notification.lang,
+ notification.body,
+ notification.tag,
+ notification.icon,
+ notification.data,
+ notification.mozbehavior,
+ notification.serviceWorkerRegistrationScope),
+ Ci.nsIThread.DISPATCH_NORMAL);
+ } catch (e) {
+ if (DEBUG) { debug("Error calling callback handle: " + e); }
+ }
+ });
+ try {
+ Services.tm.currentThread.dispatch(callback.done,
+ Ci.nsIThread.DISPATCH_NORMAL);
+ } catch (e) {
+ if (DEBUG) { debug("Error calling callback done: " + e); }
+ }
+ },
+
+ _populateCache: function(notifications) {
+ notifications.forEach(function(notification) {
+ this._notifications[notification.id] = notification;
+ if (notification.tag && notification.origin) {
+ let tag = notification.tag;
+ let origin = notification.origin;
+ if (!this._byTag[origin]) {
+ this._byTag[origin] = {};
+ }
+ this._byTag[origin][tag] = notification;
+ }
+ }.bind(this));
+ this._cached = true;
+ },
+
+ classID : Components.ID(NOTIFICATIONSTORAGE_CID),
+ contractID : NOTIFICATIONSTORAGE_CONTRACTID,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINotificationStorage,
+ Ci.nsIMessageListener]),
+};
+
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NotificationStorage]);
diff --git a/dom/notification/NotificationStorage.manifest b/dom/notification/NotificationStorage.manifest
new file mode 100644
index 000000000..34c5c5138
--- /dev/null
+++ b/dom/notification/NotificationStorage.manifest
@@ -0,0 +1,3 @@
+# NotificationStorage.js
+component {37f819b0-0b5c-11e3-8ffd-0800200c9a66} NotificationStorage.js
+contract @mozilla.org/notificationStorage;1 {37f819b0-0b5c-11e3-8ffd-0800200c9a66}
diff --git a/dom/notification/moz.build b/dom/notification/moz.build
new file mode 100644
index 000000000..d966b160d
--- /dev/null
+++ b/dom/notification/moz.build
@@ -0,0 +1,41 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_COMPONENTS += [
+ 'NotificationStorage.js',
+ 'NotificationStorage.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'NotificationDB.jsm'
+]
+
+EXPORTS.mozilla.dom += [
+ 'DesktopNotification.h',
+ 'Notification.h',
+ 'NotificationEvent.h',
+]
+
+UNIFIED_SOURCES += [
+ 'DesktopNotification.cpp',
+ 'Notification.cpp',
+ 'NotificationEvent.cpp',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
+LOCAL_INCLUDES += [
+ '/dom/base',
+ '/dom/ipc',
+ '/dom/workers',
+]
+
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
+XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
diff --git a/dom/notification/test/browser/browser.ini b/dom/notification/test/browser/browser.ini
new file mode 100644
index 000000000..8a357c1a1
--- /dev/null
+++ b/dom/notification/test/browser/browser.ini
@@ -0,0 +1,2 @@
+[browser_permission_dismiss.js]
+support-files = notification.html
diff --git a/dom/notification/test/browser/browser_permission_dismiss.js b/dom/notification/test/browser/browser_permission_dismiss.js
new file mode 100644
index 000000000..de655870b
--- /dev/null
+++ b/dom/notification/test/browser/browser_permission_dismiss.js
@@ -0,0 +1,113 @@
+"use strict";
+
+const ORIGIN_URI = Services.io.newURI("http://mochi.test:8888", null, null);
+const PERMISSION_NAME = "desktop-notification";
+const PROMPT_ALLOW_BUTTON = -1;
+const PROMPT_BLOCK_BUTTON = 0;
+const TEST_URL = "http://mochi.test:8888/browser/dom/notification/test/browser/notification.html";
+
+/**
+ * Clicks the specified web-notifications prompt button.
+ *
+ * @param {Number} aButtonIndex Number indicating which button to click.
+ * See the constants in this file.
+ * @note modified from toolkit/components/passwordmgr/test/browser/head.js
+ */
+function clickDoorhangerButton(aButtonIndex) {
+ ok(true, "Looking for action at index " + aButtonIndex);
+
+ let popup = PopupNotifications.getNotification("web-notifications");
+ let notifications = popup.owner.panel.childNodes;
+ ok(notifications.length > 0, "at least one notification displayed");
+ ok(true, notifications.length + " notification(s)");
+ let notification = notifications[0];
+
+ if (aButtonIndex == -1) {
+ ok(true, "Triggering main action");
+ notification.button.doCommand();
+ } else if (aButtonIndex <= popup.secondaryActions.length) {
+ ok(true, "Triggering secondary action " + aButtonIndex);
+ notification.childNodes[aButtonIndex].doCommand();
+ }
+}
+
+/**
+ * Opens a tab which calls `Notification.requestPermission()` with a callback
+ * argument, calls the `task` function while the permission prompt is open,
+ * and verifies that the expected permission is set.
+ *
+ * @param {Function} task Task function to run to interact with the prompt.
+ * @param {String} permission Expected permission value.
+ * @return {Promise} resolving when the task function is done and the tab
+ * closes.
+ */
+function tabWithRequest(task, permission) {
+ Services.perms.remove(ORIGIN_URI, PERMISSION_NAME);
+
+ return BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: TEST_URL,
+ }, function*(browser) {
+ let requestPromise = ContentTask.spawn(browser, {
+ permission
+ }, function*({permission}) {
+ function requestCallback(perm) {
+ is(perm, permission,
+ "Should call the legacy callback with the permission state");
+ }
+ let perm = yield content.window.Notification
+ .requestPermission(requestCallback);
+ is(perm, permission,
+ "Should resolve the promise with the permission state");
+ });
+
+ yield BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ yield task();
+ yield requestPromise;
+ });
+}
+
+add_task(function* setup() {
+ SimpleTest.registerCleanupFunction(() => {
+ Services.perms.remove(ORIGIN_URI, PERMISSION_NAME);
+ });
+});
+
+add_task(function* test_requestPermission_granted() {
+ yield tabWithRequest(function() {
+ clickDoorhangerButton(PROMPT_ALLOW_BUTTON);
+ }, "granted");
+
+ ok(!PopupNotifications.getNotification("web-notifications"),
+ "Should remove the doorhanger notification icon if granted");
+
+ is(Services.perms.testPermission(ORIGIN_URI, PERMISSION_NAME),
+ Services.perms.ALLOW_ACTION,
+ "Check permission in perm. manager");
+});
+
+add_task(function* test_requestPermission_denied() {
+ yield tabWithRequest(function() {
+ clickDoorhangerButton(PROMPT_BLOCK_BUTTON);
+ }, "denied");
+
+ ok(!PopupNotifications.getNotification("web-notifications"),
+ "Should remove the doorhanger notification icon if denied");
+
+ is(Services.perms.testPermission(ORIGIN_URI, PERMISSION_NAME),
+ Services.perms.DENY_ACTION,
+ "Check permission in perm. manager");
+});
+
+add_task(function* test_requestPermission_dismissed() {
+ yield tabWithRequest(function() {
+ PopupNotifications.panel.hidePopup();
+ }, "default");
+
+ ok(!PopupNotifications.getNotification("web-notifications"),
+ "Should remove the doorhanger notification icon if dismissed");
+
+ is(Services.perms.testPermission(ORIGIN_URI, PERMISSION_NAME),
+ Services.perms.UNKNOWN_ACTION,
+ "Check permission in perm. manager");
+});
diff --git a/dom/notification/test/browser/notification.html b/dom/notification/test/browser/notification.html
new file mode 100644
index 000000000..0ceeb8ea4
--- /dev/null
+++ b/dom/notification/test/browser/notification.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Notifications test</title>
+ </head>
+
+ <body>
+
+ </body>
+</html>
diff --git a/dom/notification/test/unit/common_test_notificationdb.js b/dom/notification/test/unit/common_test_notificationdb.js
new file mode 100644
index 000000000..116c7836a
--- /dev/null
+++ b/dom/notification/test/unit/common_test_notificationdb.js
@@ -0,0 +1,60 @@
+"use strict";
+
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1",
+ "nsIMessageSender");
+
+function getNotificationObject(app, id, tag) {
+ return {
+ origin: "https://" + app + ".gaiamobile.org/",
+ id: id,
+ title: app + "Notification:" + Date.now(),
+ dir: "auto",
+ lang: "",
+ body: app + " notification body",
+ tag: tag || "",
+ icon: "icon.png"
+ };
+}
+
+var systemNotification =
+ getNotificationObject("system", "{2bc883bf-2809-4432-b0f4-f54e10372764}");
+
+var calendarNotification =
+ getNotificationObject("calendar", "{d8d11299-a58e-429b-9a9a-57c562982fbf}");
+
+// Helper to start the NotificationDB
+function startNotificationDB() {
+ Cu.import("resource://gre/modules/NotificationDB.jsm");
+}
+
+// Helper function to add a listener, send message and treat the reply
+function addAndSend(msg, reply, callback, payload, runNext = true) {
+ let handler = {
+ receiveMessage: function(message) {
+ if (message.name === reply) {
+ cpmm.removeMessageListener(reply, handler);
+ callback(message);
+ if (runNext) {
+ run_next_test();
+ }
+ }
+ }
+ };
+ cpmm.addMessageListener(reply, handler);
+ cpmm.sendAsyncMessage(msg, payload);
+}
+
+// helper fonction, comparing two notifications
+function compareNotification(notif1, notif2) {
+ // retrieved notification should be the second one sent
+ for (let prop in notif1) {
+ // compare each property
+ do_check_eq(notif1[prop], notif2[prop]);
+ }
+}
diff --git a/dom/notification/test/unit/test_notificationdb.js b/dom/notification/test/unit/test_notificationdb.js
new file mode 100644
index 000000000..d631bd034
--- /dev/null
+++ b/dom/notification/test/unit/test_notificationdb.js
@@ -0,0 +1,310 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+ startNotificationDB();
+ run_next_test();
+}
+
+// Get one notification, none exists
+add_test(function test_get_none() {
+ let requestID = 0;
+ let msgReply = "Notification:GetAll:Return:OK";
+ let msgHandler = function(message) {
+ do_check_eq(requestID, message.data.requestID);
+ do_check_eq(0, message.data.notifications.length);
+ };
+
+ addAndSend("Notification:GetAll", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ requestID: requestID
+ });
+});
+
+// Store one notification
+add_test(function test_send_one() {
+ let requestID = 1;
+ let msgReply = "Notification:Save:Return:OK";
+ let msgHandler = function(message) {
+ do_check_eq(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Save", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ notification: systemNotification,
+ requestID: requestID
+ });
+});
+
+// Get one notification, one exists
+add_test(function test_get_one() {
+ let requestID = 2;
+ let msgReply = "Notification:GetAll:Return:OK";
+ let msgHandler = function(message) {
+ do_check_eq(requestID, message.data.requestID);
+ do_check_eq(1, message.data.notifications.length);
+ // compare the content
+ compareNotification(systemNotification, message.data.notifications[0]);
+ };
+
+ addAndSend("Notification:GetAll", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ requestID: requestID
+ });
+});
+
+// Delete one notification
+add_test(function test_delete_one() {
+ let requestID = 3;
+ let msgReply = "Notification:Delete:Return:OK";
+ let msgHandler = function(message) {
+ do_check_eq(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Delete", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ id: systemNotification.id,
+ requestID: requestID
+ });
+});
+
+// Get one notification, none exists
+add_test(function test_get_none_again() {
+ let requestID = 4;
+ let msgReply = "Notification:GetAll:Return:OK";
+ let msgHandler = function(message) {
+ do_check_eq(requestID, message.data.requestID);
+ do_check_eq(0, message.data.notifications.length);
+ };
+
+ addAndSend("Notification:GetAll", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ requestID: requestID
+ });
+});
+
+// Delete one notification that do not exists anymore
+add_test(function test_delete_one_nonexistent() {
+ let requestID = 5;
+ let msgReply = "Notification:Delete:Return:OK";
+ let msgHandler = function(message) {
+ do_check_eq(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Delete", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ id: systemNotification.id,
+ requestID: requestID
+ });
+});
+
+// Store two notifications with the same id
+add_test(function test_send_two_get_one() {
+ let requestID = 6;
+ let calls = 0;
+
+ let msgGetReply = "Notification:GetAll:Return:OK";
+ let msgGetHandler = function(message) {
+ do_check_eq(requestID + 2, message.data.requestID);
+ do_check_eq(1, message.data.notifications.length);
+ // compare the content
+ compareNotification(systemNotification, message.data.notifications[0]);
+ };
+
+ let msgSaveReply = "Notification:Save:Return:OK";
+ let msgSaveHandler = function(message) {
+ calls += 1;
+ if (calls === 2) {
+ addAndSend("Notification:GetAll", msgGetReply, msgGetHandler, {
+ origin: systemNotification.origin,
+ requestID: (requestID + 2)
+ });
+ }
+ };
+
+ addAndSend("Notification:Save", msgSaveReply, msgSaveHandler, {
+ origin: systemNotification.origin,
+ notification: systemNotification,
+ requestID: requestID
+ }, false);
+
+ addAndSend("Notification:Save", msgSaveReply, msgSaveHandler, {
+ origin: systemNotification.origin,
+ notification: systemNotification,
+ requestID: (requestID + 1)
+ }, false);
+});
+
+// Delete previous notification
+add_test(function test_delete_previous() {
+ let requestID = 8;
+ let msgReply = "Notification:Delete:Return:OK";
+ let msgHandler = function(message) {
+ do_check_eq(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Delete", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ id: systemNotification.id,
+ requestID: requestID
+ });
+});
+
+// Store two notifications from same origin with the same tag
+add_test(function test_send_two_get_one() {
+ let requestID = 10;
+ let tag = "voicemail";
+
+ let systemNotification1 =
+ getNotificationObject("system", "{f271f9ee-3955-4c10-b1f2-af552fb270ee}", tag);
+ let systemNotification2 =
+ getNotificationObject("system", "{8ef9a628-f0f4-44b4-820d-c117573c33e3}", tag);
+
+ let msgGetReply = "Notification:GetAll:Return:OK";
+ let msgGetNotifHandler = {
+ receiveMessage: function(message) {
+ if (message.name === msgGetReply) {
+ cpmm.removeMessageListener(msgGetReply, msgGetNotifHandler);
+ let notifications = message.data.notifications;
+ // same tag, so replaced
+ do_check_eq(1, notifications.length);
+ // compare the content
+ compareNotification(systemNotification2, notifications[0]);
+ run_next_test();
+ }
+ }
+ };
+
+ cpmm.addMessageListener(msgGetReply, msgGetNotifHandler);
+
+ let msgSaveReply = "Notification:Save:Return:OK";
+ let msgSaveCalls = 0;
+ let msgSaveHandler = function(message) {
+ msgSaveCalls++;
+ // Once both request have been sent, trigger getall
+ if (msgSaveCalls === 2) {
+ cpmm.sendAsyncMessage("Notification:GetAll", {
+ origin: systemNotification1.origin,
+ requestID: message.data.requestID + 2 // 12, 13
+ });
+ }
+ };
+
+ addAndSend("Notification:Save", msgSaveReply, msgSaveHandler, {
+ origin: systemNotification1.origin,
+ notification: systemNotification1,
+ requestID: requestID // 10
+ }, false);
+
+ addAndSend("Notification:Save", msgSaveReply, msgSaveHandler, {
+ origin: systemNotification2.origin,
+ notification: systemNotification2,
+ requestID: (requestID + 1) // 11
+ }, false);
+});
+
+// Delete previous notification
+add_test(function test_delete_previous() {
+ let requestID = 15;
+ let msgReply = "Notification:Delete:Return:OK";
+ let msgHandler = function(message) {
+ do_check_eq(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Delete", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ id: "{8ef9a628-f0f4-44b4-820d-c117573c33e3}",
+ requestID: requestID
+ });
+});
+
+// Store two notifications from two origins with the same tag
+add_test(function test_send_two_get_two() {
+ let requestID = 20;
+ let tag = "voicemail";
+
+ let systemNotification1 = systemNotification;
+ systemNotification1.tag = tag;
+
+ let calendarNotification2 = calendarNotification;
+ calendarNotification2.tag = tag;
+
+ let msgGetReply = "Notification:GetAll:Return:OK";
+ let msgGetCalls = 0;
+ let msgGetHandler = {
+ receiveMessage: function(message) {
+ if (message.name === msgGetReply) {
+ msgGetCalls++;
+ let notifications = message.data.notifications;
+
+ // one notification per origin
+ do_check_eq(1, notifications.length);
+
+ // first call should be system notification
+ if (msgGetCalls === 1) {
+ compareNotification(systemNotification1, notifications[0]);
+ }
+
+ // second and last call should be calendar notification
+ if (msgGetCalls === 2) {
+ cpmm.removeMessageListener(msgGetReply, msgGetHandler);
+ compareNotification(calendarNotification2, notifications[0]);
+ run_next_test();
+ }
+ }
+ }
+ };
+ cpmm.addMessageListener(msgGetReply, msgGetHandler);
+
+ let msgSaveReply = "Notification:Save:Return:OK";
+ let msgSaveCalls = 0;
+ let msgSaveHandler = {
+ receiveMessage: function(message) {
+ if (message.name === msgSaveReply) {
+ msgSaveCalls++;
+ if (msgSaveCalls === 2) {
+ cpmm.removeMessageListener(msgSaveReply, msgSaveHandler);
+
+ // Trigger getall for each origin
+ cpmm.sendAsyncMessage("Notification:GetAll", {
+ origin: systemNotification1.origin,
+ requestID: message.data.requestID + 1 // 22
+ });
+
+ cpmm.sendAsyncMessage("Notification:GetAll", {
+ origin: calendarNotification2.origin,
+ requestID: message.data.requestID + 2 // 23
+ });
+ }
+ }
+ }
+ };
+ cpmm.addMessageListener(msgSaveReply, msgSaveHandler);
+
+ cpmm.sendAsyncMessage("Notification:Save", {
+ origin: systemNotification1.origin,
+ notification: systemNotification1,
+ requestID: requestID // 20
+ });
+
+ cpmm.sendAsyncMessage("Notification:Save", {
+ origin: calendarNotification2.origin,
+ notification: calendarNotification2,
+ requestID: (requestID + 1) // 21
+ });
+});
+
+// Cleanup previous notification
+add_test(function test_delete_previous() {
+ let requestID = 25;
+ let msgReply = "Notification:Delete:Return:OK";
+ let msgHandler = function(message) {
+ do_check_eq(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Delete", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ id: "{2bc883bf-2809-4432-b0f4-f54e10372764}",
+ requestID: requestID
+ });
+});
diff --git a/dom/notification/test/unit/test_notificationdb_bug1024090.js b/dom/notification/test/unit/test_notificationdb_bug1024090.js
new file mode 100644
index 000000000..68dfb164c
--- /dev/null
+++ b/dom/notification/test/unit/test_notificationdb_bug1024090.js
@@ -0,0 +1,56 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+/// For bug 1024090: test edge case of notificationstore.json
+add_test(function test_bug1024090_purge() {
+ Cu.import("resource://gre/modules/osfile.jsm");
+ const NOTIFICATION_STORE_PATH =
+ OS.Path.join(OS.Constants.Path.profileDir, "notificationstore.json");
+ let cleanup = OS.File.writeAtomic(NOTIFICATION_STORE_PATH, "");
+ cleanup.then(
+ function onSuccess() {
+ ok(true, "Notification database cleaned.");
+ },
+ function onError(reason) {
+ ok(false, "Notification database error when cleaning: " + reason);
+ }
+ ).then(function next() {
+ do_print("Cleanup steps completed: " + NOTIFICATION_STORE_PATH);
+ startNotificationDB();
+ run_next_test();
+ });
+});
+
+// Store one notification
+add_test(function test_bug1024090_send_one() {
+ let requestID = 1;
+ let msgReply = "Notification:Save:Return:OK";
+ let msgHandler = function(message) {
+ equal(requestID, message.data.requestID, "Checking requestID");
+ };
+
+ addAndSend("Notification:Save", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ notification: systemNotification,
+ requestID: requestID
+ });
+});
+
+// Get one notification, one exists
+add_test(function test_bug1024090_get_one() {
+ let requestID = 2;
+ let msgReply = "Notification:GetAll:Return:OK";
+ let msgHandler = function(message) {
+ equal(requestID, message.data.requestID, "Checking requestID");
+ equal(1, message.data.notifications.length, "One notification stored");
+ };
+
+ addAndSend("Notification:GetAll", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ requestID: requestID
+ });
+});
diff --git a/dom/notification/test/unit/xpcshell.ini b/dom/notification/test/unit/xpcshell.ini
new file mode 100644
index 000000000..63ec4019a
--- /dev/null
+++ b/dom/notification/test/unit/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+head = common_test_notificationdb.js
+tail =
+skip-if = toolkit == 'android'
+
+[test_notificationdb.js]
+[test_notificationdb_bug1024090.js]