diff options
Diffstat (limited to 'dom/notification/Notification.cpp')
-rw-r--r-- | dom/notification/Notification.cpp | 2333 |
1 files changed, 2333 insertions, 0 deletions
diff --git a/dom/notification/Notification.cpp b/dom/notification/Notification.cpp new file mode 100644 index 0000000000..39ba7b23ff --- /dev/null +++ b/dom/notification/Notification.cpp @@ -0,0 +1,2333 @@ +/* -*- 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 <utility> + +#include "mozilla/BasePrincipal.h" +#include "mozilla/Components.h" +#include "mozilla/Encoding.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/JSONStringWriteFuncs.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/AppNotificationServiceOptionsBinding.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/dom/QMResult.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "Navigator.h" +#include "nsComponentManagerUtils.h" +#include "nsContentPermissionHelper.h" +#include "nsContentUtils.h" +#include "nsFocusManager.h" +#include "nsIAlertsService.h" +#include "nsIContentPermissionPrompt.h" +#include "nsILoadContext.h" +#include "nsINotificationStorage.h" +#include "nsIPermission.h" +#include "nsIPermissionManager.h" +#include "nsIPushService.h" +#include "nsIScriptError.h" +#include "nsIServiceWorkerManager.h" +#include "nsIUUIDGenerator.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsServiceManagerUtils.h" +#include "nsStructuredCloneContainer.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" + +namespace mozilla::dom { + +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(std::move(strings)); + return NS_OK; + } + + NS_IMETHOD Done() override = 0; + + protected: + virtual ~ScopeCheckingGetCallback() = default; + + 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 { + AutoTArray<RefPtr<Notification>, 5> notifications; + + for (uint32_t i = 0; i < mStrings.Length(); ++i) { + auto result = 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); + if (result.isErr()) { + continue; + } + RefPtr<Notification> n = result.unwrap(); + n->SetStoredState(true); + notifications.AppendElement(n.forget()); + } + + mPromise->MaybeResolve(notifications); + return NS_OK; + } + + private: + virtual ~NotificationStorageCallback() = default; + + 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) + : Runnable("NotificationGetRunnable"), + 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 ContentPermissionRequestBase, + public nsIRunnable, + public nsINamed { + public: + NS_DECL_NSIRUNNABLE + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(NotificationPermissionRequest, + ContentPermissionRequestBase) + + // nsIContentPermissionRequest + NS_IMETHOD Cancel(void) override; + NS_IMETHOD Allow(JS::Handle<JS::Value> choices) override; + + NotificationPermissionRequest(nsIPrincipal* aPrincipal, + nsPIDOMWindowInner* aWindow, Promise* aPromise, + NotificationPermissionCallback* aCallback) + : ContentPermissionRequestBase(aPrincipal, aWindow, "notification"_ns, + "desktop-notification"_ns), + mPermission(NotificationPermission::Default), + mPromise(aPromise), + mCallback(aCallback) { + MOZ_ASSERT(aPromise); + } + + NS_IMETHOD GetName(nsACString& aName) override { + aName.AssignLiteral("NotificationPermissionRequest"); + return NS_OK; + } + + protected: + ~NotificationPermissionRequest() = default; + + MOZ_CAN_RUN_SCRIPT nsresult ResolvePromise(); + nsresult DispatchResolvePromise(); + NotificationPermission mPermission; + RefPtr<Promise> mPromise; + RefPtr<NotificationPermissionCallback> mCallback; +}; + +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, "Notification :: Get Permission"_ns), + 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) + : Runnable("FocusWindowRunnable"), mWindow(aWindow) {} + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See + // bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + AssertIsOnMainThread(); + if (!mWindow->IsCurrentInnerWindow()) { + // Window has been closed, this observer is not valid anymore + return NS_OK; + } + + nsCOMPtr<nsPIDOMWindowOuter> outerWindow = mWindow->GetOuterWindow(); + nsFocusManager::FocusWindow(outerWindow, CallerType::System); + return NS_OK; + } +}; + +nsresult CheckScope(nsIPrincipal* aPrincipal, const nsACString& aScope, + uint64_t aWindowID) { + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return aPrincipal->CheckMayLoadWithReporting( + scopeURI, + /* allowIfInheritsPrincipal = */ false, aWindowID); +} +} // anonymous namespace + +// Subclass that can be directly dispatched to child workers from the main +// thread. +class NotificationWorkerRunnable : public MainThreadWorkerRunnable { + protected: + explicit NotificationWorkerRunnable( + WorkerPrivate* aWorkerPrivate, + const char* aName = "NotificationWorkerRunnable") + : MainThreadWorkerRunnable(aWorkerPrivate, aName) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->AssertIsOnWorkerThread(); + // WorkerScope might start dying at the moment. And WorkerRunInternal() + // should not be executed once WorkerScope is dying, since + // WorkerRunInternal() might access resources which already been freed + // during WorkerRef::Notify(). + if (aWorkerPrivate->GlobalScope() && + !aWorkerPrivate->GlobalScope()->IsDying()) { + WorkerRunInternal(aWorkerPrivate); + } + return true; + } + + 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, + "NotificationEventWorkerRunnable"), + 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, + "ReleaseNotificationRunnable"), + mNotification(aNotification) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + aWorkerPrivate->AssertIsOnWorkerThread(); + // ReleaseNotificationRunnable is only used in StrongWorkerRef's shutdown + // callback. At the moment, it is supposed to executing + // mNotification->ReleaseObject() safely even though the corresponding + // WorkerScope::IsDying() is true. It is unlike other + // NotificationWorkerRunnable. + WorkerRunInternal(aWorkerPrivate); + return true; + } + + void WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override { + mNotification->ReleaseObject(); + } + + nsresult Cancel() override { + mNotification->ReleaseObject(); + return NS_OK; + } +}; + +// 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(const char* aName, UniquePtr<NotificationRef> aRef, + NotificationAction aAction) + : Runnable(aName), mNotificationRef(std::move(aRef)), mAction(aAction) {} + + NS_IMETHOD + Run() override; + + protected: + virtual ~NotificationTask() = default; + + UniquePtr<NotificationRef> mNotificationRef; + NotificationAction mAction; +}; + +uint32_t Notification::sCount = 0; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(NotificationPermissionRequest, + ContentPermissionRequestBase, mCallback) +NS_IMPL_ADDREF_INHERITED(NotificationPermissionRequest, + ContentPermissionRequestBase) +NS_IMPL_RELEASE_INHERITED(NotificationPermissionRequest, + ContentPermissionRequestBase) + +NS_IMPL_QUERY_INTERFACE_CYCLE_COLLECTION_INHERITED( + NotificationPermissionRequest, ContentPermissionRequestBase, nsIRunnable, + nsINamed) + +NS_IMETHODIMP +NotificationPermissionRequest::Run() { + bool isSystem = mPrincipal->IsSystemPrincipal(); + bool blocked = false; + if (isSystem) { + mPermission = NotificationPermission::Granted; + } else if ( + mPrincipal->GetPrivateBrowsingId() != 0 && + !StaticPrefs:: + dom_webnotifications_privateBrowsing_enableDespiteLimitations()) { + mPermission = NotificationPermission::Denied; + blocked = true; + } else { + // File are automatically granted permission. + + if (mPrincipal->SchemeIs("file")) { + mPermission = NotificationPermission::Granted; + } else if (!mWindow->IsSecureContext()) { + mPermission = NotificationPermission::Denied; + blocked = true; + nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); + if (doc) { + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, "DOM"_ns, doc, + nsContentUtils::eDOM_PROPERTIES, + "NotificationsInsecureRequestIsForbidden"); + } + } + } + + // We can't call ShowPrompt() directly here since our logic for determining + // whether to display a prompt depends on the checks above as well as the + // result of CheckPromptPrefs(). So we have to manually check the prompt + // prefs and decide what to do based on that. + PromptResult pr = CheckPromptPrefs(); + switch (pr) { + case PromptResult::Granted: + mPermission = NotificationPermission::Granted; + break; + case PromptResult::Denied: + mPermission = NotificationPermission::Denied; + break; + default: + // ignore + break; + } + + if (!mHasValidTransientUserGestureActivation && + !StaticPrefs::dom_webnotifications_requireuserinteraction()) { + nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); + if (doc) { + doc->WarnOnceAbout(Document::eNotificationsRequireUserGestureDeprecation); + } + } + + // Check this after checking the prompt prefs to make sure this pref overrides + // those. We rely on this for testing purposes. + if (!isSystem && !blocked && + !StaticPrefs::dom_webnotifications_allowcrossoriginiframe() && + !mPrincipal->Subsumes(mTopLevelPrincipal)) { + mPermission = NotificationPermission::Denied; + blocked = true; + nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); + if (doc) { + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, "DOM"_ns, doc, + nsContentUtils::eDOM_PROPERTIES, + "NotificationsCrossOriginIframeRequestIsForbidden"); + } + } + + if (mPermission != NotificationPermission::Default) { + return DispatchResolvePromise(); + } + + return nsContentPermissionUtils::AskPermission(this, mWindow); +} + +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::Handle<JS::Value> aChoices) { + MOZ_ASSERT(aChoices.isUndefined()); + + mPermission = NotificationPermission::Granted; + return DispatchResolvePromise(); +} + +inline nsresult NotificationPermissionRequest::DispatchResolvePromise() { + nsCOMPtr<nsIRunnable> resolver = + NewRunnableMethod("NotificationPermissionRequest::DispatchResolvePromise", + this, &NotificationPermissionRequest::ResolvePromise); + return nsGlobalWindowInner::Cast(mWindow.get())->Dispatch(resolver.forget()); +} + +nsresult NotificationPermissionRequest::ResolvePromise() { + nsresult rv = NS_OK; + // This will still be "default" if the user dismissed the doorhanger, + // or "denied" otherwise. + if (mPermission == NotificationPermission::Default) { + // When the front-end has decided to deny the permission request + // automatically and we are not handling user input, then log a + // warning in the current document that this happened because + // Notifications require a user gesture. + if (!mHasValidTransientUserGestureActivation && + StaticPrefs::dom_webnotifications_requireuserinteraction()) { + nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); + if (doc) { + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, "DOM"_ns, + doc, nsContentUtils::eDOM_PROPERTIES, + "NotificationsRequireUserGesture"); + } + } + + mPermission = Notification::TestPermission(mPrincipal); + } + if (mCallback) { + ErrorResult error; + RefPtr<NotificationPermissionCallback> callback(mCallback); + callback->Call(mPermission, error); + rv = error.StealNSResult(); + } + mPromise->MaybeResolve(mPermission); + return rv; +} + +// 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(std::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; +} + +// static +bool Notification::PrefEnabled(JSContext* aCx, JSObject* aObj) { + return StaticPrefs::dom_webnotifications_enabled(); +} + +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, bool aSilent, + nsTArray<uint32_t>&& aVibrate, + const NotificationBehavior& aBehavior) + : DOMEventTargetHelper(aGlobal), + mWorkerPrivate(nullptr), + mObserver(nullptr), + mID(aID), + mTitle(aTitle), + mBody(aBody), + mDir(aDir), + mLang(aLang), + mTag(aTag), + mIconUrl(aIconUrl), + mRequireInteraction(aRequireInteraction), + mSilent(aSilent), + mVibrate(std::move(aVibrate)), + mBehavior(aBehavior), + mData(JS::NullValue()), + mIsClosed(false), + mIsStored(false), + mTaskCount(0) { + if (!NS_IsMainThread()) { + 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( + "Notification constructor cannot be used in ServiceWorkerGlobalScope. " + "Use registration.showNotification() instead."); + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<Notification> notification = + CreateAndShow(aGlobal.Context(), global, aTitle, aOptions, u""_ns, 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 +Result<already_AddRefed<Notification>, QMResult> +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) { + 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; + IgnoredErrorResult rv; + RefPtr<Notification> notification = + CreateInternal(aGlobal, aID, aTitle, options, rv); + if (NS_WARN_IF(rv.Failed())) { + return Err(ToQMResult(NS_ERROR_FAILURE)); + } + + QM_TRY(notification->InitFromBase64(aData)); + + 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, ErrorResult& aRv) { + 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; + } + + bool silent = false; + if (StaticPrefs::dom_webnotifications_silent_enabled()) { + silent = aOptions.mSilent; + } + + nsTArray<uint32_t> vibrate; + if (StaticPrefs::dom_webnotifications_vibrate_enabled() && + aOptions.mVibrate.WasPassed()) { + if (silent) { + aRv.ThrowTypeError( + "Silent notifications must not specify vibration patterns."); + return nullptr; + } + + const OwningUnsignedLongOrUnsignedLongSequence& value = + aOptions.mVibrate.Value(); + if (value.IsUnsignedLong()) { + AutoTArray<uint32_t, 1> array; + array.AppendElement(value.GetAsUnsignedLong()); + vibrate = SanitizeVibratePattern(array); + } else { + vibrate = SanitizeVibratePattern(value.GetAsUnsignedLongSequence()); + } + } + + RefPtr<Notification> notification = new Notification( + aGlobal, id, aTitle, aOptions.mBody, aOptions.mDir, aOptions.mLang, + aOptions.mTag, aOptions.mIcon, aOptions.mRequireInteraction, silent, + std::move(vibrate), aOptions.mMozbehavior); + rv = notification->Init(); + NS_ENSURE_SUCCESS(rv, nullptr); + return notification.forget(); +} + +Notification::~Notification() { + mozilla::DropJSObjects(this); + AssertIsOnTargetThread(); + MOZ_ASSERT(!mWorkerRef); + 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_WEAK_REFERENCE +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(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_INLINE_DECL_REFCOUNTING_INHERITED(WorkerNotificationObserver, + MainThreadNotificationObserver) + NS_DECL_NSIOBSERVER + + explicit WorkerNotificationObserver(UniquePtr<NotificationRef> aRef) + : MainThreadNotificationObserver(std::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; + } + } +}; + +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() = default; + + 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) + +bool Notification::DispatchClickEvent() { + AssertIsOnTargetThread(); + RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr); + event->InitEvent(u"click"_ns, false, true); + event->SetTrusted(true); + WantsPopupControlCheck popupControlCheck(event); + return DispatchEvent(*event, CallerType::System, IgnoreErrors()); +} + +// 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, + "NotificationClickWorkerRunnable"), + 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); + mWorkerPrivate->DispatchToMainThread(r.forget()); + } + } +}; + +NS_IMETHODIMP +NotificationObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + AssertIsOnMainThread(); + + if (!strcmp("alertdisablecallback", aTopic)) { + 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(mPrincipal); + return NS_OK; + } else if (!strcmp("alertsettingscallback", aTopic)) { + if (XRE_IsParentProcess()) { + return Notification::OpenSettings(mPrincipal); + } + // `ContentParent::RecvOpenNotificationSettings` notifies observers in the + // parent process. + ContentChild::GetSingleton()->SendOpenNotificationSettings(mPrincipal); + return NS_OK; + } else if (!strcmp("alertshow", aTopic) || !strcmp("alertfinished", aTopic)) { + Unused << NS_WARN_IF(NS_FAILED(AdjustPushQuota(aTopic))); + } + + 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()); +} + +// MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See +// bug 1539845. +MOZ_CAN_RUN_SCRIPT_BOUNDARY 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) { + nsCOMPtr<nsPIDOMWindowOuter> outerWindow = window->GetOuterWindow(); + nsFocusManager::FocusWindow(outerWindow, CallerType::System); + } + } else if (!strcmp("alertfinished", aTopic)) { + notification->UnpersistNotification(); + notification->mIsClosed = true; + notification->DispatchTrustedEvent(u"close"_ns); + } else if (!strcmp("alertshow", aTopic)) { + notification->DispatchTrustedEvent(u"show"_ns); + } + 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>( + "WorkerNotificationObserver::Observe::nsPIDOMWindowInner", window)); + + r = new NotificationClickWorkerRunnable(notification, windowHandle); + } else if (!strcmp("alertfinished", aTopic)) { + notification->UnpersistNotification(); + notification->mIsClosed = true; + r = new NotificationEventWorkerRunnable(notification, u"close"_ns); + } else if (!strcmp("alertshow", aTopic)) { + r = new NotificationEventWorkerRunnable(notification, u"show"_ns); + } + + 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; + } + + if (!strcmp("alertclickcallback", aTopic)) { + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIServiceWorkerManager> swm = + mozilla::components::ServiceWorkerManager::Service(); + if (NS_WARN_IF(!swm)) { + return NS_ERROR_FAILURE; + } + + rv = swm->SendNotificationClickEvent( + originSuffix, NS_ConvertUTF16toUTF8(mScope), mID, mTitle, mDir, mLang, + mBody, mTag, mIcon, mData, mBehavior); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } else { + auto* cc = ContentChild::GetSingleton(); + NotificationEventData data(originSuffix, NS_ConvertUTF16toUTF8(mScope), + mID, mTitle, mDir, mLang, mBody, mTag, mIcon, + mData, mBehavior); + Unused << cc->SendNotificationEvent(u"click"_ns, data); + } + 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); + } + + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIServiceWorkerManager> swm = + mozilla::components::ServiceWorkerManager::Service(); + if (NS_WARN_IF(!swm)) { + return NS_ERROR_FAILURE; + } + + rv = swm->SendNotificationCloseEvent( + originSuffix, NS_ConvertUTF16toUTF8(mScope), mID, mTitle, mDir, mLang, + mBody, mTag, mIcon, mData, mBehavior); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } else { + auto* cc = ContentChild::GetSingleton(); + NotificationEventData data(originSuffix, NS_ConvertUTF16toUTF8(mScope), + mID, mTitle, mDir, mLang, mBody, mTag, mIcon, + mData, mBehavior); + Unused << cc->SendNotificationEvent(u"close"_ns, data); + } + return NS_OK; + } + + return NS_OK; +} + +bool Notification::IsInPrivateBrowsing() { + AssertIsOnMainThread(); + + Document* 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; +} + +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; + std::swap(ownership, mTempRef); + MOZ_ASSERT(ownership->GetNotification() == this); + + nsresult rv = PersistNotification(); + if (NS_FAILED(rv)) { + NS_WARNING("Could not persist Notification"); + } + + nsCOMPtr<nsIAlertsService> alertService = components::Alerts::Service(); + + 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, u"error"_ns); + if (!r->Dispatch()) { + NS_WARNING("Could not dispatch event to worker notification"); + } + } else { + DispatchTrustedEvent(u"error"_ns); + } + 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(std::move(ownership)); + observer = mObserver; + } else { + observer = new MainThreadNotificationObserver(std::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 = u"notification:"_ns; + uniqueCookie.AppendInt(sCount++); + bool inPrivateBrowsing = IsInPrivateBrowsing(); + + bool requireInteraction = mRequireInteraction; + if (!StaticPrefs::dom_webnotifications_requireinteraction_enabled()) { + 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, mSilent, mVibrate); + NS_ENSURE_SUCCESS_VOID(rv); + + if (isPersistent) { + JSONStringWriteFunc<nsAutoCString> persistentData; + JSONWriter w(persistentData); + w.Start(); + + nsAutoString origin; + Notification::GetOrigin(principal, origin); + w.StringProperty("origin", NS_ConvertUTF16toUTF8(origin)); + + w.StringProperty("id", NS_ConvertUTF16toUTF8(mID)); + + nsAutoCString originSuffix; + principal->GetOriginSuffix(originSuffix); + w.StringProperty("originSuffix", originSuffix); + + w.End(); + + alertService->ShowPersistentNotification( + NS_ConvertUTF8toUTF16(persistentData.StringCRef()), 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(); +} + +// static +already_AddRefed<Promise> Notification::RequestPermission( + const GlobalObject& aGlobal, + const Optional<OwningNonNull<NotificationPermissionCallback> >& aCallback, + ErrorResult& aRv) { + AssertIsOnMainThread(); + + // 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 || !window) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); + if (!principal) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(window->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + NotificationPermissionCallback* permissionCallback = nullptr; + if (aCallback.WasPassed()) { + permissionCallback = &aCallback.Value(); + } + nsCOMPtr<nsIRunnable> request = new NotificationPermissionRequest( + principal, window, promise, permissionCallback); + + window->AsGlobal()->Dispatch(request.forget()); + + 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->GetAsInnerWindow(), aRv); + } else { + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + RefPtr<GetPermissionRunnable> r = new GetPermissionRunnable(worker); + r->Dispatch(Canceling, aRv); + if (aRv.Failed()) { + return NotificationPermission::Denied; + } + + return r->GetPermission(); + } +} + +/* static */ +NotificationPermission Notification::GetPermissionInternal( + nsPIDOMWindowInner* aWindow, ErrorResult& aRv) { + // Get principal from global to check permission for notifications. + nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow); + if (!sop) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return NotificationPermission::Denied; + } + + nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); + if (!principal) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return NotificationPermission::Denied; + } + + if (principal->GetPrivateBrowsingId() != 0 && + !StaticPrefs:: + dom_webnotifications_privateBrowsing_enableDespiteLimitations()) { + return NotificationPermission::Denied; + } + // Disallow showing notification if our origin is not the same origin as the + // toplevel one, see https://github.com/whatwg/notifications/issues/177. + if (!StaticPrefs::dom_webnotifications_allowcrossoriginiframe()) { + nsCOMPtr<nsIScriptObjectPrincipal> topSop = + do_QueryInterface(aWindow->GetBrowsingContext()->Top()->GetDOMWindow()); + nsIPrincipal* topPrincipal = topSop ? topSop->GetPrincipal() : nullptr; + if (!topPrincipal || !principal->Subsumes(topPrincipal)) { + return NotificationPermission::Denied; + } + } + + return GetPermissionInternal(principal, aRv); +} + +/* static */ +NotificationPermission Notification::GetPermissionInternal( + nsIPrincipal* aPrincipal, ErrorResult& aRv) { + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + if (aPrincipal->IsSystemPrincipal()) { + return NotificationPermission::Granted; + } else { + // Allow files to show notifications by default. + if (aPrincipal->SchemeIs("file")) { + 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 = + components::PermissionManager::Service(); + if (!permissionManager) { + return NotificationPermission::Default; + } + + permissionManager->TestExactPermissionFromPrincipal( + aPrincipal, "desktop-notification"_ns, &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; + + nsIURI* baseUri = nullptr; + + // 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. + auto encoding = UTF_8_ENCODING; + + if (mWorkerPrivate) { + baseUri = mWorkerPrivate->GetBaseURI(); + } else { + Document* doc = GetOwner() ? GetOwner()->GetExtantDoc() : nullptr; + if (doc) { + baseUri = doc->GetBaseURI(); + encoding = doc->GetDocumentCharacterSet(); + } 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, encoding, baseUri); + if (NS_SUCCEEDED(rv)) { + nsAutoCString src; + srcUri->GetSpec(src); + CopyUTF8toUTF16(src, iconUrl); + } + } + if (mBehavior.mSoundFile.Length() > 0) { + nsCOMPtr<nsIURI> srcUri; + rv = NS_NewURI(getter_AddRefs(srcUri), mBehavior.mSoundFile, encoding, + baseUri); + if (NS_SUCCEEDED(rv)) { + nsAutoCString src; + srcUri->GetSpec(src); + CopyUTF8toUTF16(src, soundUrl); + } + } + } + + return rv; +} + +already_AddRefed<Promise> Notification::Get( + nsPIDOMWindowInner* aWindow, const GetNotificationOptions& aFilter, + const nsAString& aScope, ErrorResult& aRv) { + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + + nsCOMPtr<Document> doc = aWindow->GetExtantDoc(); + if (!doc) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + nsString origin; + aRv = GetOrigin(doc->NodePrincipal(), origin); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(aWindow->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + nsCOMPtr<nsINotificationStorageCallback> callback = + new NotificationStorageCallback(aWindow->AsGlobal(), aScope, promise); + + RefPtr<NotificationGetRunnable> r = + new NotificationGetRunnable(origin, aFilter.mTag, callback); + + aRv = aWindow->AsGlobal()->Dispatch(r.forget()); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return promise.forget(); +} + +class WorkerGetResultRunnable final : public NotificationWorkerRunnable { + RefPtr<PromiseWorkerProxy> mPromiseProxy; + const nsTArray<NotificationStrings> mStrings; + + public: + WorkerGetResultRunnable(WorkerPrivate* aWorkerPrivate, + PromiseWorkerProxy* aPromiseProxy, + nsTArray<NotificationStrings>&& aStrings) + : NotificationWorkerRunnable(aWorkerPrivate, "WorkerGetResultRunnable"), + mPromiseProxy(aPromiseProxy), + mStrings(std::move(aStrings)) {} + + void WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override { + RefPtr<Promise> workerPromise = mPromiseProxy->GetWorkerPromise(); + // Once Worker had already started shutdown, workerPromise would be nullptr + if (!workerPromise) { + return; + } + + AutoTArray<RefPtr<Notification>, 5> notifications; + for (uint32_t i = 0; i < mStrings.Length(); ++i) { + auto result = 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); + if (result.isErr()) { + continue; + } + RefPtr<Notification> n = result.unwrap(); + n->SetStoredState(true); + 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 = std::move(mPromiseProxy); + MutexAutoLock lock(proxy->Lock()); + if (proxy->CleanedUp()) { + return NS_OK; + } + + RefPtr<WorkerGetResultRunnable> r = new WorkerGetResultRunnable( + proxy->GetWorkerPrivate(), proxy, std::move(mStrings)); + + r->Dispatch(); + return NS_OK; + } + + private: + ~WorkerGetCallback() = default; +}; + +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) + : Runnable("WorkerGetRunnable"), + 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() = default; +}; + +// static +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(aWorkerPrivate->DispatchToMainThread(r.forget())); + return p.forget(); +} + +JSObject* Notification::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return mozilla::dom::Notification_Binding::Wrap(aCx, this, aGivenProto); +} + +void Notification::Close() { + AssertIsOnTargetThread(); + auto ref = MakeUnique<NotificationRef>(this); + if (!ref->Initialized()) { + return; + } + + nsCOMPtr<nsIRunnable> closeNotificationTask = new NotificationTask( + "Notification::Close", std::move(ref), NotificationTask::eClose); + nsresult rv = DispatchToMainThread(closeNotificationTask.forget()); + + if (NS_FAILED(rv)) { + DispatchTrustedEvent(u"error"_ns); + // If dispatch fails, NotificationTask will release the ref when it goes + // out of scope at the end of this function. + } +} + +void Notification::CloseInternal(bool aContextClosed) { + 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; + std::swap(ownership, mTempRef); + + SetAlertName(); + UnpersistNotification(); + if (!mIsClosed) { + nsCOMPtr<nsIAlertsService> alertService = components::Alerts::Service(); + if (alertService) { + nsAutoString alertName; + GetAlertName(alertName); + alertService->CloseAlert(alertName, aContextClosed); + } + } +} + +nsresult Notification::GetOrigin(nsIPrincipal* aPrincipal, nsString& aOrigin) { + if (!aPrincipal) { + return NS_ERROR_FAILURE; + } + + nsresult rv = + nsContentUtils::GetWebExposedOriginSerialization(aPrincipal, aOrigin); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +bool Notification::RequireInteraction() const { return mRequireInteraction; } + +bool Notification::Silent() const { return mSilent; } + +void Notification::GetVibrate(nsTArray<uint32_t>& aRetval) const { + aRetval = mVibrate.Clone(); +} + +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; + } + + aRv = dataObjectContainer->GetDataAsBase64(mDataAsBase64); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +Result<Ok, QMResult> Notification::InitFromBase64(const nsAString& aData) { + MOZ_ASSERT(mDataAsBase64.IsEmpty()); + if (aData.IsEmpty()) { + // No data; skipping + return Ok(); + } + + // To and fro to ensure it is valid base64. + RefPtr<nsStructuredCloneContainer> container = + new nsStructuredCloneContainer(); + QM_TRY(QM_TO_RESULT( + container->InitFromBase64(aData, JS_STRUCTURED_CLONE_VERSION))); + QM_TRY(QM_TO_RESULT(container->GetDataAsBase64(mDataAsBase64))); + + return Ok(); +} + +bool Notification::AddRefObject() { + AssertIsOnTargetThread(); + MOZ_ASSERT_IF(mWorkerPrivate && !mWorkerRef, mTaskCount == 0); + MOZ_ASSERT_IF(mWorkerPrivate && mWorkerRef, mTaskCount > 0); + if (mWorkerPrivate && !mWorkerRef) { + if (!CreateWorkerRef()) { + return false; + } + } + AddRef(); + ++mTaskCount; + return true; +} + +void Notification::ReleaseObject() { + AssertIsOnTargetThread(); + MOZ_ASSERT(mTaskCount > 0); + MOZ_ASSERT_IF(mWorkerPrivate, mWorkerRef); + + --mTaskCount; + if (mWorkerPrivate && mTaskCount == 0) { + MOZ_ASSERT(mWorkerRef); + mWorkerRef = nullptr; + } + Release(); +} + +/* + * 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, + "Notification :: Close Notification"_ns), + 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 Notification::CreateWorkerRef() { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!mWorkerRef); + + RefPtr<Notification> self = this; + mWorkerRef = + StrongWorkerRef::Create(mWorkerPrivate, "Notification", [self]() { + // 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. + + // Dispatched to main thread, blocks on closing the Notification. + RefPtr<CloseNotificationRunnable> r = + new CloseNotificationRunnable(self); + ErrorResult rv; + r->Dispatch(Killing, 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()) { + self->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! + }); + + if (NS_WARN_IF(!mWorkerRef)) { + return false; + } + + return true; +} + +/* + * 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; + ServiceWorkerRegistrationDescriptor mDescriptor; + + public: + explicit CheckLoadRunnable( + WorkerPrivate* aWorker, const nsACString& aScope, + const ServiceWorkerRegistrationDescriptor& aDescriptor) + : WorkerMainThreadRunnable(aWorker, "Notification :: Check Load"_ns), + mRv(NS_ERROR_DOM_SECURITY_ERR), + mScope(aScope), + mDescriptor(aDescriptor) {} + + bool MainThreadRun() override { + nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); + mRv = CheckScope(principal, mScope, mWorkerPrivate->WindowID()); + + if (NS_FAILED(mRv)) { + return true; + } + + auto activeWorker = mDescriptor.GetActive(); + + if (!activeWorker || + activeWorker.ref().Id() != mWorkerPrivate->ServiceWorkerID()) { + mRv = NS_ERROR_NOT_AVAILABLE; + } + + return true; + } + + nsresult Result() { return mRv; } +}; + +// Step 2, 5, 6 of +// https://notifications.spec.whatwg.org/#dom-serviceworkerregistration-shownotification +/* static */ +already_AddRefed<Promise> Notification::ShowPersistentNotification( + JSContext* aCx, nsIGlobalObject* aGlobal, const nsAString& aScope, + const nsAString& aTitle, const NotificationOptions& aOptions, + const ServiceWorkerRegistrationDescriptor& aDescriptor, 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; + } + + uint64_t windowID = 0; + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal); + if (win) { + windowID = win->WindowID(); + } + + aRv = CheckScope(principal, NS_ConvertUTF16toUTF8(aScope), windowID); + 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), aDescriptor); + loadChecker->Dispatch(Canceling, 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>(NS_ConvertUTF16toUTF8(aScope)); + } else { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + } + return nullptr; + } + } + + // Step 2: Let promise be a new promise in this’s relevant Realm. + 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. + // XXX: GetPermission is a synchronous blocking function on workers. + NotificationPermission permission = GetPermission(aGlobal, aRv); + + // Step 6.1: If the result of getting the notifications permission state is + // not "granted", then queue a global task on the DOM manipulation task source + // given global to reject promise with a TypeError, and abort these steps. + if (NS_WARN_IF(aRv.Failed()) || + permission != NotificationPermission::Granted) { + p->MaybeRejectWithTypeError("Permission to show Notification denied."); + 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(); + + // Step 5: Let notification be the result of creating a notification given + // title, options, this’s relevant settings object, and + // serviceWorkerRegistration. If this threw an exception, then reject promise + // with that exception and return promise. + // + // XXX: This should happen before the permission check per the spec, as this + // can throw errors too. This should be split into create and show. + 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, u""_ns, aTitle, aOptions, aRv); + if (aRv.Failed()) { + return nullptr; + } + + // 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( + "Notification::CreateAndShow", std::move(ref), NotificationTask::eShow); + + nsresult rv = + notification->DispatchToMainThread(showNotificationTask.forget()); + + if (NS_WARN_IF(NS_FAILED(rv))) { + notification->DispatchTrustedEvent(u"error"_ns); + } + + return notification.forget(); +} + +/* static */ +nsresult Notification::RemovePermission(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + nsCOMPtr<nsIPermissionManager> permissionManager = + mozilla::components::PermissionManager::Service(); + if (!permissionManager) { + return NS_ERROR_FAILURE; + } + permissionManager->RemoveFromPrincipal(aPrincipal, "desktop-notification"_ns); + 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(true); + } + } + + return NS_OK; +} + +nsresult Notification::DispatchToMainThread( + already_AddRefed<nsIRunnable>&& aRunnable) { + if (mWorkerPrivate) { + return mWorkerPrivate->DispatchToMainThread(std::move(aRunnable)); + } + AssertIsOnMainThread(); + return NS_DispatchToCurrentThread(std::move(aRunnable)); +} + +} // namespace mozilla::dom |