/* 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 "NotificationUtils.h" #include "mozilla/AlertNotification.h" #include "mozilla/BasePrincipal.h" #include "mozilla/Components.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/dom/DOMTypes.h" #include "mozilla/dom/NotificationBinding.h" #include "mozilla/dom/notification/NotificationHandler.h" #include "mozilla/glean/DomNotificationMetrics.h" #include "nsComponentManagerUtils.h" #include "nsContentUtils.h" #include "nsIAlertsService.h" #include "nsINotificationStorage.h" #include "nsIPermissionManager.h" #include "nsIPushService.h" #include "nsISiteCategory.h" #include "nsIURIClassifier.h" #include "nsNetUtil.h" #include "nsServiceManagerUtils.h" static bool gTriedStorageCleanup = false; namespace mozilla::dom::notification { using GleanLabel = glean::web_notification::ShowOriginLabel; static void ReportTelemetry(GleanLabel aLabel, PermissionCheckPurpose aPurpose) { switch (aPurpose) { case PermissionCheckPurpose::PermissionAttribute: glean::web_notification::permission_origin .EnumGet(static_cast( aLabel)) .Add(); return; case PermissionCheckPurpose::PermissionRequest: glean::web_notification::request_permission_origin .EnumGet(static_cast< glean::web_notification::RequestPermissionOriginLabel>( aLabel)) .Add(); return; case PermissionCheckPurpose::NotificationShow: glean::web_notification::show_origin.EnumGet(aLabel).Add(); return; case PermissionCheckPurpose::LoadImageForShow: // This will always be followed by a NotificationShow permissions check // anyway. return; default: MOZ_CRASH("Unknown permission checker"); return; } } bool IsNotificationAllowedFor(nsIPrincipal* aPrincipal) { if (aPrincipal->IsSystemPrincipal()) { return true; } // Allow files to show notifications by default. return aPrincipal->SchemeIs("file"); } bool IsNotificationForbiddenFor(nsIPrincipal* aPrincipal, nsIPrincipal* aEffectiveStoragePrincipal, bool isSecureContext, PermissionCheckPurpose aPurpose, Document* aRequestorDoc) { if (aPrincipal->GetIsInPrivateBrowsing() && !StaticPrefs::dom_webnotifications_privateBrowsing_enabled()) { return true; } if (!isSecureContext) { if (aRequestorDoc) { glean::web_notification::insecure_context_permission_request.Add(); nsContentUtils::ReportToConsole( nsIScriptError::errorFlag, "DOM"_ns, aRequestorDoc, PropertiesFile::DOM_PROPERTIES, "NotificationsInsecureRequestIsForbidden"); } return true; } const nsString& partitionKey = aEffectiveStoragePrincipal->OriginAttributesRef().mPartitionKey; if (aEffectiveStoragePrincipal->OriginAttributesRef() .mPartitionKey.IsEmpty()) { // first party ReportTelemetry(GleanLabel::eFirstParty, aPurpose); return false; } nsString outScheme; nsString outBaseDomain; int32_t outPort; bool outForeignByAncestorContext; OriginAttributes::ParsePartitionKey(partitionKey, outScheme, outBaseDomain, outPort, outForeignByAncestorContext); if (outForeignByAncestorContext) { // nested first party ReportTelemetry(GleanLabel::eNestedFirstParty, aPurpose); return StaticPrefs:: dom_webnotifications_forbid_nested_first_party_enabled(); } // third party ReportTelemetry(GleanLabel::eThirdParty, aPurpose); if (aRequestorDoc) { nsContentUtils::ReportToConsole( nsIScriptError::errorFlag, "DOM"_ns, aRequestorDoc, PropertiesFile::DOM_PROPERTIES, "NotificationsCrossOriginIframeRequestIsForbidden"); } return true; } NotificationPermission GetRawNotificationPermission(nsIPrincipal* aPrincipal) { AssertIsOnMainThread(); uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION; nsCOMPtr 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; } } NotificationPermission GetNotificationPermission( nsIPrincipal* aPrincipal, nsIPrincipal* aEffectiveStoragePrincipal, bool isSecureContext, PermissionCheckPurpose aPurpose) { if (IsNotificationAllowedFor(aPrincipal)) { return NotificationPermission::Granted; } if (IsNotificationForbiddenFor(aPrincipal, aEffectiveStoragePrincipal, isSecureContext, aPurpose)) { return NotificationPermission::Denied; } return GetRawNotificationPermission(aPrincipal); } nsresult GetOrigin(nsIPrincipal* aPrincipal, nsString& aOrigin) { if (!aPrincipal) { return NS_ERROR_FAILURE; } nsAutoCString origin; MOZ_TRY(aPrincipal->GetOrigin(origin)); CopyUTF8toUTF16(origin, aOrigin); return NS_OK; } nsCOMPtr GetNotificationStorage(bool isPrivate) { return do_GetService(isPrivate ? NS_MEMORY_NOTIFICATION_STORAGE_CONTRACTID : NS_NOTIFICATION_STORAGE_CONTRACTID); } class NotificationsCallback : public nsINotificationStorageCallback { public: NS_DECL_ISUPPORTS already_AddRefed Promise() { return mPromiseHolder.Ensure(__func__); } NS_IMETHOD Done( const nsTArray>& aEntries) final { AssertIsOnMainThread(); nsTArray notifications(aEntries.Length()); for (const auto& entry : aEntries) { auto result = NotificationStorageEntry::ToIPC(*entry); if (result.isErr()) { continue; } MOZ_ASSERT(!result.inspect().id().IsEmpty()); notifications.AppendElement(result.unwrap()); } mPromiseHolder.Resolve(std::move(notifications), __func__); return NS_OK; } protected: virtual ~NotificationsCallback() { // We may be shutting down prematurely without getting the result, so make // sure to settle the promise. mPromiseHolder.RejectIfExists(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); }; MozPromiseHolder mPromiseHolder; }; NS_IMPL_ISUPPORTS(NotificationsCallback, nsINotificationStorageCallback) already_AddRefed GetStoredNotificationsForScope( nsIPrincipal* aPrincipal, const nsACString& aScope, const nsAString& aTag) { nsString origin; nsresult rv = GetOrigin(aPrincipal, origin); if (NS_WARN_IF(NS_FAILED(rv))) { return NotificationsPromise::CreateAndReject(rv, __func__).forget(); } RefPtr callback = new NotificationsCallback(); RefPtr promise = callback->Promise(); nsCOMPtr notificationStorage = GetNotificationStorage(aPrincipal->GetIsInPrivateBrowsing()); if (!notificationStorage) { return NotificationsPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__) .forget(); } rv = notificationStorage->Get(origin, NS_ConvertUTF8toUTF16(aScope), aTag, callback); if (NS_WARN_IF(NS_FAILED(rv))) { return NotificationsPromise::CreateAndReject(rv, __func__).forget(); } return promise.forget(); } nsresult PersistNotification(nsIPrincipal* aPrincipal, const IPCNotification& aNotification, const nsString& aScope) { nsCOMPtr notificationStorage = GetNotificationStorage(aPrincipal->GetIsInPrivateBrowsing()); if (NS_WARN_IF(!notificationStorage)) { return NS_ERROR_UNEXPECTED; } nsString origin; nsresult rv = GetOrigin(aPrincipal, origin); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } RefPtr entry = new NotificationStorageEntry(aNotification); rv = notificationStorage->Put(origin, entry, aScope); if (NS_FAILED(rv)) { return rv; } return NS_OK; } nsresult UnpersistNotification(nsIPrincipal* aPrincipal, const nsString& aId) { if (!aPrincipal) { return NS_ERROR_FAILURE; } if (nsCOMPtr notificationStorage = GetNotificationStorage(aPrincipal->GetIsInPrivateBrowsing())) { nsString origin; MOZ_TRY(GetOrigin(aPrincipal, origin)); return notificationStorage->Delete(origin, aId); } return NS_ERROR_FAILURE; } nsresult UnpersistAllNotificationsExcept(const nsTArray& aIds) { // Cleanup makes only sense for on-disk storage if (nsCOMPtr notificationStorage = GetNotificationStorage(false)) { return notificationStorage->DeleteAllExcept(aIds); } return NS_ERROR_FAILURE; } void UnregisterNotification(nsIPrincipal* aPrincipal, const nsString& aId) { UnpersistNotification(aPrincipal, aId); if (nsCOMPtr alertService = components::Alerts::Service()) { alertService->CloseAlert(aId, /* aContextClosed */ false); } } nsresult ShowAlertWithCleanup(nsIAlertNotification* aAlert, nsIAlertCallbacks* aAlertCallbacks) { nsCOMPtr alertService = components::Alerts::Service(); if (!gTriedStorageCleanup || StaticPrefs:: dom_webnotifications_testing_force_storage_cleanup_enabled()) { // The below may fail, but retry probably won't make it work gTriedStorageCleanup = true; // Get the list of currently displayed notifications known to the // notification backend and unpersist all other notifications from // NotificationDB. // (This won't affect the following persist call by ShowAlert, as the DB // maintains a job queue) // Note that we ignore the result of GetHistory - we still go ahead and // clears notifications even if it fails, as the failure implies there's no // history and thus we should clear everything. nsTArray history; (void)alertService->GetHistory(history); UnpersistAllNotificationsExcept(history); } MOZ_TRY(alertService->ShowAlertWithCallbacks(aAlert, aAlertCallbacks)); return NS_OK; } nsresult RemovePermission(nsIPrincipal* aPrincipal) { MOZ_ASSERT(XRE_IsParentProcess()); nsCOMPtr permissionManager = mozilla::components::PermissionManager::Service(); if (!permissionManager) { return NS_ERROR_FAILURE; } permissionManager->RemoveFromPrincipal(aPrincipal, "desktop-notification"_ns); return NS_OK; } nsresult OpenSettings(nsIPrincipal* aPrincipal) { MOZ_ASSERT(XRE_IsParentProcess()); nsCOMPtr obs = mozilla::services::GetObserverService(); if (!obs) { return NS_ERROR_FAILURE; } // Notify other observers so they can show settings UI. obs->NotifyObservers(aPrincipal, "notifications-open-settings", nullptr); return NS_OK; } nsresult AdjustPushQuota(nsIPrincipal* aPrincipal, NotificationStatusChange aChange) { MOZ_ASSERT(XRE_IsParentProcess()); nsCOMPtr pushQuotaManager = do_GetService("@mozilla.org/push/Service;1"); if (!pushQuotaManager) { return NS_ERROR_FAILURE; } nsAutoCString origin; MOZ_TRY(aPrincipal->GetOrigin(origin)); if (aChange == NotificationStatusChange::Shown) { return pushQuotaManager->NotificationForOriginShown(origin.get()); } return pushQuotaManager->NotificationForOriginClosed(origin.get()); } NotificationCallbacksCommon::NotificationCallbacksCommon( const nsAString& aScope, nsIPrincipal* aPrincipal, IPCNotification aNotification) : mScope(aScope), mPrincipal(aPrincipal), mNotification(std::move(aNotification)) { if (nsCOMPtr siteCategory = do_GetService("@mozilla.org/site-category;1")) { nsCString category; if (NS_SUCCEEDED(siteCategory->GetCategory(mPrincipal, category))) { mCategory = Some(category); } } } NotificationCallbacksCommon::~NotificationCallbacksCommon() = default; NS_IMETHODIMP NotificationCallbacksCommon::OnAlertDisable() { glean::web_notification::clicked.Record( Some(glean::web_notification::ClickedExtra{.action = Some("disable"_ns), .siteCategory = mCategory})); return RemovePermission(mPrincipal); } NS_IMETHODIMP NotificationCallbacksCommon::OnAlertSettings() { glean::web_notification::clicked.Record( Some(glean::web_notification::ClickedExtra{.action = Some("settings"_ns), .siteCategory = mCategory})); return OpenSettings(mPrincipal); } NS_IMETHODIMP NotificationCallbacksCommon::OnAlertShow() { mShown = true; glean::web_notification::shown.Record( Some(glean::web_notification::ShownExtra{.siteCategory = mCategory})); return NS_OK; } NS_IMETHODIMP NotificationCallbacksCommon::OnAlertClick( nsIAlertAction* aAction) { mClicked = true; glean::web_notification::clicked.Record( Some(glean::web_notification::ClickedExtra{ .action = Some(aAction ? "action-button"_ns : "body"_ns), .siteCategory = mCategory})); return NS_OK; } NS_IMETHODIMP NotificationCallbacksCommon::OnAlertDismissedFromForeground() { glean::web_notification::ignored.Record( Some(glean::web_notification::IgnoredExtra{.siteCategory = mCategory})); return NS_OK; } NS_IMETHODIMP NotificationCallbacksCommon::OnAlertClosed() { if (mShown && !mClicked) { glean::web_notification::dismissed.Record(Some( glean::web_notification::DismissedExtra{.siteCategory = mCategory})); } return NS_OK; } NS_IMETHODIMP NotificationCallbacksCommon::OnAlertFinished() { if (mShown && !mClicked) { glean::web_notification::dismissed.Record(Some( glean::web_notification::DismissedExtra{.siteCategory = mCategory})); } return NS_OK; } void NotificationCallbacksCommon::PersistNotification() { (void)NS_WARN_IF( NS_FAILED(AdjustPushQuota(mPrincipal, NotificationStatusChange::Shown))); nsresult rv = notification::PersistNotification(mPrincipal, mNotification, mScope); if (NS_FAILED(rv)) { NS_WARNING("Could not persist Notification"); } } void NotificationCallbacksCommon::UnpersistNotification() { (void)NS_WARN_IF( NS_FAILED(AdjustPushQuota(mPrincipal, NotificationStatusChange::Closed))); (void)NS_WARN_IF(NS_FAILED( notification::UnpersistNotification(mPrincipal, mNotification.id()))); } nsresult NotificationCallbacksCommon::RespondOnClick(nsIAlertAction* aAction) { nsAutoString actionName; if (aAction) { MOZ_TRY(aAction->GetAction(actionName)); } return notification::RespondOnClick(mPrincipal, mScope, mNotification, actionName); } NS_IMPL_ISUPPORTS(NotificationCallbacksCommon, nsIAlertCallbacks) class SafeBrowsingClassificationCallback final : public nsIURIClassifierCallback { public: NS_DECL_ISUPPORTS SafeBrowsingClassificationCallback() = default; already_AddRefed Promise() { return mPromiseHolder.Ensure(__func__); } NS_IMETHOD OnClassifyComplete(nsresult aErrorCode, const nsACString& aList, const nsACString& aProvider, const nsACString& aFullHash) override { if (NS_FAILED(aErrorCode)) { mPromiseHolder.Reject(aErrorCode, __func__); } else { mPromiseHolder.Resolve(Ok(), __func__); } return NS_OK; } private: ~SafeBrowsingClassificationCallback() { mPromiseHolder.RejectIfExists(NS_ERROR_ABORT, __func__); } MozPromiseHolder mPromiseHolder; }; NS_IMPL_ISUPPORTS(SafeBrowsingClassificationCallback, nsIURIClassifierCallback) RefPtr EnsureValidNotificationPermission( nsIPrincipal* aPrincipal, nsIPrincipal* aEffectiveStoragePrincipal, bool aIsSecureContext) { NotificationPermission permission = GetNotificationPermission( aPrincipal, aEffectiveStoragePrincipal, aIsSecureContext, PermissionCheckPurpose::NotificationShow); if (permission != NotificationPermission::Granted) { return NotificationPermissionPromise::CreateAndReject( NS_ERROR_DOM_NOT_ALLOWED_ERR, __func__); } // Check Safe Browsing blocklist if the feature is enabled (bug 1986300). if (StaticPrefs::dom_webnotifications_block_if_on_safebrowsing()) { nsresult rv = NS_OK; nsCOMPtr uriClassifier = do_GetService(NS_URICLASSIFIERSERVICE_CONTRACTID, &rv); if (NS_FAILED(rv) || !uriClassifier) { NS_WARNING("URI classifier unavailable for notification check"); } else { RefPtr callback = new SafeBrowsingClassificationCallback(); RefPtr promise = callback->Promise(); bool willClassify = false; rv = uriClassifier->Classify(aPrincipal, callback, &willClassify); if (NS_SUCCEEDED(rv) && willClassify) { promise->Then( GetMainThreadSerialEventTarget(), __func__, [principal = RefPtr(aPrincipal)]( const NotificationPermissionPromise::ResolveOrRejectValue& aResult) { if (aResult.IsReject()) { // Remove permission if it's on the blocklist RemovePermission(principal); } }); return promise; } } } return NotificationPermissionPromise::CreateAndResolve(Ok(), __func__); } Result, nsresult> CreateAlertForNotification( const IPCNotificationOptions& aOptions, nsIPrincipal& aPrincipal, Maybe&& aIcon) { // Step 4.3 the show steps, which are almost all about processing `tag` and // then displaying the notification. Both are handled by // nsIAlertsService::ShowAlert. The below is all about constructing the // observer (for show and close events) right and ultimately call the alerts // service function. // In the case of IPC, the parent process uses the cookie to map to // nsIObserver. Thus the cookie must be unique to differentiate observers. // XXX(krosylight): This is about ContentChild::mAlertObserver which is not // useful when called by the parent process. This should be removed when we // make nsIAlertsService parent process only. nsString obsoleteCookie = u"notification:"_ns; bool requireInteraction = aOptions.requireInteraction(); if (!StaticPrefs::dom_webnotifications_requireinteraction_enabled()) { requireInteraction = false; } nsCOMPtr alert = do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID); if (!alert) { return Err(NS_ERROR_NOT_AVAILABLE); } nsCOMPtr principal = &aPrincipal; nsAutoCString iconUrl; if (RefPtr iconUri = aOptions.icon()) { iconUri->GetSpec(iconUrl); } MOZ_TRY(alert->Init(aOptions.tag(), NS_ConvertUTF8toUTF16(iconUrl), aOptions.title(), aOptions.body(), true, obsoleteCookie, NS_ConvertASCIItoUTF16(GetEnumString(aOptions.dir())), aOptions.lang(), aOptions.dataSerialized(), principal, principal->GetIsInPrivateBrowsing(), requireInteraction, aOptions.silent(), aOptions.vibrate())); if (aIcon) { if (nsCOMPtr image = nsContentUtils::IPCImageToImage(*aIcon)) { alert->SetImage(image); } } if (StaticPrefs::dom_webnotifications_actions_enabled()) { nsTArray> actions; MOZ_ASSERT(aOptions.actions().Length() <= kMaxActions); for (const auto& action : aOptions.actions()) { actions.AppendElement( new AlertAction(action.name(), action.title(), action.navigate())); } alert->SetActions(actions); } return alert; } NS_IMPL_ISUPPORTS(NotificationActionStorageEntry, nsINotificationActionStorageEntry) NS_IMETHODIMP NotificationActionStorageEntry::GetName(nsAString& aName) { aName = mIPCAction.name(); return NS_OK; } NS_IMETHODIMP NotificationActionStorageEntry::GetTitle(nsAString& aTitle) { aTitle = mIPCAction.title(); return NS_OK; } NS_IMETHODIMP NotificationActionStorageEntry::GetNavigate( nsACString& aNavigate) { nsIURI* navigateUri = mIPCAction.navigate(); if (!navigateUri) { aNavigate.SetIsVoid(true); return NS_OK; } navigateUri->GetSpec(aNavigate); return NS_OK; } Result NotificationActionStorageEntry::ToIPC( nsINotificationActionStorageEntry& aEntry) { IPCNotificationAction action; MOZ_TRY(aEntry.GetName(action.name())); MOZ_TRY(aEntry.GetTitle(action.title())); if (StaticPrefs::dom_webnotifications_navigate_enabled()) { nsAutoCString navigateUrl; MOZ_TRY(aEntry.GetNavigate(navigateUrl)); if (!navigateUrl.IsVoid()) { MOZ_TRY(NS_NewURI(getter_AddRefs(action.navigate()), navigateUrl)); } } return action; } NS_IMPL_ISUPPORTS(NotificationStorageEntry, nsINotificationStorageEntry) NS_IMETHODIMP NotificationStorageEntry::GetId(nsAString& aId) { aId = mIPCNotification.id(); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetTitle(nsAString& aTitle) { aTitle = mIPCNotification.options().title(); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetDir(nsACString& aDir) { aDir = GetEnumString(mIPCNotification.options().dir()); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetLang(nsAString& aLang) { aLang = mIPCNotification.options().lang(); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetBody(nsAString& aBody) { aBody = mIPCNotification.options().body(); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetTag(nsAString& aTag) { aTag = mIPCNotification.options().tag(); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetIcon(nsACString& aIcon) { nsIURI* iconUri = mIPCNotification.options().icon(); if (!iconUri) { aIcon.Truncate(); return NS_OK; } iconUri->GetSpec(aIcon); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetNavigate(nsACString& aNavigate) { nsIURI* navigateUri = mIPCNotification.options().navigate(); if (!navigateUri) { aNavigate.SetIsVoid(true); return NS_OK; } navigateUri->GetSpec(aNavigate); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetRequireInteraction( bool* aRequireInteraction) { *aRequireInteraction = mIPCNotification.options().requireInteraction(); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetSilent(bool* aSilent) { *aSilent = mIPCNotification.options().silent(); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetDataSerialized( nsAString& aDataSerialized) { aDataSerialized = mIPCNotification.options().dataSerialized(); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetActions( nsTArray>& aRetVal) { nsTArray> actions( mIPCNotification.options().actions().Length()); for (const auto& action : mIPCNotification.options().actions()) { actions.AppendElement(new NotificationActionStorageEntry(action)); } aRetVal = std::move(actions); return NS_OK; } NS_IMETHODIMP NotificationStorageEntry::GetServiceWorkerRegistrationScope( nsAString& aScope) { // Scope is only provided from JS, for now // TODO(krosylight): Change nsINotificationStorage::Put to provide scope via // storage entry? aScope.SetIsVoid(true); return NS_OK; } Result NotificationStorageEntry::ToIPC( nsINotificationStorageEntry& aEntry) { IPCNotification notification; IPCNotificationOptions& options = notification.options(); MOZ_TRY(aEntry.GetId(notification.id())); MOZ_TRY(aEntry.GetTitle(options.title())); nsCString dir; MOZ_TRY(aEntry.GetDir(dir)); options.dir() = StringToEnum(dir).valueOr( NotificationDirection::Auto); MOZ_TRY(aEntry.GetLang(options.lang())); MOZ_TRY(aEntry.GetBody(options.body())); MOZ_TRY(aEntry.GetTag(options.tag())); nsAutoCString iconUrl; MOZ_TRY(aEntry.GetIcon(iconUrl)); if (!iconUrl.IsEmpty()) { MOZ_TRY(NS_NewURI(getter_AddRefs(notification.options().icon()), iconUrl)); } if (StaticPrefs::dom_webnotifications_navigate_enabled()) { nsAutoCString navigateUrl; MOZ_TRY(aEntry.GetNavigate(navigateUrl)); if (!navigateUrl.IsVoid()) { MOZ_TRY(NS_NewURI(getter_AddRefs(notification.options().navigate()), navigateUrl)); } } MOZ_TRY(aEntry.GetRequireInteraction(&options.requireInteraction())); MOZ_TRY(aEntry.GetSilent(&options.silent())); MOZ_TRY(aEntry.GetDataSerialized(options.dataSerialized())); nsTArray> actionEntries; MOZ_TRY(aEntry.GetActions(actionEntries)); nsTArray actions(actionEntries.Length()); for (const auto& actionEntry : actionEntries) { IPCNotificationAction action = MOZ_TRY(NotificationActionStorageEntry::ToIPC(*actionEntry)); actions.AppendElement(std::move(action)); } options.actions() = std::move(actions); return notification; } } // namespace mozilla::dom::notification