782 lines
26 KiB
C++
782 lines
26 KiB
C++
/* 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<glean::web_notification::PermissionOriginLabel>(
|
|
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<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;
|
|
}
|
|
}
|
|
|
|
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<nsINotificationStorage> 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<NotificationsPromise> Promise() {
|
|
return mPromiseHolder.Ensure(__func__);
|
|
}
|
|
|
|
NS_IMETHOD Done(
|
|
const nsTArray<RefPtr<nsINotificationStorageEntry>>& aEntries) final {
|
|
AssertIsOnMainThread();
|
|
|
|
nsTArray<IPCNotification> 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<NotificationsPromise> mPromiseHolder;
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(NotificationsCallback, nsINotificationStorageCallback)
|
|
|
|
already_AddRefed<NotificationsPromise> 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<NotificationsCallback> callback = new NotificationsCallback();
|
|
RefPtr<NotificationsPromise> promise = callback->Promise();
|
|
|
|
nsCOMPtr<nsINotificationStorage> 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<nsINotificationStorage> 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<NotificationStorageEntry> 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<nsINotificationStorage> notificationStorage =
|
|
GetNotificationStorage(aPrincipal->GetIsInPrivateBrowsing())) {
|
|
nsString origin;
|
|
MOZ_TRY(GetOrigin(aPrincipal, origin));
|
|
return notificationStorage->Delete(origin, aId);
|
|
}
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
nsresult UnpersistAllNotificationsExcept(const nsTArray<nsString>& aIds) {
|
|
// Cleanup makes only sense for on-disk storage
|
|
if (nsCOMPtr<nsINotificationStorage> notificationStorage =
|
|
GetNotificationStorage(false)) {
|
|
return notificationStorage->DeleteAllExcept(aIds);
|
|
}
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
void UnregisterNotification(nsIPrincipal* aPrincipal, const nsString& aId) {
|
|
UnpersistNotification(aPrincipal, aId);
|
|
if (nsCOMPtr<nsIAlertsService> alertService = components::Alerts::Service()) {
|
|
alertService->CloseAlert(aId, /* aContextClosed */ false);
|
|
}
|
|
}
|
|
|
|
nsresult ShowAlertWithCleanup(nsIAlertNotification* aAlert,
|
|
nsIAlertCallbacks* aAlertCallbacks) {
|
|
nsCOMPtr<nsIAlertsService> 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<nsString> 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<nsIPermissionManager> 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<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;
|
|
}
|
|
|
|
nsresult AdjustPushQuota(nsIPrincipal* aPrincipal,
|
|
NotificationStatusChange aChange) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
nsCOMPtr<nsIPushQuotaManager> 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<nsISiteCategory> 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<NotificationPermissionPromise> 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<NotificationPermissionPromise> mPromiseHolder;
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(SafeBrowsingClassificationCallback, nsIURIClassifierCallback)
|
|
|
|
RefPtr<NotificationPermissionPromise> 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<nsIURIClassifier> uriClassifier =
|
|
do_GetService(NS_URICLASSIFIERSERVICE_CONTRACTID, &rv);
|
|
|
|
if (NS_FAILED(rv) || !uriClassifier) {
|
|
NS_WARNING("URI classifier unavailable for notification check");
|
|
} else {
|
|
RefPtr<SafeBrowsingClassificationCallback> callback =
|
|
new SafeBrowsingClassificationCallback();
|
|
RefPtr<NotificationPermissionPromise> 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<nsCOMPtr<nsIAlertNotification>, nsresult> CreateAlertForNotification(
|
|
const IPCNotificationOptions& aOptions, nsIPrincipal& aPrincipal,
|
|
Maybe<IPCImage>&& 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<nsIAlertNotification> alert =
|
|
do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
|
|
if (!alert) {
|
|
return Err(NS_ERROR_NOT_AVAILABLE);
|
|
}
|
|
|
|
nsCOMPtr<nsIPrincipal> principal = &aPrincipal;
|
|
nsAutoCString iconUrl;
|
|
if (RefPtr<nsIURI> 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<imgIContainer> image =
|
|
nsContentUtils::IPCImageToImage(*aIcon)) {
|
|
alert->SetImage(image);
|
|
}
|
|
}
|
|
|
|
if (StaticPrefs::dom_webnotifications_actions_enabled()) {
|
|
nsTArray<RefPtr<nsIAlertAction>> 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<IPCNotificationAction, nsresult> 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<RefPtr<nsINotificationActionStorageEntry>>& aRetVal) {
|
|
nsTArray<RefPtr<nsINotificationActionStorageEntry>> 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<IPCNotification, nsresult> 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<NotificationDirection>(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<RefPtr<nsINotificationActionStorageEntry>> actionEntries;
|
|
MOZ_TRY(aEntry.GetActions(actionEntries));
|
|
nsTArray<IPCNotificationAction> 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
|