diff options
Diffstat (limited to 'dom/notification')
-rw-r--r-- | dom/notification/DesktopNotification.cpp | 319 | ||||
-rw-r--r-- | dom/notification/DesktopNotification.h | 180 | ||||
-rw-r--r-- | dom/notification/Notification.cpp | 2769 | ||||
-rw-r--r-- | dom/notification/Notification.h | 471 | ||||
-rw-r--r-- | dom/notification/NotificationDB.jsm | 360 | ||||
-rw-r--r-- | dom/notification/NotificationEvent.cpp | 26 | ||||
-rw-r--r-- | dom/notification/NotificationEvent.h | 75 | ||||
-rw-r--r-- | dom/notification/NotificationStorage.js | 274 | ||||
-rw-r--r-- | dom/notification/NotificationStorage.manifest | 3 | ||||
-rw-r--r-- | dom/notification/moz.build | 41 | ||||
-rw-r--r-- | dom/notification/test/browser/browser.ini | 2 | ||||
-rw-r--r-- | dom/notification/test/browser/browser_permission_dismiss.js | 113 | ||||
-rw-r--r-- | dom/notification/test/browser/notification.html | 11 | ||||
-rw-r--r-- | dom/notification/test/unit/common_test_notificationdb.js | 60 | ||||
-rw-r--r-- | dom/notification/test/unit/test_notificationdb.js | 310 | ||||
-rw-r--r-- | dom/notification/test/unit/test_notificationdb_bug1024090.js | 56 | ||||
-rw-r--r-- | dom/notification/test/unit/xpcshell.ini | 7 |
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] |