/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #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/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 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, 5> notifications; for (uint32_t i = 0; i < mStrings.Length(); ++i) { RefPtr 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 mWindow; RefPtr 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 mCallback; public: NotificationGetRunnable(const nsAString& aOrigin, const nsAString& aTag, nsINotificationStorageCallback* aCallback) : mOrigin(aOrigin), mTag(aTag), mCallback(aCallback) {} NS_IMETHOD Run() override { nsresult rv; nsCOMPtr 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 mPrincipal; nsCOMPtr mWindow; NotificationPermission mPermission; RefPtr mPromise; RefPtr mCallback; nsCOMPtr 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 mWindow; public: explicit FocusWindowRunnable(const nsMainThreadPtrHandle& 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 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 r = new ReleaseNotificationRunnable(notification); if (!r->Dispatch()) { RefPtr 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 aRef, NotificationAction aAction) : mNotificationRef(Move(aRef)), mAction(aAction) {} NS_IMETHOD Run() override; protected: virtual ~NotificationTask() {} UniquePtr 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 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 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(); } mPromise->MaybeResolve(mPermission); return rv; } NS_IMETHODIMP NotificationPermissionRequest::GetTypes(nsIArray** aTypes) { nsTArray emptyOptions; return nsContentPermissionUtils::CreatePermissionArray(NS_LITERAL_CSTRING("desktop-notification"), NS_LITERAL_CSTRING("unused"), emptyOptions, aTypes); } // 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 mObserver; nsCOMPtr 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 mNotificationRef; NS_DECL_ISUPPORTS NS_DECL_NSIOBSERVER explicit MainThreadNotificationObserver(UniquePtr 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 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::Constructor(const GlobalObject& aGlobal, const nsAString& aTitle, const NotificationOptions& aOptions, ErrorResult& aRv) { // FIXME(nsm): If the sticky flag is set, throw an error. RefPtr scope; UNWRAP_OBJECT(ServiceWorkerGlobalScope, aGlobal.Get(), scope); if (scope) { aRv.ThrowTypeError(); return nullptr; } nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); RefPtr 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::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 options(RootingCx()); options.mDir = Notification::StringToDirection(nsString(aDir)); options.mLang = aLang; options.mBody = aBody; options.mTag = aTag; options.mIcon = aIcon; RefPtr 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 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 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::CreateInternal(nsIGlobalObject* aGlobal, const nsAString& aID, const nsAString& aTitle, const NotificationOptions& aOptions) { nsresult rv; nsString id; if (!aID.IsEmpty()) { id = aID; } else { nsCOMPtr 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 = 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 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 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 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 target = mWorkerPrivate->GlobalScope(); RefPtr 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 = 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 mWindow; public: NotificationClickWorkerRunnable(Notification* aNotification, const nsMainThreadPtrHandle& 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 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)) { 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("alertsettingscallback", aTopic)) { 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)) { Unused << NS_WARN_IF(NS_FAILED(AdjustPushQuota(aTopic))); } return mObserver->Observe(aSubject, aTopic, aData); } nsresult NotificationObserver::AdjustPushQuota(const char* aTopic) { nsCOMPtr 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 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 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 windowHandle( new nsMainThreadPtrHolder(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 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 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 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 loadGroup = mWorkerPrivate->GetLoadGroup(); nsCOMPtr 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 ownership; mozilla::Swap(ownership, mTempRef); MOZ_ASSERT(ownership->GetNotification() == this); nsresult rv = PersistNotification(); if (NS_FAILED(rv)) { NS_WARNING("Could not persist Notification"); } nsCOMPtr 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 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 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 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 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(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 Notification::RequestPermission(const GlobalObject& aGlobal, const Optional >& aCallback, ErrorResult& aRv) { // Get principal from global to make permission request for notifications. nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); nsCOMPtr sop = do_QueryInterface(aGlobal.GetAsSupports()); if (!sop) { aRv.Throw(NS_ERROR_UNEXPECTED); return nullptr; } nsCOMPtr principal = sop->GetPrincipal(); nsCOMPtr global = do_QueryInterface(window); RefPtr promise = Promise::Create(global, aRv); if (aRv.Failed()) { return nullptr; } NotificationPermissionCallback* permissionCallback = nullptr; if (aCallback.WasPassed()) { permissionCallback = &aCallback.Value(); } nsCOMPtr request = new NotificationPermissionRequest(principal, window, promise, permissionCallback); NS_DispatchToMainThread(request); return promise.forget(); } // static NotificationPermission Notification::GetPermission(const GlobalObject& aGlobal, ErrorResult& aRv) { nsCOMPtr 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 r = new GetPermissionRunnable(worker); r->Dispatch(Terminating, 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 sop = do_QueryInterface(aGlobal); if (!sop) { aRv.Throw(NS_ERROR_UNEXPECTED); return NotificationPermission::Denied; } nsCOMPtr 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 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 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 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 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 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 Notification::Get(nsPIDOMWindowInner* aWindow, const GetNotificationOptions& aFilter, const nsAString& aScope, ErrorResult& aRv) { MOZ_ASSERT(aWindow); nsCOMPtr 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 global = do_QueryInterface(aWindow); RefPtr promise = Promise::Create(global, aRv); if (aRv.Failed()) { return nullptr; } nsCOMPtr callback = new NotificationStorageCallback(global, aScope, promise); RefPtr r = new NotificationGetRunnable(origin, aFilter.mTag, callback); aRv = NS_DispatchToMainThread(r); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } return promise.forget(); } already_AddRefed Notification::Get(const GlobalObject& aGlobal, const GetNotificationOptions& aFilter, ErrorResult& aRv) { AssertIsOnMainThread(); nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); MOZ_ASSERT(global); nsCOMPtr window = do_QueryInterface(global); return Get(window, aFilter, EmptyString(), aRv); } class WorkerGetResultRunnable final : public NotificationWorkerRunnable { RefPtr mPromiseProxy; const nsTArray mStrings; public: WorkerGetResultRunnable(WorkerPrivate* aWorkerPrivate, PromiseWorkerProxy* aPromiseProxy, const nsTArray&& aStrings) : NotificationWorkerRunnable(aWorkerPrivate) , mPromiseProxy(aPromiseProxy) , mStrings(Move(aStrings)) { } void WorkerRunInternal(WorkerPrivate* aWorkerPrivate) override { RefPtr workerPromise = mPromiseProxy->WorkerPromise(); ErrorResult result; AutoTArray, 5> notifications; for (uint32_t i = 0; i < mStrings.Length(); ++i) { RefPtr 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 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 proxy = mPromiseProxy.forget(); MutexAutoLock lock(proxy->Lock()); if (proxy->CleanedUp()) { return NS_OK; } RefPtr 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 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 callback = new WorkerGetCallback(mPromiseProxy, mScope); nsresult rv; nsCOMPtr 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 Notification::WorkerGet(WorkerPrivate* aWorkerPrivate, const GetNotificationOptions& aFilter, const nsAString& aScope, ErrorResult& aRv) { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); RefPtr p = Promise::Create(aWorkerPrivate->GlobalScope(), aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr proxy = PromiseWorkerProxy::Create(aWorkerPrivate, p); if (!proxy) { aRv.Throw(NS_ERROR_DOM_ABORT_ERR); return nullptr; } RefPtr 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 aGivenProto) { return mozilla::dom::NotificationBinding::Wrap(aCx, this, aGivenProto); } void Notification::Close() { AssertIsOnTargetThread(); auto ref = MakeUnique(this); if (!ref->Initialized()) { return; } nsCOMPtr 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 ownership; mozilla::Swap(ownership, mTempRef); SetAlertName(); UnpersistNotification(); if (!mIsClosed) { nsCOMPtr 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 aRetval) { if (mData.isNull() && !mDataAsBase64.IsEmpty()) { nsresult rv; RefPtr container = new nsStructuredCloneContainer(); rv = container->InitFromBase64(mDataAsBase64, JS_STRUCTURED_CLONE_VERSION); if (NS_WARN_IF(NS_FAILED(rv))) { aRetval.setNull(); return; } JS::Rooted 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 aData, ErrorResult& aRv) { if (!mDataAsBase64.IsEmpty() || aData.isNull()) { return; } RefPtr 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 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 kungFuDeathGrip = mNotification; // Dispatched to main thread, blocks on closing the Notification. RefPtr r = new CloseNotificationRunnable(kungFuDeathGrip); 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()) { 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(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 swm = ServiceWorkerManager::GetInstance(); if (!swm) { // browser shutdown began mRv = NS_ERROR_FAILURE; return true; } RefPtr 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 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 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 loadChecker = new CheckLoadRunnable(worker, NS_ConvertUTF16toUTF8(aScope)); loadChecker->Dispatch(Terminating, aRv); if (aRv.Failed()) { return nullptr; } if (NS_WARN_IF(NS_FAILED(loadChecker->Result()))) { if (loadChecker->Result() == NS_ERROR_NOT_AVAILABLE) { aRv.ThrowTypeError(aScope); } else { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); } return nullptr; } } RefPtr 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(); 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 = CreateAndShow(aCx, aGlobal, aTitle, aOptions, aScope, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } return p.forget(); } /* static */ already_AddRefed Notification::CreateAndShow(JSContext* aCx, nsIGlobalObject* aGlobal, const nsAString& aTitle, const NotificationOptions& aOptions, const nsAString& aScope, ErrorResult& aRv) { MOZ_ASSERT(aGlobal); RefPtr notification = CreateInternal(aGlobal, EmptyString(), aTitle, aOptions); // Make a structured clone of the aOptions.mData object JS::Rooted data(aCx, aOptions.mData); notification->InitFromJSVal(aCx, data, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } notification->SetScope(aScope); auto ref = MakeUnique(notification); if (NS_WARN_IF(!ref->Initialized())) { aRv.Throw(NS_ERROR_DOM_ABORT_ERR); return nullptr; } // Queue a task to show the notification. nsCOMPtr 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 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 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 window = GetOwner(); if (SameCOMIdentity(aSubject, window)) { nsCOMPtr 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