/* -*- 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 "NotificationUtils.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/glean/DomNotificationMetrics.h" #include "nsContentUtils.h" #include "nsIAlertsService.h" #include "nsINotificationStorage.h" #include "nsIPermissionManager.h" #include "nsIPushService.h" #include "nsServiceManagerUtils.h" 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; 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, nsContentUtils::eDOM_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 false; } // third party ReportTelemetry(GleanLabel::eThirdParty, aPurpose); if (aRequestorDoc) { nsContentUtils::ReportToConsole( nsIScriptError::errorFlag, "DOM"_ns, aRequestorDoc, nsContentUtils::eDOM_PROPERTIES, "NotificationsCrossOriginIframeRequestIsForbidden"); } return !StaticPrefs::dom_webnotifications_allowcrossoriginiframe(); } 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; } MOZ_TRY( nsContentUtils::GetWebExposedOriginSerialization(aPrincipal, aOrigin)); return NS_OK; } void ComputeAlertName(nsIPrincipal* aPrincipal, const nsString& aTag, const nsString& aId, nsString& aResult) { nsAutoString alertName; nsresult rv = GetOrigin(aPrincipal, 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 (!aTag.IsEmpty()) { alertName.AppendLiteral("tag:"); alertName.Append(aTag); } else { alertName.AppendLiteral("notag:"); alertName.Append(aId); } aResult = alertName; } nsCOMPtr GetNotificationStorage(bool isPrivate) { return do_GetService(isPrivate ? NS_MEMORY_NOTIFICATION_STORAGE_CONTRACTID : NS_NOTIFICATION_STORAGE_CONTRACTID); } nsresult PersistNotification(nsIPrincipal* aPrincipal, const nsString& aId, const nsString& aAlertName, const IPCNotificationOptions& aOptions, 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; } rv = notificationStorage->Put( origin, aId, aOptions.title(), GetEnumString(aOptions.dir()), aOptions.lang(), aOptions.body(), aOptions.tag(), aOptions.icon(), aAlertName, aOptions.dataSerialized(), 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; } void UnregisterNotification(nsIPrincipal* aPrincipal, const nsString& aId, const nsString& aAlertName, CloseMode aCloseMode) { // XXX: unpersist only when explicitly closed, bug 1095073 UnpersistNotification(aPrincipal, aId); if (nsCOMPtr alertService = components::Alerts::Service()) { alertService->CloseAlert( aAlertName, /* aContextClosed */ aCloseMode == CloseMode::InactiveGlobal); } } 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()); } } // namespace mozilla::dom::notification