summaryrefslogtreecommitdiffstats
path: root/dom/notification
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/notification
parentInitial commit. (diff)
downloadfirefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.tar.xz
firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/notification')
-rw-r--r--dom/notification/Notification.cpp2333
-rw-r--r--dom/notification/Notification.h360
-rw-r--r--dom/notification/NotificationEvent.cpp21
-rw-r--r--dom/notification/NotificationEvent.h65
-rw-r--r--dom/notification/NotificationStorage.sys.mjs205
-rw-r--r--dom/notification/components.conf14
-rw-r--r--dom/notification/moz.build48
-rw-r--r--dom/notification/new/NotificationDB.sys.mjs375
-rw-r--r--dom/notification/old/NotificationDB.sys.mjs386
-rw-r--r--dom/notification/test/browser/browser.toml4
-rw-r--r--dom/notification/test/browser/browser_permission_dismiss.js231
-rw-r--r--dom/notification/test/browser/notification.html11
-rw-r--r--dom/notification/test/chrome/chrome.toml3
-rw-r--r--dom/notification/test/chrome/test_notification_system_principal.xhtml76
-rw-r--r--dom/notification/test/mochitest/MockServices.js176
-rw-r--r--dom/notification/test/mochitest/NotificationTest.js102
-rw-r--r--dom/notification/test/mochitest/blank.html4
-rw-r--r--dom/notification/test/mochitest/create_notification.html16
-rw-r--r--dom/notification/test/mochitest/mochitest.toml30
-rw-r--r--dom/notification/test/mochitest/test_notification_basics.html125
-rw-r--r--dom/notification/test/mochitest/test_notification_crossorigin_iframe.html67
-rw-r--r--dom/notification/test/mochitest/test_notification_insecure_context.html42
-rw-r--r--dom/notification/test/mochitest/test_notification_permissions.html68
-rw-r--r--dom/notification/test/mochitest/test_notification_tag.html169
-rw-r--r--dom/notification/test/unit/head_notificationdb.js61
-rw-r--r--dom/notification/test/unit/test_notificationdb.js340
-rw-r--r--dom/notification/test/unit/test_notificationdb_bug1024090.js59
-rw-r--r--dom/notification/test/unit/test_notificationdb_migration.js129
-rw-r--r--dom/notification/test/unit/xpcshell.toml9
29 files changed, 5529 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
diff --git a/dom/notification/Notification.h b/dom/notification/Notification.h
new file mode 100644
index 0000000000..4ffa69cf49
--- /dev/null
+++ b/dom/notification/Notification.h
@@ -0,0 +1,360 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_notification_h__
+#define mozilla_dom_notification_h__
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/NotificationBinding.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+
+#include "nsIObserver.h"
+#include "nsISupports.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsWeakReference.h"
+
+class nsIPrincipal;
+class nsIVariant;
+
+namespace mozilla::dom {
+
+class NotificationRef;
+class WorkerNotificationObserver;
+class Promise;
+class StrongWorkerRef;
+
+/*
+ * Notifications on workers introduce some lifetime issues. The property we
+ * are trying to satisfy is:
+ * Whenever a task is dispatched to the main thread to operate on
+ * a Notification, the Notification should be addrefed on the worker thread
+ * and a feature should be added to observe the worker lifetime. This main
+ * thread owner should ensure it properly releases the reference to the
+ * Notification, additionally removing the feature if necessary.
+ *
+ * To enforce the correct addref and release, along with managing the feature,
+ * we introduce a NotificationRef. Only one object may ever own
+ * a NotificationRef, so UniquePtr<> is used throughout. The NotificationRef
+ * constructor calls AddRefObject(). When it is destroyed (on any thread) it
+ * releases the Notification on the correct thread.
+ *
+ * Code should only access the underlying Notification object when it can
+ * guarantee that it retains ownership of the NotificationRef while doing so.
+ *
+ * The one kink in this mechanism is that the worker feature may be Notify()ed
+ * if the worker stops running script, even if the Notification's corresponding
+ * UI is still visible to the user. We handle this case with the following
+ * steps:
+ * a) Close the notification. This is done by blocking the worker on the main
+ * thread. This ensures that there are no main thread holders when the worker
+ * resumes. This also deals with the case where Notify() runs on the worker
+ * before the observer has been created on the main thread. Even in such
+ * a situation, the CloseNotificationRunnable() will only run after the
+ * Show task that was previously queued. Since the show task is only queued
+ * once when the Notification is created, we can be sure that no new tasks
+ * will follow the Notify().
+ *
+ * b) Ask the observer to let go of its NotificationRef's underlying
+ * Notification without proper cleanup since the feature will handle the
+ * release. This is only OK because every notification has only one
+ * associated observer. The NotificationRef itself is still owned by the
+ * observer and deleted by the UniquePtr, but it doesn't do anything since
+ * the underlying Notification is null.
+ *
+ * To unify code-paths, we use the same NotificationRef in the main
+ * thread implementation too.
+ *
+ * Note that the Notification's JS wrapper does it's standard
+ * AddRef()/Release() and is not affected by any of this.
+ *
+ * Since the worker event queue can have runnables that will dispatch events on
+ * the Notification, the NotificationRef destructor will first try to release
+ * the Notification by dispatching a normal runnable to the worker so that it is
+ * queued after any event runnables. If that dispatch fails, it means the worker
+ * is no longer running and queued WorkerRunnables will be canceled, so we
+ * dispatch a control runnable instead.
+ *
+ */
+class Notification : public DOMEventTargetHelper,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ friend class CloseNotificationRunnable;
+ friend class NotificationTask;
+ friend class NotificationPermissionRequest;
+ friend class MainThreadNotificationObserver;
+ friend class NotificationStorageCallback;
+ friend class ServiceWorkerNotificationObserver;
+ friend class WorkerGetRunnable;
+ friend class WorkerNotificationObserver;
+
+ public:
+ IMPL_EVENT_HANDLER(click)
+ IMPL_EVENT_HANDLER(show)
+ IMPL_EVENT_HANDLER(error)
+ IMPL_EVENT_HANDLER(close)
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(Notification,
+ DOMEventTargetHelper)
+ NS_DECL_NSIOBSERVER
+
+ static bool PrefEnabled(JSContext* aCx, JSObject* aObj);
+
+ static already_AddRefed<Notification> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aTitle,
+ const NotificationOptions& aOption, ErrorResult& aRv);
+
+ /**
+ * Used when dispatching the ServiceWorkerEvent.
+ *
+ * Does not initialize the Notification's behavior.
+ * This is because:
+ * 1) The Notification is not shown to the user and so the behavior
+ * parameters don't matter.
+ * 2) The default binding requires main thread for parsing the JSON from the
+ * string behavior.
+ */
+ static Result<already_AddRefed<Notification>, QMResult> 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);
+
+ void GetID(nsAString& aRetval) { aRetval = mID; }
+
+ void GetTitle(nsAString& aRetval) { aRetval = mTitle; }
+
+ NotificationDirection Dir() { return mDir; }
+
+ void GetLang(nsAString& aRetval) { aRetval = mLang; }
+
+ void GetBody(nsAString& aRetval) { aRetval = mBody; }
+
+ void GetTag(nsAString& aRetval) { aRetval = mTag; }
+
+ void GetIcon(nsAString& aRetval) { aRetval = mIconUrl; }
+
+ void SetStoredState(bool val) { mIsStored = val; }
+
+ bool IsStored() { return mIsStored; }
+
+ static bool RequestPermissionEnabledForScope(JSContext* aCx,
+ JSObject* /* unused */);
+
+ static already_AddRefed<Promise> RequestPermission(
+ const GlobalObject& aGlobal,
+ const Optional<OwningNonNull<NotificationPermissionCallback> >& aCallback,
+ ErrorResult& aRv);
+
+ static NotificationPermission GetPermission(const GlobalObject& aGlobal,
+ ErrorResult& aRv);
+
+ static already_AddRefed<Promise> Get(nsPIDOMWindowInner* aWindow,
+ const GetNotificationOptions& aFilter,
+ const nsAString& aScope,
+ ErrorResult& aRv);
+
+ static already_AddRefed<Promise> WorkerGet(
+ WorkerPrivate* aWorkerPrivate, const GetNotificationOptions& aFilter,
+ const nsAString& aScope, ErrorResult& aRv);
+
+ // Notification implementation of
+ // ServiceWorkerRegistration.showNotification.
+ //
+ //
+ // Note that aCx may not be in the compartment of aGlobal, but aOptions will
+ // have its JS things in the compartment of aCx.
+ static already_AddRefed<Promise> ShowPersistentNotification(
+ JSContext* aCx, nsIGlobalObject* aGlobal, const nsAString& aScope,
+ const nsAString& aTitle, const NotificationOptions& aOptions,
+ const ServiceWorkerRegistrationDescriptor& aDescriptor, ErrorResult& aRv);
+
+ void Close();
+
+ nsPIDOMWindowInner* GetParentObject() { return GetOwner(); }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ bool RequireInteraction() const;
+
+ bool Silent() const;
+
+ void GetVibrate(nsTArray<uint32_t>& aRetval) const;
+
+ void GetData(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval);
+
+ void InitFromJSVal(JSContext* aCx, JS::Handle<JS::Value> aData,
+ ErrorResult& aRv);
+
+ Result<Ok, QMResult> InitFromBase64(const nsAString& aData);
+
+ void AssertIsOnTargetThread() const { MOZ_ASSERT(IsTargetThread()); }
+
+ // Initialized on the worker thread, never unset, and always used in
+ // a read-only capacity. Used on any thread.
+ CheckedUnsafePtr<WorkerPrivate> mWorkerPrivate;
+
+ // Main thread only.
+ WorkerNotificationObserver* mObserver;
+
+ // The NotificationTask calls ShowInternal()/CloseInternal() on the
+ // Notification. At this point the task has ownership of the Notification. It
+ // passes this on to the Notification itself via mTempRef so that
+ // ShowInternal()/CloseInternal() may pass it along appropriately (or release
+ // it).
+ //
+ // Main thread only.
+ UniquePtr<NotificationRef> mTempRef;
+
+ // Returns true if addref succeeded.
+ bool AddRefObject();
+ void ReleaseObject();
+
+ static NotificationPermission GetPermission(nsIGlobalObject* aGlobal,
+ ErrorResult& aRv);
+
+ static NotificationPermission GetPermissionInternal(nsIPrincipal* aPrincipal,
+ ErrorResult& rv);
+
+ static NotificationPermission TestPermission(nsIPrincipal* aPrincipal);
+
+ bool DispatchClickEvent();
+
+ static nsresult RemovePermission(nsIPrincipal* aPrincipal);
+ static nsresult OpenSettings(nsIPrincipal* aPrincipal);
+
+ nsresult DispatchToMainThread(already_AddRefed<nsIRunnable>&& aRunnable);
+
+ protected:
+ Notification(nsIGlobalObject* aGlobal, const nsAString& aID,
+ const nsAString& aTitle, const nsAString& aBody,
+ NotificationDirection aDir, const nsAString& aLang,
+ const nsAString& aTag, const nsAString& aIconUrl,
+ bool aRequireInteraction, bool aSilent,
+ nsTArray<uint32_t>&& aVibrate,
+ const NotificationBehavior& aBehavior);
+
+ static already_AddRefed<Notification> CreateInternal(
+ nsIGlobalObject* aGlobal, const nsAString& aID, const nsAString& aTitle,
+ const NotificationOptions& aOptions, ErrorResult& aRv);
+
+ nsresult Init();
+ bool IsInPrivateBrowsing();
+ void ShowInternal();
+ void CloseInternal(bool aContextClosed = false);
+
+ static NotificationPermission GetPermissionInternal(
+ nsPIDOMWindowInner* aWindow, ErrorResult& rv);
+
+ static const nsString DirectionToString(NotificationDirection aDirection) {
+ switch (aDirection) {
+ case NotificationDirection::Ltr:
+ return u"ltr"_ns;
+ case NotificationDirection::Rtl:
+ return u"rtl"_ns;
+ default:
+ return u"auto"_ns;
+ }
+ }
+
+ static NotificationDirection StringToDirection(const nsAString& aDirection) {
+ if (aDirection.EqualsLiteral("ltr")) {
+ return NotificationDirection::Ltr;
+ }
+ if (aDirection.EqualsLiteral("rtl")) {
+ return NotificationDirection::Rtl;
+ }
+ return NotificationDirection::Auto;
+ }
+
+ static nsresult GetOrigin(nsIPrincipal* aPrincipal, nsString& aOrigin);
+
+ void GetAlertName(nsAString& aRetval) {
+ AssertIsOnMainThread();
+ if (mAlertName.IsEmpty()) {
+ SetAlertName();
+ }
+ aRetval = mAlertName;
+ }
+
+ void GetScope(nsAString& aScope) { aScope = mScope; }
+
+ void SetScope(const nsAString& aScope) {
+ MOZ_ASSERT(mScope.IsEmpty());
+ mScope = aScope;
+ }
+
+ const nsString mID;
+ const nsString mTitle;
+ const nsString mBody;
+ const NotificationDirection mDir;
+ const nsString mLang;
+ const nsString mTag;
+ const nsString mIconUrl;
+ const bool mRequireInteraction;
+ const bool mSilent;
+ nsTArray<uint32_t> mVibrate;
+ nsString mDataAsBase64;
+ const NotificationBehavior mBehavior;
+
+ // It's null until GetData is first called
+ JS::Heap<JS::Value> mData;
+
+ nsString mAlertName;
+ nsString mScope;
+
+ // Main thread only.
+ bool mIsClosed;
+
+ // We need to make a distinction between the notification being closed i.e.
+ // removed from any pending or active lists, and the notification being
+ // removed from the database. NotificationDB might fail when trying to remove
+ // the notification.
+ bool mIsStored;
+
+ static uint32_t sCount;
+
+ private:
+ virtual ~Notification();
+
+ // Creates a Notification and shows it. Returns a reference to the
+ // Notification if result is NS_OK. The lifetime of this Notification is tied
+ // to an underlying NotificationRef. Do not hold a non-stack raw pointer to
+ // it. Be careful about thread safety if acquiring a strong reference.
+ //
+ // Note that aCx may not be in the compartment of aGlobal, but aOptions will
+ // have its JS things in the compartment of aCx.
+ static already_AddRefed<Notification> CreateAndShow(
+ JSContext* aCx, nsIGlobalObject* aGlobal, const nsAString& aTitle,
+ const NotificationOptions& aOptions, const nsAString& aScope,
+ ErrorResult& aRv);
+
+ nsIPrincipal* GetPrincipal();
+
+ nsresult PersistNotification();
+ void UnpersistNotification();
+
+ void SetAlertName();
+
+ bool IsTargetThread() const { return NS_IsMainThread() == !mWorkerPrivate; }
+
+ bool CreateWorkerRef();
+
+ nsresult ResolveIconAndSoundURL(nsString&, nsString&);
+
+ // Only used for Notifications on Workers, worker thread only.
+ RefPtr<StrongWorkerRef> mWorkerRef;
+ // Target thread only.
+ uint32_t mTaskCount;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_notification_h__
diff --git a/dom/notification/NotificationEvent.cpp b/dom/notification/NotificationEvent.cpp
new file mode 100644
index 0000000000..df9800c46a
--- /dev/null
+++ b/dom/notification/NotificationEvent.cpp
@@ -0,0 +1,21 @@
+/* -*- 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 "NotificationEvent.h"
+
+using namespace mozilla::dom;
+
+NotificationEvent::NotificationEvent(EventTarget* aOwner)
+ : ExtendableEvent(aOwner) {}
+
+NS_IMPL_ADDREF_INHERITED(NotificationEvent, ExtendableEvent)
+NS_IMPL_RELEASE_INHERITED(NotificationEvent, ExtendableEvent)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(NotificationEvent)
+NS_INTERFACE_MAP_END_INHERITING(ExtendableEvent)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(NotificationEvent, ExtendableEvent,
+ mNotification)
diff --git a/dom/notification/NotificationEvent.h b/dom/notification/NotificationEvent.h
new file mode 100644
index 0000000000..744c216798
--- /dev/null
+++ b/dom/notification/NotificationEvent.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_workers_notificationevent_h__
+#define mozilla_dom_workers_notificationevent_h__
+
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/NotificationEventBinding.h"
+#include "mozilla/dom/ServiceWorkerEvents.h"
+#include "mozilla/dom/WorkerCommon.h"
+
+namespace mozilla::dom {
+
+class ServiceWorker;
+class ServiceWorkerClient;
+
+class NotificationEvent final : public ExtendableEvent {
+ protected:
+ explicit NotificationEvent(EventTarget* aOwner);
+ ~NotificationEvent() = default;
+
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(NotificationEvent, ExtendableEvent)
+
+ virtual JSObject* WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override {
+ return NotificationEvent_Binding::Wrap(aCx, this, aGivenProto);
+ }
+
+ static already_AddRefed<NotificationEvent> Constructor(
+ mozilla::dom::EventTarget* aOwner, const nsAString& aType,
+ const NotificationEventInit& aOptions) {
+ RefPtr<NotificationEvent> e = new NotificationEvent(aOwner);
+ bool trusted = e->Init(aOwner);
+ e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable);
+ e->SetTrusted(trusted);
+ e->SetComposed(aOptions.mComposed);
+ e->mNotification = aOptions.mNotification;
+ e->SetWantsPopupControlCheck(e->IsTrusted());
+ return e.forget();
+ }
+
+ static already_AddRefed<NotificationEvent> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const NotificationEventInit& aOptions) {
+ nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports());
+ return Constructor(owner, aType, aOptions);
+ }
+
+ already_AddRefed<Notification> Notification_() {
+ RefPtr<Notification> n = mNotification;
+ return n.forget();
+ }
+
+ private:
+ RefPtr<Notification> mNotification;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_workers_notificationevent_h__ */
diff --git a/dom/notification/NotificationStorage.sys.mjs b/dom/notification/NotificationStorage.sys.mjs
new file mode 100644
index 0000000000..132771e088
--- /dev/null
+++ b/dom/notification/NotificationStorage.sys.mjs
@@ -0,0 +1,205 @@
+/* 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/. */
+
+const DEBUG = false;
+function debug(s) {
+ dump("-*- NotificationStorage.js: " + s + "\n");
+}
+
+const kMessageNotificationGetAllOk = "Notification:GetAll:Return:OK";
+const kMessageNotificationGetAllKo = "Notification:GetAll:Return:KO";
+const kMessageNotificationSaveKo = "Notification:Save:Return:KO";
+const kMessageNotificationDeleteKo = "Notification:Delete:Return:KO";
+
+const kMessages = [
+ kMessageNotificationGetAllOk,
+ kMessageNotificationGetAllKo,
+ kMessageNotificationSaveKo,
+ kMessageNotificationDeleteKo,
+];
+
+export function NotificationStorage() {
+ this._requests = {};
+ this._requestCount = 0;
+
+ Services.obs.addObserver(this, "xpcom-shutdown");
+
+ // Register for message listeners.
+ this.registerListeners();
+}
+
+NotificationStorage.prototype = {
+ registerListeners() {
+ for (let message of kMessages) {
+ Services.cpmm.addMessageListener(message, this);
+ }
+ },
+
+ unregisterListeners() {
+ for (let message of kMessages) {
+ Services.cpmm.removeMessageListener(message, this);
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (DEBUG) {
+ debug("Topic: " + aTopic);
+ }
+ if (aTopic === "xpcom-shutdown") {
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ this.unregisterListeners();
+ }
+ },
+
+ put(
+ origin,
+ id,
+ title,
+ dir,
+ lang,
+ body,
+ tag,
+ icon,
+ alertName,
+ data,
+ behavior,
+ serviceWorkerRegistrationScope
+ ) {
+ if (DEBUG) {
+ debug("PUT: " + origin + " " + id + ": " + title);
+ }
+ var notification = {
+ id,
+ title,
+ dir,
+ lang,
+ body,
+ tag,
+ icon,
+ alertName,
+ timestamp: new Date().getTime(),
+ origin,
+ data,
+ mozbehavior: behavior,
+ serviceWorkerRegistrationScope,
+ };
+
+ Services.cpmm.sendAsyncMessage("Notification:Save", {
+ origin,
+ notification,
+ });
+ },
+
+ get(origin, tag, callback) {
+ if (DEBUG) {
+ debug("GET: " + origin + " " + tag);
+ }
+ this._fetchFromDB(origin, tag, callback);
+ },
+
+ delete(origin, id) {
+ if (DEBUG) {
+ debug("DELETE: " + id);
+ }
+ Services.cpmm.sendAsyncMessage("Notification:Delete", {
+ origin,
+ id,
+ });
+ },
+
+ receiveMessage(message) {
+ var request = this._requests[message.data.requestID];
+
+ switch (message.name) {
+ case kMessageNotificationGetAllOk:
+ delete this._requests[message.data.requestID];
+ this._returnNotifications(
+ message.data.notifications,
+ request.origin,
+ request.tag,
+ request.callback
+ );
+ break;
+
+ case kMessageNotificationGetAllKo:
+ delete this._requests[message.data.requestID];
+ try {
+ request.callback.done();
+ } catch (e) {
+ debug("Error calling callback done: " + e);
+ }
+ break;
+ case kMessageNotificationSaveKo:
+ case kMessageNotificationDeleteKo:
+ if (DEBUG) {
+ debug(
+ "Error received when treating: '" +
+ message.name +
+ "': " +
+ message.data.errorMsg
+ );
+ }
+ break;
+
+ default:
+ if (DEBUG) {
+ debug("Unrecognized message: " + message.name);
+ }
+ break;
+ }
+ },
+
+ _fetchFromDB(origin, tag, callback) {
+ var request = {
+ origin,
+ tag,
+ callback,
+ };
+ var requestID = this._requestCount++;
+ this._requests[requestID] = request;
+ Services.cpmm.sendAsyncMessage("Notification:GetAll", {
+ origin,
+ tag,
+ requestID,
+ });
+ },
+
+ _returnNotifications(notifications, origin, tag, callback) {
+ // Pass each notification back separately.
+ // The callback is called asynchronously to match the behaviour when
+ // fetching from the database.
+ notifications.forEach(function (notification) {
+ try {
+ Services.tm.dispatchToMainThread(
+ callback.handle.bind(
+ callback,
+ notification.id,
+ notification.title,
+ notification.dir,
+ notification.lang,
+ notification.body,
+ notification.tag,
+ notification.icon,
+ notification.data,
+ notification.mozbehavior,
+ notification.serviceWorkerRegistrationScope
+ )
+ );
+ } catch (e) {
+ if (DEBUG) {
+ debug("Error calling callback handle: " + e);
+ }
+ }
+ });
+ try {
+ Services.tm.dispatchToMainThread(callback.done);
+ } catch (e) {
+ if (DEBUG) {
+ debug("Error calling callback done: " + e);
+ }
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsINotificationStorage"]),
+};
diff --git a/dom/notification/components.conf b/dom/notification/components.conf
new file mode 100644
index 0000000000..2cb6bdb33a
--- /dev/null
+++ b/dom/notification/components.conf
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{37f819b0-0b5c-11e3-8ffd-0800200c9a66}',
+ 'contract_ids': ['@mozilla.org/notificationStorage;1'],
+ 'esModule': 'resource://gre/modules/NotificationStorage.sys.mjs',
+ 'constructor': 'NotificationStorage',
+ },
+]
diff --git a/dom/notification/moz.build b/dom/notification/moz.build
new file mode 100644
index 0000000000..b42e7a13ab
--- /dev/null
+++ b/dom/notification/moz.build
@@ -0,0 +1,48 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Notifications")
+
+EXTRA_JS_MODULES += [
+ "NotificationStorage.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+EXPORTS.mozilla.dom += [
+ "Notification.h",
+ "NotificationEvent.h",
+]
+
+UNIFIED_SOURCES += [
+ "Notification.cpp",
+ "NotificationEvent.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/dom/ipc",
+]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"]
+XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"]
+MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.toml"]
+MOCHITEST_CHROME_MANIFESTS += ["test/chrome/chrome.toml"]
+
+if CONFIG["MOZ_NEW_NOTIFICATION_STORE"]:
+ EXTRA_JS_MODULES += [
+ "new/NotificationDB.sys.mjs",
+ ]
+else:
+ EXTRA_JS_MODULES += [
+ "old/NotificationDB.sys.mjs",
+ ]
diff --git a/dom/notification/new/NotificationDB.sys.mjs b/dom/notification/new/NotificationDB.sys.mjs
new file mode 100644
index 0000000000..b607fdd234
--- /dev/null
+++ b/dom/notification/new/NotificationDB.sys.mjs
@@ -0,0 +1,375 @@
+/* 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/. */
+
+const DEBUG = false;
+function debug(s) {
+ dump("-*- NotificationDB component: " + s + "\n");
+}
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ KeyValueService: "resource://gre/modules/kvstore.sys.mjs",
+});
+
+const kMessages = [
+ "Notification:Save",
+ "Notification:Delete",
+ "Notification:GetAll",
+];
+
+// Given its origin and ID, produce the key that uniquely identifies
+// a notification.
+function makeKey(origin, id) {
+ return origin.concat("\t", id);
+}
+
+var NotificationDB = {
+ // Ensure we won't call init() while xpcom-shutdown is performed
+ _shutdownInProgress: false,
+
+ // A handle to the kvstore, retrieved lazily when we load the data.
+ _store: null,
+
+ // A promise that resolves once the store has been loaded.
+ // The promise doesn't resolve to a value; it merely captures the state
+ // of the load via its resolution.
+ _loadPromise: null,
+
+ init() {
+ if (this._shutdownInProgress) {
+ return;
+ }
+
+ this.tasks = []; // read/write operation queue
+ this.runningTask = null;
+
+ Services.obs.addObserver(this, "xpcom-shutdown");
+ this.registerListeners();
+ },
+
+ registerListeners() {
+ for (let message of kMessages) {
+ Services.ppmm.addMessageListener(message, this);
+ }
+ },
+
+ unregisterListeners() {
+ for (let message of kMessages) {
+ Services.ppmm.removeMessageListener(message, this);
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (DEBUG) {
+ debug("Topic: " + aTopic);
+ }
+ if (aTopic == "xpcom-shutdown") {
+ this._shutdownInProgress = true;
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ this.unregisterListeners();
+ }
+ },
+
+ filterNonAppNotifications(notifications) {
+ for (let origin in notifications) {
+ let persistentNotificationCount = 0;
+ for (let id in notifications[origin]) {
+ if (notifications[origin][id].serviceWorkerRegistrationScope) {
+ persistentNotificationCount++;
+ } else {
+ delete notifications[origin][id];
+ }
+ }
+ if (persistentNotificationCount == 0) {
+ if (DEBUG) {
+ debug(
+ "Origin " + origin + " is not linked to an app manifest, deleting."
+ );
+ }
+ delete notifications[origin];
+ }
+ }
+
+ return notifications;
+ },
+
+ async maybeMigrateData() {
+ const oldStore = PathUtils.join(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ "notificationstore.json"
+ );
+
+ if (!(await IOUtils.exists(oldStore))) {
+ if (DEBUG) {
+ debug("Old store doesn't exist; not migrating data.");
+ }
+ return;
+ }
+
+ let data;
+ try {
+ data = await IOUtils.readUTF8(oldStore);
+ } catch (ex) {
+ // If read failed, we assume we have no notifications to migrate.
+ if (DEBUG) {
+ debug("Failed to read old store; not migrating data.");
+ }
+ return;
+ } finally {
+ // Finally, delete the old file so we don't try to migrate it again.
+ await IOUtils.remove(oldStore);
+ }
+
+ if (data.length) {
+ // Preprocessing phase intends to cleanly separate any migration-related
+ // tasks.
+ //
+ // NB: This code existed before we migrated the data to a kvstore,
+ // and the "migration-related tasks" it references are from an earlier
+ // migration. We used to do it every time we read the JSON file;
+ // now we do it once, when migrating the JSON file to the kvstore.
+ const notifications = this.filterNonAppNotifications(JSON.parse(data));
+
+ // Copy the data from the JSON file to the kvstore.
+ // TODO: use a transaction to improve the performance of these operations
+ // once the kvstore API supports it (bug 1515096).
+ for (const origin in notifications) {
+ for (const id in notifications[origin]) {
+ await this._store.put(
+ makeKey(origin, id),
+ JSON.stringify(notifications[origin][id])
+ );
+ }
+ }
+ }
+ },
+
+ // Attempt to read notification file, if it's not there we will create it.
+ async load() {
+ // Get and cache a handle to the kvstore.
+ const dir = PathUtils.join(PathUtils.profileDir, "notificationstore");
+ await IOUtils.makeDirectory(dir, { ignoreExisting: true });
+ this._store = await lazy.KeyValueService.getOrCreate(dir, "notifications");
+
+ // Migrate data from the old JSON file to the new kvstore if the old file
+ // is present in the user's profile directory.
+ await this.maybeMigrateData();
+ },
+
+ // Helper function: promise will be resolved once file exists and/or is loaded.
+ ensureLoaded() {
+ if (!this._loadPromise) {
+ this._loadPromise = this.load();
+ }
+ return this._loadPromise;
+ },
+
+ receiveMessage(message) {
+ if (DEBUG) {
+ debug("Received message:" + message.name);
+ }
+
+ // sendAsyncMessage can fail if the child process exits during a
+ // notification storage operation, so always wrap it in a try/catch.
+ function returnMessage(name, data) {
+ try {
+ message.target.sendAsyncMessage(name, data);
+ } catch (e) {
+ if (DEBUG) {
+ debug("Return message failed, " + name);
+ }
+ }
+ }
+
+ switch (message.name) {
+ case "Notification:GetAll":
+ this.queueTask("getall", message.data)
+ .then(function (notifications) {
+ returnMessage("Notification:GetAll:Return:OK", {
+ requestID: message.data.requestID,
+ origin: message.data.origin,
+ notifications,
+ });
+ })
+ .catch(function (error) {
+ returnMessage("Notification:GetAll:Return:KO", {
+ requestID: message.data.requestID,
+ origin: message.data.origin,
+ errorMsg: error,
+ });
+ });
+ break;
+
+ case "Notification:Save":
+ this.queueTask("save", message.data)
+ .then(function () {
+ returnMessage("Notification:Save:Return:OK", {
+ requestID: message.data.requestID,
+ });
+ })
+ .catch(function (error) {
+ returnMessage("Notification:Save:Return:KO", {
+ requestID: message.data.requestID,
+ errorMsg: error,
+ });
+ });
+ break;
+
+ case "Notification:Delete":
+ this.queueTask("delete", message.data)
+ .then(function () {
+ returnMessage("Notification:Delete:Return:OK", {
+ requestID: message.data.requestID,
+ });
+ })
+ .catch(function (error) {
+ returnMessage("Notification:Delete:Return:KO", {
+ requestID: message.data.requestID,
+ errorMsg: error,
+ });
+ });
+ break;
+
+ default:
+ if (DEBUG) {
+ debug("Invalid message name" + message.name);
+ }
+ }
+ },
+
+ // We need to make sure any read/write operations are atomic,
+ // so use a queue to run each operation sequentially.
+ queueTask(operation, data) {
+ if (DEBUG) {
+ debug("Queueing task: " + operation);
+ }
+
+ var defer = {};
+
+ this.tasks.push({ operation, data, defer });
+
+ var promise = new Promise(function (resolve, reject) {
+ defer.resolve = resolve;
+ defer.reject = reject;
+ });
+
+ // Only run immediately if we aren't currently running another task.
+ if (!this.runningTask) {
+ if (DEBUG) {
+ debug("Task queue was not running, starting now...");
+ }
+ this.runNextTask();
+ }
+
+ return promise;
+ },
+
+ runNextTask() {
+ if (this.tasks.length === 0) {
+ if (DEBUG) {
+ debug("No more tasks to run, queue depleted");
+ }
+ this.runningTask = null;
+ return;
+ }
+ this.runningTask = this.tasks.shift();
+
+ // Always make sure we are loaded before performing any read/write tasks.
+ this.ensureLoaded()
+ .then(() => {
+ var task = this.runningTask;
+
+ switch (task.operation) {
+ case "getall":
+ return this.taskGetAll(task.data);
+
+ case "save":
+ return this.taskSave(task.data);
+
+ case "delete":
+ return this.taskDelete(task.data);
+ }
+
+ throw new Error(`Unknown task operation: ${task.operation}`);
+ })
+ .then(payload => {
+ if (DEBUG) {
+ debug("Finishing task: " + this.runningTask.operation);
+ }
+ this.runningTask.defer.resolve(payload);
+ })
+ .catch(err => {
+ if (DEBUG) {
+ debug(
+ "Error while running " + this.runningTask.operation + ": " + err
+ );
+ }
+ this.runningTask.defer.reject(err);
+ })
+ .then(() => {
+ this.runNextTask();
+ });
+ },
+
+ enumerate(origin) {
+ // The "from" and "to" key parameters to nsIKeyValueStore.enumerate()
+ // are inclusive and exclusive, respectively, and keys are tuples
+ // of origin and ID joined by a tab (\t), which is character code 9;
+ // so enumerating ["origin", "origin\n"), where the line feed (\n)
+ // is character code 10, enumerates all pairs with the given origin.
+ return this._store.enumerate(origin, `${origin}\n`);
+ },
+
+ async taskGetAll(data) {
+ if (DEBUG) {
+ debug("Task, getting all");
+ }
+ var origin = data.origin;
+ var notifications = [];
+
+ for (const { value } of await this.enumerate(origin)) {
+ notifications.push(JSON.parse(value));
+ }
+
+ if (data.tag) {
+ notifications = notifications.filter(n => n.tag === data.tag);
+ }
+
+ return notifications;
+ },
+
+ async taskSave(data) {
+ if (DEBUG) {
+ debug("Task, saving");
+ }
+ var origin = data.origin;
+ var notification = data.notification;
+
+ // We might have existing notification with this tag,
+ // if so we need to remove it before saving the new one.
+ if (notification.tag) {
+ for (const { key, value } of await this.enumerate(origin)) {
+ const oldNotification = JSON.parse(value);
+ if (oldNotification.tag === notification.tag) {
+ await this._store.delete(key);
+ }
+ }
+ }
+
+ await this._store.put(
+ makeKey(origin, notification.id),
+ JSON.stringify(notification)
+ );
+ },
+
+ async taskDelete(data) {
+ if (DEBUG) {
+ debug("Task, deleting");
+ }
+ await this._store.delete(makeKey(data.origin, data.id));
+ },
+};
+
+NotificationDB.init();
diff --git a/dom/notification/old/NotificationDB.sys.mjs b/dom/notification/old/NotificationDB.sys.mjs
new file mode 100644
index 0000000000..79a9965628
--- /dev/null
+++ b/dom/notification/old/NotificationDB.sys.mjs
@@ -0,0 +1,386 @@
+/* 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/. */
+
+const DEBUG = false;
+function debug(s) {
+ dump("-*- NotificationDB component: " + s + "\n");
+}
+
+const NOTIFICATION_STORE_DIR = PathUtils.profileDir;
+const NOTIFICATION_STORE_PATH = PathUtils.join(
+ NOTIFICATION_STORE_DIR,
+ "notificationstore.json"
+);
+
+const kMessages = [
+ "Notification:Save",
+ "Notification:Delete",
+ "Notification:GetAll",
+];
+
+var NotificationDB = {
+ // Ensure we won't call init() while xpcom-shutdown is performed
+ _shutdownInProgress: false,
+
+ init() {
+ if (this._shutdownInProgress) {
+ return;
+ }
+
+ this.notifications = Object.create(null);
+ this.byTag = Object.create(null);
+ this.loaded = false;
+
+ this.tasks = []; // read/write operation queue
+ this.runningTask = null;
+
+ Services.obs.addObserver(this, "xpcom-shutdown");
+ this.registerListeners();
+ },
+
+ registerListeners() {
+ for (let message of kMessages) {
+ Services.ppmm.addMessageListener(message, this);
+ }
+ },
+
+ unregisterListeners() {
+ for (let message of kMessages) {
+ Services.ppmm.removeMessageListener(message, this);
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (DEBUG) {
+ debug("Topic: " + aTopic);
+ }
+ if (aTopic == "xpcom-shutdown") {
+ this._shutdownInProgress = true;
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ this.unregisterListeners();
+ }
+ },
+
+ filterNonAppNotifications(notifications) {
+ let result = Object.create(null);
+ for (let origin in notifications) {
+ result[origin] = Object.create(null);
+ let persistentNotificationCount = 0;
+ for (let id in notifications[origin]) {
+ if (notifications[origin][id].serviceWorkerRegistrationScope) {
+ persistentNotificationCount++;
+ result[origin][id] = notifications[origin][id];
+ }
+ }
+ if (persistentNotificationCount == 0) {
+ if (DEBUG) {
+ debug(
+ "Origin " + origin + " is not linked to an app manifest, deleting."
+ );
+ }
+ delete result[origin];
+ }
+ }
+
+ return result;
+ },
+
+ // Attempt to read notification file, if it's not there we will create it.
+ load() {
+ var promise = IOUtils.readUTF8(NOTIFICATION_STORE_PATH);
+ return promise.then(
+ data => {
+ if (data.length) {
+ // Preprocessing phase intends to cleanly separate any migration-related
+ // tasks.
+ this.notifications = this.filterNonAppNotifications(JSON.parse(data));
+ }
+
+ // populate the list of notifications by tag
+ if (this.notifications) {
+ for (var origin in this.notifications) {
+ this.byTag[origin] = Object.create(null);
+ for (var id in this.notifications[origin]) {
+ var curNotification = this.notifications[origin][id];
+ if (curNotification.tag) {
+ this.byTag[origin][curNotification.tag] = curNotification;
+ }
+ }
+ }
+ }
+
+ this.loaded = true;
+ },
+
+ // If read failed, we assume we have no notifications to load.
+ reason => {
+ this.loaded = true;
+ return this.createStore();
+ }
+ );
+ },
+
+ // Creates the notification directory.
+ createStore() {
+ var promise = IOUtils.makeDirectory(NOTIFICATION_STORE_DIR, {
+ ignoreExisting: true,
+ });
+ return promise.then(this.createFile.bind(this));
+ },
+
+ // Creates the notification file once the directory is created.
+ createFile() {
+ return IOUtils.writeUTF8(NOTIFICATION_STORE_PATH, "", {
+ tmpPath: NOTIFICATION_STORE_PATH + ".tmp",
+ });
+ },
+
+ // Save current notifications to the file.
+ save() {
+ var data = JSON.stringify(this.notifications);
+ return IOUtils.writeUTF8(NOTIFICATION_STORE_PATH, data, {
+ tmpPath: NOTIFICATION_STORE_PATH + ".tmp",
+ });
+ },
+
+ // Helper function: promise will be resolved once file exists and/or is loaded.
+ ensureLoaded() {
+ if (!this.loaded) {
+ return this.load();
+ }
+ return Promise.resolve();
+ },
+
+ receiveMessage(message) {
+ if (DEBUG) {
+ debug("Received message:" + message.name);
+ }
+
+ // sendAsyncMessage can fail if the child process exits during a
+ // notification storage operation, so always wrap it in a try/catch.
+ function returnMessage(name, data) {
+ try {
+ message.target.sendAsyncMessage(name, data);
+ } catch (e) {
+ if (DEBUG) {
+ debug("Return message failed, " + name);
+ }
+ }
+ }
+
+ switch (message.name) {
+ case "Notification:GetAll":
+ this.queueTask("getall", message.data)
+ .then(function (notifications) {
+ returnMessage("Notification:GetAll:Return:OK", {
+ requestID: message.data.requestID,
+ origin: message.data.origin,
+ notifications,
+ });
+ })
+ .catch(function (error) {
+ returnMessage("Notification:GetAll:Return:KO", {
+ requestID: message.data.requestID,
+ origin: message.data.origin,
+ errorMsg: error,
+ });
+ });
+ break;
+
+ case "Notification:Save":
+ this.queueTask("save", message.data)
+ .then(function () {
+ returnMessage("Notification:Save:Return:OK", {
+ requestID: message.data.requestID,
+ });
+ })
+ .catch(function (error) {
+ returnMessage("Notification:Save:Return:KO", {
+ requestID: message.data.requestID,
+ errorMsg: error,
+ });
+ });
+ break;
+
+ case "Notification:Delete":
+ this.queueTask("delete", message.data)
+ .then(function () {
+ returnMessage("Notification:Delete:Return:OK", {
+ requestID: message.data.requestID,
+ });
+ })
+ .catch(function (error) {
+ returnMessage("Notification:Delete:Return:KO", {
+ requestID: message.data.requestID,
+ errorMsg: error,
+ });
+ });
+ break;
+
+ default:
+ if (DEBUG) {
+ debug("Invalid message name" + message.name);
+ }
+ }
+ },
+
+ // We need to make sure any read/write operations are atomic,
+ // so use a queue to run each operation sequentially.
+ queueTask(operation, data) {
+ if (DEBUG) {
+ debug("Queueing task: " + operation);
+ }
+
+ var defer = {};
+
+ this.tasks.push({
+ operation,
+ data,
+ defer,
+ });
+
+ var promise = new Promise(function (resolve, reject) {
+ defer.resolve = resolve;
+ defer.reject = reject;
+ });
+
+ // Only run immediately if we aren't currently running another task.
+ if (!this.runningTask) {
+ if (DEBUG) {
+ debug("Task queue was not running, starting now...");
+ }
+ this.runNextTask();
+ }
+
+ return promise;
+ },
+
+ runNextTask() {
+ if (this.tasks.length === 0) {
+ if (DEBUG) {
+ debug("No more tasks to run, queue depleted");
+ }
+ this.runningTask = null;
+ return;
+ }
+ this.runningTask = this.tasks.shift();
+
+ // Always make sure we are loaded before performing any read/write tasks.
+ this.ensureLoaded()
+ .then(() => {
+ var task = this.runningTask;
+
+ switch (task.operation) {
+ case "getall":
+ return this.taskGetAll(task.data);
+
+ case "save":
+ return this.taskSave(task.data);
+
+ case "delete":
+ return this.taskDelete(task.data);
+
+ default:
+ return Promise.reject(
+ new Error(`Found a task with unknown operation ${task.operation}`)
+ );
+ }
+ })
+ .then(payload => {
+ if (DEBUG) {
+ debug("Finishing task: " + this.runningTask.operation);
+ }
+ this.runningTask.defer.resolve(payload);
+ })
+ .catch(err => {
+ if (DEBUG) {
+ debug(
+ "Error while running " + this.runningTask.operation + ": " + err
+ );
+ }
+ this.runningTask.defer.reject(err);
+ })
+ .then(() => {
+ this.runNextTask();
+ });
+ },
+
+ taskGetAll(data) {
+ if (DEBUG) {
+ debug("Task, getting all");
+ }
+ var origin = data.origin;
+ var notifications = [];
+ // Grab only the notifications for specified origin.
+ if (this.notifications[origin]) {
+ if (data.tag) {
+ let n;
+ if ((n = this.byTag[origin][data.tag])) {
+ notifications.push(n);
+ }
+ } else {
+ for (var i in this.notifications[origin]) {
+ notifications.push(this.notifications[origin][i]);
+ }
+ }
+ }
+ return Promise.resolve(notifications);
+ },
+
+ taskSave(data) {
+ if (DEBUG) {
+ debug("Task, saving");
+ }
+ var origin = data.origin;
+ var notification = data.notification;
+ if (!this.notifications[origin]) {
+ this.notifications[origin] = Object.create(null);
+ this.byTag[origin] = Object.create(null);
+ }
+
+ // We might have existing notification with this tag,
+ // if so we need to remove it before saving the new one.
+ if (notification.tag) {
+ var oldNotification = this.byTag[origin][notification.tag];
+ if (oldNotification) {
+ delete this.notifications[origin][oldNotification.id];
+ }
+ this.byTag[origin][notification.tag] = notification;
+ }
+
+ this.notifications[origin][notification.id] = notification;
+ return this.save();
+ },
+
+ taskDelete(data) {
+ if (DEBUG) {
+ debug("Task, deleting");
+ }
+ var origin = data.origin;
+ var id = data.id;
+ if (!this.notifications[origin]) {
+ if (DEBUG) {
+ debug("No notifications found for origin: " + origin);
+ }
+ return Promise.resolve();
+ }
+
+ // Make sure we can find the notification to delete.
+ var oldNotification = this.notifications[origin][id];
+ if (!oldNotification) {
+ if (DEBUG) {
+ debug("No notification found with id: " + id);
+ }
+ return Promise.resolve();
+ }
+
+ if (oldNotification.tag) {
+ delete this.byTag[origin][oldNotification.tag];
+ }
+ delete this.notifications[origin][id];
+ return this.save();
+ },
+};
+
+NotificationDB.init();
diff --git a/dom/notification/test/browser/browser.toml b/dom/notification/test/browser/browser.toml
new file mode 100644
index 0000000000..426a79b940
--- /dev/null
+++ b/dom/notification/test/browser/browser.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+["browser_permission_dismiss.js"]
+support-files = ["notification.html"]
diff --git a/dom/notification/test/browser/browser_permission_dismiss.js b/dom/notification/test/browser/browser_permission_dismiss.js
new file mode 100644
index 0000000000..72b2db2827
--- /dev/null
+++ b/dom/notification/test/browser/browser_permission_dismiss.js
@@ -0,0 +1,231 @@
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const ORIGIN_URI = Services.io.newURI("https://example.com");
+const PERMISSION_NAME = "desktop-notification";
+const PROMPT_ALLOW_BUTTON = -1;
+const PROMPT_NOT_NOW_BUTTON = 0;
+const PROMPT_NEVER_BUTTON = 1;
+const TEST_URL =
+ "https://example.com/browser/dom/notification/test/browser/notification.html";
+
+/**
+ * Clicks the specified web-notifications prompt button.
+ *
+ * @param {Number} aButtonIndex Number indicating which button to click.
+ * See the constants in this file.
+ * @note modified from toolkit/components/passwordmgr/test/browser/head.js
+ */
+function clickDoorhangerButton(aButtonIndex, browser) {
+ let popup = PopupNotifications.getNotification("web-notifications", browser);
+ let notifications = popup.owner.panel.childNodes;
+ ok(notifications.length, "at least one notification displayed");
+ ok(true, notifications.length + " notification(s)");
+ let notification = notifications[0];
+
+ if (aButtonIndex == PROMPT_ALLOW_BUTTON) {
+ ok(true, "Triggering main action (allow the permission)");
+ notification.button.doCommand();
+ } else if (aButtonIndex == PROMPT_NEVER_BUTTON) {
+ ok(true, "Triggering secondary action (deny the permission permanently)");
+ notification.menupopup.querySelector("menuitem").doCommand();
+ } else {
+ ok(true, "Triggering secondary action (deny the permission temporarily)");
+ notification.secondaryButton.doCommand();
+ }
+}
+
+/**
+ * Opens a tab which calls `Notification.requestPermission()` with a callback
+ * argument, calls the `task` function while the permission prompt is open,
+ * and verifies that the expected permission is set.
+ *
+ * @param {Function} task Task function to run to interact with the prompt.
+ * @param {String} permission Expected permission value.
+ * @return {Promise} resolving when the task function is done and the tab
+ * closes.
+ */
+function tabWithRequest(
+ task,
+ permission,
+ browser = window.gBrowser,
+ privateWindow = false
+) {
+ clearPermission(ORIGIN_URI, PERMISSION_NAME, privateWindow);
+
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser: browser,
+ url: TEST_URL,
+ },
+ async function (linkedBrowser) {
+ let requestPromise = SpecialPowers.spawn(
+ linkedBrowser,
+ [
+ {
+ permission,
+ },
+ ],
+ async function ({ permission }) {
+ function requestCallback(perm) {
+ is(
+ perm,
+ permission,
+ "Should call the legacy callback with the permission state"
+ );
+ }
+ let perm = await content.window.Notification.requestPermission(
+ requestCallback
+ );
+ is(
+ perm,
+ permission,
+ "Should resolve the promise with the permission state"
+ );
+ }
+ );
+
+ await task(linkedBrowser);
+ await requestPromise;
+ }
+ );
+}
+
+function clearPermission(origin, permissionName, isPrivate) {
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ origin,
+ isPrivate ? { privateBrowsingId: 1 } : {} /* attrs */
+ );
+ PermissionTestUtils.remove(principal, permissionName);
+}
+
+add_setup(async function () {
+ Services.prefs.setBoolPref(
+ "dom.webnotifications.requireuserinteraction",
+ false
+ );
+ Services.prefs.setBoolPref(
+ "permissions.desktop-notification.notNow.enabled",
+ true
+ );
+ SimpleTest.registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("dom.webnotifications.requireuserinteraction");
+ Services.prefs.clearUserPref(
+ "permissions.desktop-notification.notNow.enabled"
+ );
+
+ clearPermission(ORIGIN_URI, PERMISSION_NAME, false /* private origin */);
+ clearPermission(ORIGIN_URI, PERMISSION_NAME, true /* private origin */);
+ });
+});
+
+add_task(async function test_requestPermission_granted() {
+ await tabWithRequest(async function (linkedBrowser) {
+ await BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ clickDoorhangerButton(PROMPT_ALLOW_BUTTON, linkedBrowser);
+ }, "granted");
+
+ ok(
+ !PopupNotifications.getNotification("web-notifications"),
+ "Should remove the doorhanger notification icon if granted"
+ );
+
+ is(
+ PermissionTestUtils.testPermission(ORIGIN_URI, PERMISSION_NAME),
+ Services.perms.ALLOW_ACTION,
+ "Check permission in perm. manager"
+ );
+});
+
+add_task(async function test_requestPermission_denied_temporarily() {
+ await tabWithRequest(async function (linkedBrowser) {
+ await BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ clickDoorhangerButton(PROMPT_NOT_NOW_BUTTON, linkedBrowser);
+ }, "default");
+
+ ok(
+ !PopupNotifications.getNotification("web-notifications"),
+ "Should remove the doorhanger notification icon if denied"
+ );
+
+ is(
+ PermissionTestUtils.testPermission(ORIGIN_URI, PERMISSION_NAME),
+ Services.perms.UNKNOWN_ACTION,
+ "Check permission in perm. manager"
+ );
+});
+
+add_task(async function test_requestPermission_denied_permanently() {
+ await tabWithRequest(async function (linkedBrowser) {
+ await BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown");
+ clickDoorhangerButton(PROMPT_NEVER_BUTTON, linkedBrowser);
+ }, "denied");
+
+ ok(
+ !PopupNotifications.getNotification("web-notifications"),
+ "Should remove the doorhanger notification icon if denied"
+ );
+
+ is(
+ PermissionTestUtils.testPermission(ORIGIN_URI, PERMISSION_NAME),
+ Services.perms.DENY_ACTION,
+ "Check permission in perm. manager"
+ );
+});
+
+add_task(
+ async function test_requestPermission_defaultPrivateNotificationsPref() {
+ ok(
+ !SpecialPowers.getBoolPref(
+ "dom.webnotifications.privateBrowsing.enableDespiteLimitations"
+ ),
+ "Pref should be default disabled"
+ );
+ }
+);
+
+add_task(async function test_requestPermission_privateNotifications() {
+ async function run(perm) {
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await tabWithRequest(
+ async linkedBrowser => {
+ if (perm != Services.perms.UNKNOWN_ACTION) {
+ await BrowserTestUtils.waitForEvent(
+ privateWindow.PopupNotifications.panel,
+ "popupshown"
+ );
+
+ clickDoorhangerButton(PROMPT_ALLOW_BUTTON, linkedBrowser);
+ }
+ },
+ perm == Services.perms.ALLOW_ACTION ? "granted" : "denied",
+ privateWindow.gBrowser,
+ true /* privateWindow */
+ );
+
+ ok(
+ !PopupNotifications.getNotification(
+ "web-notifications",
+ privateWindow.gBrowser
+ ),
+ "doorhanger should have been removed in all cases by now"
+ );
+
+ await BrowserTestUtils.closeWindow(privateWindow);
+ }
+
+ await run(Services.perms.UNKNOWN_ACTION);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.webnotifications.privateBrowsing.enableDespiteLimitations", true],
+ ],
+ });
+ await run(Services.perms.ALLOW_ACTION);
+});
diff --git a/dom/notification/test/browser/notification.html b/dom/notification/test/browser/notification.html
new file mode 100644
index 0000000000..0ceeb8ea46
--- /dev/null
+++ b/dom/notification/test/browser/notification.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Notifications test</title>
+ </head>
+
+ <body>
+
+ </body>
+</html>
diff --git a/dom/notification/test/chrome/chrome.toml b/dom/notification/test/chrome/chrome.toml
new file mode 100644
index 0000000000..d4027438d2
--- /dev/null
+++ b/dom/notification/test/chrome/chrome.toml
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+["test_notification_system_principal.xhtml"]
diff --git a/dom/notification/test/chrome/test_notification_system_principal.xhtml b/dom/notification/test/chrome/test_notification_system_principal.xhtml
new file mode 100644
index 0000000000..0700d59338
--- /dev/null
+++ b/dom/notification/test/chrome/test_notification_system_principal.xhtml
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=874090
+-->
+<window title="Mozilla Bug 874090" onload="runTests()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=874090"
+ target="_blank">Mozilla Bug 874090</a>
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ /** Test for Bug 874090 **/
+ const MOCK_CID = Components.ID("{2a0f83c4-8818-4914-a184-f1172b4eaaa7}");
+ const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
+
+ var mockAlertsService = {
+ showAlert(alert, alertListener) {
+ ok(true, "System principal was granted permission and is able to call showAlert.");
+ unregisterMock();
+ SimpleTest.finish();
+ },
+
+ showAlertNotification(imageUrl, title, text, textClickable,
+ cookie, alertListener, name, dir, lang, data) {
+ this.showAlert();
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAlertsService"]),
+
+ createInstance(aIID) {
+ return this.QueryInterface(aIID);
+ }
+ };
+
+ function registerMock() {
+ Components.manager.QueryInterface(Ci.nsIComponentRegistrar).
+ registerFactory(MOCK_CID, "alerts service", ALERTS_SERVICE_CONTRACT_ID, mockAlertsService);
+ }
+
+ function unregisterMock() {
+ Components.manager.QueryInterface(Ci.nsIComponentRegistrar).
+ unregisterFactory(MOCK_CID, mockAlertsService);
+ }
+
+ function runTests() {
+ registerMock();
+
+ is(Notification.permission, "granted", "System principal should be automatically granted permission.");
+
+ Notification.requestPermission(function(permission) {
+ is(permission, "granted", "System principal should be granted permission when calling requestPermission.");
+
+ if (permission == "granted") {
+ // Create a notification and make sure that it is able to call into
+ // the mock alert service to show the notification.
+ new Notification("Hello");
+ } else {
+ unregisterMock();
+ SimpleTest.finish();
+ }
+ });
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ ]]>
+ </script>
+</window>
diff --git a/dom/notification/test/mochitest/MockServices.js b/dom/notification/test/mochitest/MockServices.js
new file mode 100644
index 0000000000..4b2526c26a
--- /dev/null
+++ b/dom/notification/test/mochitest/MockServices.js
@@ -0,0 +1,176 @@
+var MockServices = (function () {
+ "use strict";
+
+ const MOCK_ALERTS_CID = SpecialPowers.wrap(SpecialPowers.Components).ID(
+ "{48068bc2-40ab-4904-8afd-4cdfb3a385f3}"
+ );
+ const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
+
+ const MOCK_SYSTEM_ALERTS_CID = SpecialPowers.wrap(
+ SpecialPowers.Components
+ ).ID("{e86d888c-e41b-4b78-9104-2f2742a532de}");
+ const SYSTEM_ALERTS_SERVICE_CONTRACT_ID =
+ "@mozilla.org/system-alerts-service;1";
+
+ var registrar = SpecialPowers.wrap(
+ SpecialPowers.Components
+ ).manager.QueryInterface(SpecialPowers.Ci.nsIComponentRegistrar);
+
+ var activeAlertNotifications = Object.create(null);
+
+ var activeAppNotifications = Object.create(null);
+
+ window.addEventListener("mock-notification-close-event", function (e) {
+ for (var alertName in activeAlertNotifications) {
+ var notif = activeAlertNotifications[alertName];
+ if (notif.title === e.detail.title) {
+ notif.listener.observe(null, "alertfinished", null);
+ delete activeAlertNotifications[alertName];
+ delete activeAppNotifications[alertName];
+ return;
+ }
+ }
+ });
+
+ var mockAlertsService = {
+ showPersistentNotification(persistentData, alert, alertListener) {
+ this.showAlert(alert, alertListener);
+ },
+
+ showAlert(alert, alertListener) {
+ var listener = SpecialPowers.wrap(alertListener);
+ activeAlertNotifications[alert.name] = {
+ listener,
+ cookie: alert.cookie,
+ title: alert.title,
+ };
+
+ // fake async alert show event
+ if (listener) {
+ setTimeout(function () {
+ listener.observe(null, "alertshow", alert.cookie);
+ }, 100);
+ setTimeout(function () {
+ listener.observe(null, "alertclickcallback", alert.cookie);
+ }, 100);
+ }
+ },
+
+ showAlertNotification(
+ imageUrl,
+ title,
+ text,
+ textClickable,
+ cookie,
+ alertListener,
+ name
+ ) {
+ this.showAlert(
+ {
+ name,
+ cookie,
+ title,
+ },
+ alertListener
+ );
+ },
+
+ closeAlert(name) {
+ var alertNotification = activeAlertNotifications[name];
+ if (alertNotification) {
+ if (alertNotification.listener) {
+ alertNotification.listener.observe(
+ null,
+ "alertfinished",
+ alertNotification.cookie
+ );
+ }
+ delete activeAlertNotifications[name];
+ }
+
+ var appNotification = activeAppNotifications[name];
+ if (appNotification) {
+ delete activeAppNotifications[name];
+ }
+ },
+
+ QueryInterface(aIID) {
+ if (
+ SpecialPowers.wrap(aIID).equals(SpecialPowers.Ci.nsISupports) ||
+ SpecialPowers.wrap(aIID).equals(SpecialPowers.Ci.nsIAlertsService)
+ ) {
+ return this;
+ }
+ throw SpecialPowers.Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ createInstance(aIID) {
+ return this.QueryInterface(aIID);
+ },
+ };
+ mockAlertsService = SpecialPowers.wrapCallbackObject(mockAlertsService);
+
+ // MockServices API
+ return {
+ register() {
+ try {
+ this.originalAlertsCID = registrar.contractIDToCID(
+ ALERTS_SERVICE_CONTRACT_ID
+ );
+ } catch (ex) {
+ this.originalAlertsCID = null;
+ }
+ try {
+ this.originalSystemAlertsCID = registrar.contractIDToCID(
+ SYSTEM_ALERTS_SERVICE_CONTRACT_ID
+ );
+ } catch (ex) {
+ this.originalSystemAlertsCID = null;
+ }
+
+ registrar.registerFactory(
+ MOCK_ALERTS_CID,
+ "alerts service",
+ ALERTS_SERVICE_CONTRACT_ID,
+ mockAlertsService
+ );
+
+ registrar.registerFactory(
+ MOCK_SYSTEM_ALERTS_CID,
+ "system alerts service",
+ SYSTEM_ALERTS_SERVICE_CONTRACT_ID,
+ mockAlertsService
+ );
+ },
+
+ unregister() {
+ registrar.unregisterFactory(MOCK_ALERTS_CID, mockAlertsService);
+ registrar.unregisterFactory(MOCK_SYSTEM_ALERTS_CID, mockAlertsService);
+
+ // Passing `null` for the factory re-maps the contract ID to the
+ // entry for its original CID.
+
+ if (this.originalAlertsCID) {
+ registrar.registerFactory(
+ this.originalAlertsCID,
+ "alerts service",
+ ALERTS_SERVICE_CONTRACT_ID,
+ null
+ );
+ }
+
+ if (this.originalSystemAlertsCID) {
+ registrar.registerFactory(
+ this.originalSystemAlertsCID,
+ "system alerts service",
+ SYSTEM_ALERTS_SERVICE_CONTRACT_ID,
+ null
+ );
+ }
+ },
+
+ activeAlertNotifications,
+
+ activeAppNotifications,
+ };
+})();
diff --git a/dom/notification/test/mochitest/NotificationTest.js b/dom/notification/test/mochitest/NotificationTest.js
new file mode 100644
index 0000000000..400ff56253
--- /dev/null
+++ b/dom/notification/test/mochitest/NotificationTest.js
@@ -0,0 +1,102 @@
+var NotificationTest = (function () {
+ "use strict";
+
+ function info(msg, name) {
+ SimpleTest.info("::Notification Tests::" + (name || ""), msg);
+ }
+
+ function setup_testing_env() {
+ SimpleTest.waitForExplicitFinish();
+ // turn on testing pref (used by notification.cpp, and mock the alerts
+ return SpecialPowers.setBoolPref("notification.prompt.testing", true);
+ }
+
+ async function teardown_testing_env() {
+ await SpecialPowers.clearUserPref("notification.prompt.testing");
+ await SpecialPowers.clearUserPref("notification.prompt.testing.allow");
+
+ SimpleTest.finish();
+ }
+
+ function executeTests(tests, callback) {
+ // context is `this` object in test functions
+ // it can be used to track data between tests
+ var context = {};
+
+ (function executeRemainingTests(remainingTests) {
+ if (!remainingTests.length) {
+ callback();
+ return;
+ }
+
+ var nextTest = remainingTests.shift();
+ var finishTest = executeRemainingTests.bind(null, remainingTests);
+ var startTest = nextTest.call.bind(nextTest, context, finishTest);
+
+ try {
+ startTest();
+ // if no callback was defined for test function,
+ // we must manually invoke finish to continue
+ if (nextTest.length === 0) {
+ finishTest();
+ }
+ } catch (e) {
+ ok(false, "Test threw exception!");
+ finishTest();
+ }
+ })(tests);
+ }
+
+ // NotificationTest API
+ return {
+ run(tests, callback) {
+ let ready = setup_testing_env();
+
+ addLoadEvent(async function () {
+ await ready;
+ executeTests(tests, function () {
+ teardown_testing_env();
+ callback && callback();
+ });
+ });
+ },
+
+ allowNotifications() {
+ return SpecialPowers.setBoolPref(
+ "notification.prompt.testing.allow",
+ true
+ );
+ },
+
+ denyNotifications() {
+ return SpecialPowers.setBoolPref(
+ "notification.prompt.testing.allow",
+ false
+ );
+ },
+
+ clickNotification(notification) {
+ // TODO: how??
+ },
+
+ fireCloseEvent(title) {
+ window.dispatchEvent(
+ new CustomEvent("mock-notification-close-event", {
+ detail: {
+ title,
+ },
+ })
+ );
+ },
+
+ info,
+
+ payload: {
+ body: "Body",
+ tag: "fakeTag",
+ icon: "icon.jpg",
+ lang: "en-US",
+ dir: "ltr",
+ },
+ };
+})();
diff --git a/dom/notification/test/mochitest/blank.html b/dom/notification/test/mochitest/blank.html
new file mode 100644
index 0000000000..1f9324523a
--- /dev/null
+++ b/dom/notification/test/mochitest/blank.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<html>
+ <body></body>
+</html>
diff --git a/dom/notification/test/mochitest/create_notification.html b/dom/notification/test/mochitest/create_notification.html
new file mode 100644
index 0000000000..b0387e4ffb
--- /dev/null
+++ b/dom/notification/test/mochitest/create_notification.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset=utf-8>
+ <title>Create a notification</title>
+</head>
+<body>
+<script>
+
+var notification = new Notification("This is a title", {
+ body: "This is a notification body",
+ tag: "sometag",
+ });
+
+</script>
+</body>
+</html>
diff --git a/dom/notification/test/mochitest/mochitest.toml b/dom/notification/test/mochitest/mochitest.toml
new file mode 100644
index 0000000000..19320326fc
--- /dev/null
+++ b/dom/notification/test/mochitest/mochitest.toml
@@ -0,0 +1,30 @@
+[DEFAULT]
+
+support-files = [
+ "blank.html",
+ "create_notification.html",
+ "MockServices.js",
+ "NotificationTest.js",
+]
+
+["test_notification_basics.html"]
+skip-if = ["xorigin"] # Bug 1792790
+
+["test_notification_crossorigin_iframe.html"]
+scheme = "https"
+# This test needs to be run on HTTP (not HTTPS).
+
+["test_notification_insecure_context.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_notification_permissions.html"]
+scheme = "https"
+
+["test_notification_tag.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
diff --git a/dom/notification/test/mochitest/test_notification_basics.html b/dom/notification/test/mochitest/test_notification_basics.html
new file mode 100644
index 0000000000..3dde839a96
--- /dev/null
+++ b/dom/notification/test/mochitest/test_notification_basics.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Notification Basics</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="MockServices.js"></script>
+ <script type="text/javascript" src="NotificationTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript">
+
+ var info = NotificationTest.info;
+ var options;
+
+ SimpleTest.requestFlakyTimeout("untriaged");
+
+ var steps = [
+ function() {
+ info("Test notification spec");
+ ok(Notification, "Notification constructor exists");
+ ok(Notification.permission, "Notification.permission exists");
+ ok(Notification.requestPermission, "Notification.requestPermission exists");
+ },
+
+ function() {
+ info("Test requestPermission without callback");
+ Notification.requestPermission();
+ },
+
+ async function(done) {
+ info("Test requestPermission deny");
+ function assertPermissionDenied(perm) {
+ is(perm, "denied", "Permission should be denied.");
+ is(Notification.permission, "denied", "Permission should be denied.");
+ }
+ await NotificationTest.denyNotifications();
+ Notification.requestPermission()
+ .then(assertPermissionDenied)
+ .then(_ => Notification.requestPermission(assertPermissionDenied))
+ .catch(err => {
+ ok(!err, "requestPermission should not reject promise");
+ })
+ .then(done);
+ },
+
+ async function(done) {
+ info("Test requestPermission grant");
+ function assertPermissionGranted(perm) {
+ is(perm, "granted", "Permission should be granted.");
+ is(Notification.permission, "granted", "Permission should be granted");
+ }
+ await NotificationTest.allowNotifications();
+ Notification.requestPermission()
+ .then(assertPermissionGranted)
+ .then(_ => Notification.requestPermission(assertPermissionGranted))
+ .catch(err => {
+ ok(!err, "requestPermission should not reject promise");
+ })
+ .then(done);
+ },
+
+ function(done) {
+ info("Test invalid requestPermission");
+ Notification.requestPermission({})
+ .then(_ => {
+ ok(false, "Non callable arg to requestPermission should reject promise");
+ }, err => {
+ ok(true, "Non callable arg to requestPermission should reject promise");
+ })
+ .then(done);
+ },
+
+ function(done) {
+ info("Test create notification");
+
+ options = NotificationTest.payload;
+
+ var notification = new Notification("This is a title", options);
+
+ ok(notification, "Notification exists");
+ is(notification.onclick, null, "onclick() should be null");
+ is(notification.onshow, null, "onshow() should be null");
+ is(notification.onerror, null, "onerror() should be null");
+ is(notification.onclose, null, "onclose() should be null");
+ is(typeof notification.close, "function", "close() should exist");
+
+ is(notification.dir, options.dir, "auto should get set");
+ is(notification.lang, options.lang, "lang should get set");
+ is(notification.body, options.body, "body should get set");
+ is(notification.tag, options.tag, "tag should get set");
+ is(notification.icon, options.icon, "icon should get set");
+
+ // store notification in test context
+ this.notification = notification;
+
+ notification.onshow = function() {
+ ok(true, "onshow handler should be called");
+ done();
+ };
+ },
+
+ function(done) {
+ info("Test closing a notification");
+ var notification = this.notification;
+
+ notification.onclose = function() {
+ ok(true, "onclose handler should be called");
+ done();
+ };
+
+ notification.close();
+ },
+ ];
+
+ MockServices.register();
+ NotificationTest.run(steps, function() {
+ MockServices.unregister();
+ });
+</script>
+</body>
+</html>
diff --git a/dom/notification/test/mochitest/test_notification_crossorigin_iframe.html b/dom/notification/test/mochitest/test_notification_crossorigin_iframe.html
new file mode 100644
index 0000000000..0abfce3722
--- /dev/null
+++ b/dom/notification/test/mochitest/test_notification_crossorigin_iframe.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests that Notification permissions are denied in cross-origin iframes.
+https://bugzilla.mozilla.org/show_bug.cgi?id=1560741
+-->
+<head>
+ <title>Notification permission in cross-origin iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ const kBlankURL = "https://example.org/tests/dom/notification/test/mochitest/blank.html";
+
+ (async function runTest() {
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["notification.prompt.testing", true],
+ ["notification.prompt.testing.allow", true],
+ ]});
+
+ let iframe = document.createElement("iframe");
+ iframe.src = kBlankURL;
+ document.body.appendChild(iframe);
+ await new Promise(resolve => {
+ iframe.onload = resolve;
+ });
+
+ let checkRequest = async (expectedResponse, msg) => {
+ let response = await this.content.Notification.requestPermission();
+ Assert.equal(response, expectedResponse, msg);
+ };
+
+ await SpecialPowers.spawn(iframe,
+ ["denied", "Denied permission in cross-origin iframe"],
+ checkRequest);
+
+ let checkPermission = async (expectedPermission, msg) => {
+ let permission = this.content.Notification.permission;
+ Assert.equal(permission, expectedPermission, msg);
+ };
+
+ await SpecialPowers.spawn(iframe,
+ ["denied", "Permission is denied in cross-origin iframe"],
+ checkPermission);
+
+ await SpecialPowers.pushPrefEnv({"set": [["dom.webnotifications.allowcrossoriginiframe", true]]});
+
+ await SpecialPowers.spawn(iframe,
+ ["granted", "Granted permission in cross-origin iframe with pref set"],
+ checkRequest);
+ await SpecialPowers.spawn(iframe,
+ ["granted", "Permission is granted in cross-origin iframe with pref set"],
+ checkPermission);
+
+ SimpleTest.finish();
+ })();
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/dom/notification/test/mochitest/test_notification_insecure_context.html b/dom/notification/test/mochitest/test_notification_insecure_context.html
new file mode 100644
index 0000000000..bb64db64a9
--- /dev/null
+++ b/dom/notification/test/mochitest/test_notification_insecure_context.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests that Notification permissions are denied in insecure context.
+https://bugzilla.mozilla.org/show_bug.cgi?id=1429432
+-->
+<head>
+ <title>Notification permission in insecure context</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ // Add an allow permission for the mochitest origin to test this.
+ let script = SpecialPowers.loadChromeScript(function() {
+ /* eslint-env mozilla/chrome-script */
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin("http://mochi.test:8888");
+ Services.perms.addFromPrincipal(principal, "desktop-notification", Services.perms.ALLOW_ACTION);
+ addMessageListener("destroy", function() {
+ Services.perms.removeFromPrincipal(principal, "desktop-notification");
+ });
+ });
+
+ (async function runTest() {
+ let response = await Notification.requestPermission();
+ is(response, "denied", "Denied permission in insecure context");
+
+ script.sendAsyncMessage("destroy");
+ script.destroy();
+
+ SimpleTest.finish();
+ })();
+ </script>
+ </pre>
+</body>
+</html>
diff --git a/dom/notification/test/mochitest/test_notification_permissions.html b/dom/notification/test/mochitest/test_notification_permissions.html
new file mode 100644
index 0000000000..0290fb9c87
--- /dev/null
+++ b/dom/notification/test/mochitest/test_notification_permissions.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Tests that the Notification.requestPermission and navigator.permissions.query
+return values are consistent with the stored permission.
+https://bugzilla.mozilla.org/show_bug.cgi?id=1589754
+-->
+<head>
+ <title>Notification permissions and permissions API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+<script class="testbody">
+
+add_task(async function test_notifications_permission() {
+ await SpecialPowers.clearUserPref("notification.prompt.testing");
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Automatically dismiss the permission request when it appears.
+ ["dom.webnotifications.requireuserinteraction", true],
+ ],
+ });
+
+ async function testPermissionInWindow(win) {
+ async function checkPermission(perm, expectedResult, expectedPermission) {
+ await SpecialPowers.pushPermissions([
+ {
+ type: "desktop-notification",
+ allow: SpecialPowers.Ci.nsIPermissionManager[perm],
+ context: document,
+ },
+ ]);
+ is(
+ await win.Notification.requestPermission(),
+ expectedResult,
+ `expected requestPermission() result for permission ${perm}`
+ );
+
+ let result =
+ await win.navigator.permissions.query({ name: "notifications" });
+ is(
+ result.state,
+ expectedPermission,
+ `expected permissions API result for permission ${perm}`
+ );
+ }
+
+ await checkPermission("UNKNOWN_ACTION", "default", "prompt");
+ await checkPermission("ALLOW_ACTION", "granted", "granted");
+ await checkPermission("DENY_ACTION", "denied", "denied");
+ await checkPermission("PROMPT_ACTION", "default", "prompt");
+ }
+
+ var win = window.open("blank.html");
+ await new Promise(resolve => { win.onload = resolve; });
+ await testPermissionInWindow(win);
+ win.close();
+});
+
+</script>
+ </pre>
+</body>
+</html>
diff --git a/dom/notification/test/mochitest/test_notification_tag.html b/dom/notification/test/mochitest/test_notification_tag.html
new file mode 100644
index 0000000000..f4fc72bbe3
--- /dev/null
+++ b/dom/notification/test/mochitest/test_notification_tag.html
@@ -0,0 +1,169 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=782211
+-->
+<head>
+ <title>Bug 782211</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=782211">Bug 782211</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+<script type="text/javascript">
+ /* eslint-disable mozilla/use-chromeutils-generateqi */
+
+ // The mock is not a general purpose mock, but is specific for this test.
+ // It is always registered in the parent process using LoadChromeScript by
+ // the MockAlertsService below, to allow this to work regardless of whether
+ // the frames from different origins live in the same process or in different
+ // processes (with Fission), since the default content-process alerts service
+ // relays messages to the parent process.
+ function mockServicesChromeScript() {
+ /* eslint-env mozilla/chrome-script */
+ const MOCK_CID = Components.ID("{dbe37e64-d9a3-402c-8d8a-0826c619f7ad}");
+ const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1";
+
+ var notificationsCreated = [];
+
+ const mockAlertsService = {
+ showAlert(alert, alertListener) {
+ notificationsCreated.push(alert.name);
+ if (notificationsCreated.length == 3) {
+ // notifications created by the test1 origin
+ var test1notifications = [];
+ // notifications created by the test2 origin
+ var test2notifications = [];
+ for (var i = 0; i < notificationsCreated.length; i++) {
+ var notificationName = notificationsCreated[i];
+ if (notificationName.includes("test1")) {
+ test1notifications.push(notificationsCreated[i]);
+ } else if (notificationName.includes("test2")) {
+ test2notifications.push(notificationsCreated[i]);
+ }
+ }
+
+ is(
+ test1notifications.length,
+ 2,
+ "2 notifications should be created by test1.example.org:80 origin."
+ );
+ is(
+ test1notifications[0],
+ test1notifications[1],
+ "notification names should be identical."
+ );
+ is(
+ test2notifications.length,
+ 1,
+ "1 notification should be created by test2.example.org:80 origin."
+ );
+
+ // Register original alerts service.
+ registrar.unregisterFactory(MOCK_CID, this);
+
+ sendAsyncMessage("mock-alert-service:unregistered");
+ }
+ },
+
+ showAlertNotification(
+ imageUrl,
+ title,
+ text,
+ textClickable,
+ cookie,
+ alertListener,
+ name,
+ dir,
+ lang,
+ data
+ ) {
+ this.showAlert({ name });
+ },
+
+ QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsISupports) || aIID.equals(Ci.nsIAlertsService)) {
+ return this;
+ }
+ throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
+ },
+
+ createInstance(aIID) {
+ return this.QueryInterface(aIID);
+ },
+ };
+
+ const registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+ registrar.registerFactory(
+ MOCK_CID,
+ "alerts service",
+ ALERTS_SERVICE_CONTRACT_ID,
+ mockAlertsService
+ );
+
+ const { sendAsyncMessage } = this;
+
+ sendAsyncMessage("mock-alert-service:registered");
+ }
+
+ const MockAlertsService = {
+ async register() {
+ if (this._chromeScript) {
+ throw new Error("MockAlertsService already registered");
+ }
+ this._chromeScript = SpecialPowers.loadChromeScript(
+ mockServicesChromeScript
+ );
+ await this._chromeScript.promiseOneMessage("mock-alert-service:registered");
+ },
+ async unregistered() {
+ await this._chromeScript.promiseOneMessage(
+ "mock-alert-service:unregistered"
+ );
+ },
+ };
+
+ if (window.Notification) {
+ SimpleTest.waitForExplicitFinish();
+
+ async function showNotifications() {
+ await MockAlertsService.register();
+
+ // Load two frames with the same origin that create notification with the same tag.
+ // Both pages should generate notifications with the same name, and thus the second
+ // notification should replace the first.
+ let sameDomain = window.open("http://test1.example.org:80/tests/dom/notification/test/mochitest/create_notification.html");
+ let anotherSameDomain = window.open("http://test1.example.org:80/tests/dom/notification/test/mochitest/create_notification.html");
+ // Load a frame with a different origin that creates a notification with the same tag.
+ // The notification name should be different and thus no notifications should be replaced.
+ let crossDomain = window.open("http://test2.example.org:80/tests/dom/notification/test/mochitest/create_notification.html");
+
+ await MockAlertsService.unregistered();
+
+ sameDomain.close();
+ anotherSameDomain.close();
+ crossDomain.close();
+ SimpleTest.finish();
+ }
+
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["notification.prompt.testing", true],
+ ["notification.prompt.testing.allow", true],
+ ],
+ },
+ showNotifications
+ );
+ } else {
+ ok(true, "Notifications are not enabled on the platform.");
+ }
+</script>
+</body>
+</html>
diff --git a/dom/notification/test/unit/head_notificationdb.js b/dom/notification/test/unit/head_notificationdb.js
new file mode 100644
index 0000000000..1b23d88729
--- /dev/null
+++ b/dom/notification/test/unit/head_notificationdb.js
@@ -0,0 +1,61 @@
+"use strict";
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+function getNotificationObject(app, id, tag, includeScope) {
+ const origin = `https://${app}.gaiamobile.org/`;
+ return {
+ origin,
+ id,
+ title: app + "Notification:" + Date.now(),
+ dir: "auto",
+ lang: "",
+ body: app + " notification body",
+ tag: tag || "",
+ icon: "icon.png",
+ serviceWorkerRegistrationScope: includeScope ? origin : undefined,
+ };
+}
+
+var systemNotification = getNotificationObject(
+ "system",
+ "{2bc883bf-2809-4432-b0f4-f54e10372764}"
+);
+
+var calendarNotification = getNotificationObject(
+ "calendar",
+ "{d8d11299-a58e-429b-9a9a-57c562982fbf}"
+);
+
+// Helper to start the NotificationDB
+function startNotificationDB() {
+ ChromeUtils.importESModule("resource://gre/modules/NotificationDB.sys.mjs");
+}
+
+// Helper function to add a listener, send message and treat the reply
+function addAndSend(msg, reply, callback, payload, runNext = true) {
+ let handler = {
+ receiveMessage(message) {
+ if (message.name === reply) {
+ Services.cpmm.removeMessageListener(reply, handler);
+ callback(message);
+ if (runNext) {
+ run_next_test();
+ }
+ }
+ },
+ };
+ Services.cpmm.addMessageListener(reply, handler);
+ Services.cpmm.sendAsyncMessage(msg, payload);
+}
+
+// helper fonction, comparing two notifications
+function compareNotification(notif1, notif2) {
+ // retrieved notification should be the second one sent
+ for (let prop in notif1) {
+ // compare each property
+ Assert.equal(notif1[prop], notif2[prop]);
+ }
+}
diff --git a/dom/notification/test/unit/test_notificationdb.js b/dom/notification/test/unit/test_notificationdb.js
new file mode 100644
index 0000000000..33397f87f3
--- /dev/null
+++ b/dom/notification/test/unit/test_notificationdb.js
@@ -0,0 +1,340 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+ startNotificationDB();
+ run_next_test();
+}
+
+// Get one notification, none exists
+add_test(function test_get_none() {
+ let requestID = 0;
+ let msgReply = "Notification:GetAll:Return:OK";
+ let msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ Assert.equal(0, message.data.notifications.length);
+ };
+
+ addAndSend("Notification:GetAll", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ requestID,
+ });
+});
+
+// Store one notification
+add_test(function test_send_one() {
+ let requestID = 1;
+ let msgReply = "Notification:Save:Return:OK";
+ let msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Save", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ notification: systemNotification,
+ requestID,
+ });
+});
+
+// Get one notification, one exists
+add_test(function test_get_one() {
+ let requestID = 2;
+ let msgReply = "Notification:GetAll:Return:OK";
+ let msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ Assert.equal(1, message.data.notifications.length);
+ // compare the content
+ compareNotification(systemNotification, message.data.notifications[0]);
+ };
+
+ addAndSend("Notification:GetAll", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ requestID,
+ });
+});
+
+// Delete one notification
+add_test(function test_delete_one() {
+ let requestID = 3;
+ let msgReply = "Notification:Delete:Return:OK";
+ let msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Delete", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ id: systemNotification.id,
+ requestID,
+ });
+});
+
+// Get one notification, none exists
+add_test(function test_get_none_again() {
+ let requestID = 4;
+ let msgReply = "Notification:GetAll:Return:OK";
+ let msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ Assert.equal(0, message.data.notifications.length);
+ };
+
+ addAndSend("Notification:GetAll", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ requestID,
+ });
+});
+
+// Delete one notification that do not exists anymore
+add_test(function test_delete_one_nonexistent() {
+ let requestID = 5;
+ let msgReply = "Notification:Delete:Return:OK";
+ let msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Delete", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ id: systemNotification.id,
+ requestID,
+ });
+});
+
+// Store two notifications with the same id
+add_test(function test_send_two_get_one() {
+ let requestID = 6;
+ let calls = 0;
+
+ let msgGetReply = "Notification:GetAll:Return:OK";
+ let msgGetHandler = function (message) {
+ Assert.equal(requestID + 2, message.data.requestID);
+ Assert.equal(1, message.data.notifications.length);
+ // compare the content
+ compareNotification(systemNotification, message.data.notifications[0]);
+ };
+
+ let msgSaveReply = "Notification:Save:Return:OK";
+ let msgSaveHandler = function (message) {
+ calls += 1;
+ if (calls === 2) {
+ addAndSend("Notification:GetAll", msgGetReply, msgGetHandler, {
+ origin: systemNotification.origin,
+ requestID: requestID + 2,
+ });
+ }
+ };
+
+ addAndSend(
+ "Notification:Save",
+ msgSaveReply,
+ msgSaveHandler,
+ {
+ origin: systemNotification.origin,
+ notification: systemNotification,
+ requestID,
+ },
+ false
+ );
+
+ addAndSend(
+ "Notification:Save",
+ msgSaveReply,
+ msgSaveHandler,
+ {
+ origin: systemNotification.origin,
+ notification: systemNotification,
+ requestID: requestID + 1,
+ },
+ false
+ );
+});
+
+// Delete previous notification
+add_test(function test_delete_previous() {
+ let requestID = 8;
+ let msgReply = "Notification:Delete:Return:OK";
+ let msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Delete", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ id: systemNotification.id,
+ requestID,
+ });
+});
+
+// Store two notifications from same origin with the same tag
+add_test(function test_send_two_get_one() {
+ let requestID = 10;
+ let tag = "voicemail";
+
+ let systemNotification1 = getNotificationObject(
+ "system",
+ "{f271f9ee-3955-4c10-b1f2-af552fb270ee}",
+ tag
+ );
+ let systemNotification2 = getNotificationObject(
+ "system",
+ "{8ef9a628-f0f4-44b4-820d-c117573c33e3}",
+ tag
+ );
+
+ let msgGetReply = "Notification:GetAll:Return:OK";
+ let msgGetNotifHandler = {
+ receiveMessage(message) {
+ if (message.name === msgGetReply) {
+ Services.cpmm.removeMessageListener(msgGetReply, msgGetNotifHandler);
+ let notifications = message.data.notifications;
+ // same tag, so replaced
+ Assert.equal(1, notifications.length);
+ // compare the content
+ compareNotification(systemNotification2, notifications[0]);
+ run_next_test();
+ }
+ },
+ };
+
+ Services.cpmm.addMessageListener(msgGetReply, msgGetNotifHandler);
+
+ let msgSaveReply = "Notification:Save:Return:OK";
+ let msgSaveCalls = 0;
+ let msgSaveHandler = function (message) {
+ msgSaveCalls++;
+ // Once both request have been sent, trigger getall
+ if (msgSaveCalls === 2) {
+ Services.cpmm.sendAsyncMessage("Notification:GetAll", {
+ origin: systemNotification1.origin,
+ requestID: message.data.requestID + 2, // 12, 13
+ });
+ }
+ };
+
+ addAndSend(
+ "Notification:Save",
+ msgSaveReply,
+ msgSaveHandler,
+ {
+ origin: systemNotification1.origin,
+ notification: systemNotification1,
+ requestID, // 10
+ },
+ false
+ );
+
+ addAndSend(
+ "Notification:Save",
+ msgSaveReply,
+ msgSaveHandler,
+ {
+ origin: systemNotification2.origin,
+ notification: systemNotification2,
+ requestID: requestID + 1, // 11
+ },
+ false
+ );
+});
+
+// Delete previous notification
+add_test(function test_delete_previous() {
+ let requestID = 15;
+ let msgReply = "Notification:Delete:Return:OK";
+ let msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Delete", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ id: "{8ef9a628-f0f4-44b4-820d-c117573c33e3}",
+ requestID,
+ });
+});
+
+// Store two notifications from two origins with the same tag
+add_test(function test_send_two_get_two() {
+ let requestID = 20;
+ let tag = "voicemail";
+
+ let systemNotification1 = systemNotification;
+ systemNotification1.tag = tag;
+
+ let calendarNotification2 = calendarNotification;
+ calendarNotification2.tag = tag;
+
+ let msgGetReply = "Notification:GetAll:Return:OK";
+ let msgGetCalls = 0;
+ let msgGetHandler = {
+ receiveMessage(message) {
+ if (message.name === msgGetReply) {
+ msgGetCalls++;
+ let notifications = message.data.notifications;
+
+ // one notification per origin
+ Assert.equal(1, notifications.length);
+
+ // first call should be system notification
+ if (msgGetCalls === 1) {
+ compareNotification(systemNotification1, notifications[0]);
+ }
+
+ // second and last call should be calendar notification
+ if (msgGetCalls === 2) {
+ Services.cpmm.removeMessageListener(msgGetReply, msgGetHandler);
+ compareNotification(calendarNotification2, notifications[0]);
+ run_next_test();
+ }
+ }
+ },
+ };
+ Services.cpmm.addMessageListener(msgGetReply, msgGetHandler);
+
+ let msgSaveReply = "Notification:Save:Return:OK";
+ let msgSaveCalls = 0;
+ let msgSaveHandler = {
+ receiveMessage(message) {
+ if (message.name === msgSaveReply) {
+ msgSaveCalls++;
+ if (msgSaveCalls === 2) {
+ Services.cpmm.removeMessageListener(msgSaveReply, msgSaveHandler);
+
+ // Trigger getall for each origin
+ Services.cpmm.sendAsyncMessage("Notification:GetAll", {
+ origin: systemNotification1.origin,
+ requestID: message.data.requestID + 1, // 22
+ });
+
+ Services.cpmm.sendAsyncMessage("Notification:GetAll", {
+ origin: calendarNotification2.origin,
+ requestID: message.data.requestID + 2, // 23
+ });
+ }
+ }
+ },
+ };
+ Services.cpmm.addMessageListener(msgSaveReply, msgSaveHandler);
+
+ Services.cpmm.sendAsyncMessage("Notification:Save", {
+ origin: systemNotification1.origin,
+ notification: systemNotification1,
+ requestID, // 20
+ });
+
+ Services.cpmm.sendAsyncMessage("Notification:Save", {
+ origin: calendarNotification2.origin,
+ notification: calendarNotification2,
+ requestID: requestID + 1, // 21
+ });
+});
+
+// Cleanup previous notification
+add_test(function test_delete_previous() {
+ let requestID = 25;
+ let msgReply = "Notification:Delete:Return:OK";
+ let msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ };
+
+ addAndSend("Notification:Delete", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ id: "{2bc883bf-2809-4432-b0f4-f54e10372764}",
+ requestID,
+ });
+});
diff --git a/dom/notification/test/unit/test_notificationdb_bug1024090.js b/dom/notification/test/unit/test_notificationdb_bug1024090.js
new file mode 100644
index 0000000000..37dee0a639
--- /dev/null
+++ b/dom/notification/test/unit/test_notificationdb_bug1024090.js
@@ -0,0 +1,59 @@
+"use strict";
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+// For bug 1024090: test edge case of notificationstore.json
+add_test(function test_bug1024090_purge() {
+ const NOTIFICATION_STORE_PATH = PathUtils.join(
+ PathUtils.profileDir,
+ "notificationstore"
+ );
+ let cleanup = IOUtils.remove(NOTIFICATION_STORE_PATH, { recursive: true });
+ cleanup
+ .then(
+ function onSuccess() {
+ ok(true, "Notification database cleaned.");
+ },
+ function onError(reason) {
+ ok(false, "Notification database error when cleaning: " + reason);
+ }
+ )
+ .then(function next() {
+ info("Cleanup steps completed: " + NOTIFICATION_STORE_PATH);
+ startNotificationDB();
+ run_next_test();
+ });
+});
+
+// Store one notification
+add_test(function test_bug1024090_send_one() {
+ let requestID = 1;
+ let msgReply = "Notification:Save:Return:OK";
+ let msgHandler = function (message) {
+ equal(requestID, message.data.requestID, "Checking requestID");
+ };
+
+ addAndSend("Notification:Save", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ notification: systemNotification,
+ requestID,
+ });
+});
+
+// Get one notification, one exists
+add_test(function test_bug1024090_get_one() {
+ let requestID = 2;
+ let msgReply = "Notification:GetAll:Return:OK";
+ let msgHandler = function (message) {
+ equal(requestID, message.data.requestID, "Checking requestID");
+ equal(1, message.data.notifications.length, "One notification stored");
+ };
+
+ addAndSend("Notification:GetAll", msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ requestID,
+ });
+});
diff --git a/dom/notification/test/unit/test_notificationdb_migration.js b/dom/notification/test/unit/test_notificationdb_migration.js
new file mode 100644
index 0000000000..2b38e8d43a
--- /dev/null
+++ b/dom/notification/test/unit/test_notificationdb_migration.js
@@ -0,0 +1,129 @@
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const fooNotification = getNotificationObject(
+ "foo",
+ "a4f1d54a-98b7-4231-9120-5afc26545bad",
+ null,
+ true
+);
+const barNotification = getNotificationObject(
+ "bar",
+ "a4f1d54a-98b7-4231-9120-5afc26545bad",
+ "baz",
+ true
+);
+const msg = "Notification:GetAll";
+const msgReply = "Notification:GetAll:Return:OK";
+
+do_get_profile();
+const OLD_STORE_PATH = PathUtils.join(
+ PathUtils.profileDir,
+ "notificationstore.json"
+);
+
+let nextRequestID = 0;
+
+// Create the old datastore and populate it with data before we initialize
+// the notification database so it has data to migrate. This is a setup step,
+// not a test, but it seems like we need to do it in a test function
+// rather than in run_test() because the test runner doesn't handle async steps
+// in run_test().
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_NOTIFICATION_STORE,
+ },
+ async function test_create_old_datastore() {
+ const notifications = {
+ [fooNotification.origin]: {
+ [fooNotification.id]: fooNotification,
+ },
+ [barNotification.origin]: {
+ [barNotification.id]: barNotification,
+ },
+ };
+
+ await IOUtils.writeJSON(OLD_STORE_PATH, notifications);
+
+ startNotificationDB();
+ }
+);
+
+add_test(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_NOTIFICATION_STORE,
+ },
+ function test_get_system_notification() {
+ const requestID = nextRequestID++;
+ const msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ Assert.equal(0, message.data.notifications.length);
+ };
+
+ addAndSend(msg, msgReply, msgHandler, {
+ origin: systemNotification.origin,
+ requestID,
+ });
+ }
+);
+
+add_test(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_NOTIFICATION_STORE,
+ },
+ function test_get_foo_notification() {
+ const requestID = nextRequestID++;
+ const msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ Assert.equal(1, message.data.notifications.length);
+ Assert.deepEqual(
+ fooNotification,
+ message.data.notifications[0],
+ "Notification data migrated"
+ );
+ };
+
+ addAndSend(msg, msgReply, msgHandler, {
+ origin: fooNotification.origin,
+ requestID,
+ });
+ }
+);
+
+add_test(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_NOTIFICATION_STORE,
+ },
+ function test_get_bar_notification() {
+ const requestID = nextRequestID++;
+ const msgHandler = function (message) {
+ Assert.equal(requestID, message.data.requestID);
+ Assert.equal(1, message.data.notifications.length);
+ Assert.deepEqual(
+ barNotification,
+ message.data.notifications[0],
+ "Notification data migrated"
+ );
+ };
+
+ addAndSend(msg, msgReply, msgHandler, {
+ origin: barNotification.origin,
+ requestID,
+ });
+ }
+);
+
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_NEW_NOTIFICATION_STORE,
+ },
+ async function test_old_datastore_deleted() {
+ Assert.ok(
+ !(await IOUtils.exists(OLD_STORE_PATH)),
+ "old datastore no longer exists"
+ );
+ }
+);
diff --git a/dom/notification/test/unit/xpcshell.toml b/dom/notification/test/unit/xpcshell.toml
new file mode 100644
index 0000000000..178af95b1f
--- /dev/null
+++ b/dom/notification/test/unit/xpcshell.toml
@@ -0,0 +1,9 @@
+[DEFAULT]
+head = "head_notificationdb.js"
+skip-if = ["os == 'android'"]
+
+["test_notificationdb.js"]
+
+["test_notificationdb_bug1024090.js"]
+
+["test_notificationdb_migration.js"]