NotificationUtils.cpp (16397B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "NotificationUtils.h" 8 9 #include "mozilla/BasePrincipal.h" 10 #include "mozilla/Components.h" 11 #include "mozilla/StaticPrefs_dom.h" 12 #include "mozilla/dom/DOMTypes.h" 13 #include "mozilla/dom/NotificationBinding.h" 14 #include "mozilla/glean/DomNotificationMetrics.h" 15 #include "nsContentUtils.h" 16 #include "nsIAlertsService.h" 17 #include "nsINotificationStorage.h" 18 #include "nsIPermissionManager.h" 19 #include "nsIPushService.h" 20 #include "nsNetUtil.h" 21 #include "nsServiceManagerUtils.h" 22 23 static bool gTriedStorageCleanup = false; 24 25 namespace mozilla::dom::notification { 26 27 using GleanLabel = glean::web_notification::ShowOriginLabel; 28 29 static void ReportTelemetry(GleanLabel aLabel, 30 PermissionCheckPurpose aPurpose) { 31 switch (aPurpose) { 32 case PermissionCheckPurpose::PermissionAttribute: 33 glean::web_notification::permission_origin 34 .EnumGet(static_cast<glean::web_notification::PermissionOriginLabel>( 35 aLabel)) 36 .Add(); 37 return; 38 case PermissionCheckPurpose::PermissionRequest: 39 glean::web_notification::request_permission_origin 40 .EnumGet(static_cast< 41 glean::web_notification::RequestPermissionOriginLabel>( 42 aLabel)) 43 .Add(); 44 return; 45 case PermissionCheckPurpose::NotificationShow: 46 glean::web_notification::show_origin.EnumGet(aLabel).Add(); 47 return; 48 default: 49 MOZ_CRASH("Unknown permission checker"); 50 return; 51 } 52 } 53 54 bool IsNotificationAllowedFor(nsIPrincipal* aPrincipal) { 55 if (aPrincipal->IsSystemPrincipal()) { 56 return true; 57 } 58 // Allow files to show notifications by default. 59 return aPrincipal->SchemeIs("file"); 60 } 61 62 bool IsNotificationForbiddenFor(nsIPrincipal* aPrincipal, 63 nsIPrincipal* aEffectiveStoragePrincipal, 64 bool isSecureContext, 65 PermissionCheckPurpose aPurpose, 66 Document* aRequestorDoc) { 67 if (aPrincipal->GetIsInPrivateBrowsing() && 68 !StaticPrefs::dom_webnotifications_privateBrowsing_enabled()) { 69 return true; 70 } 71 72 if (!isSecureContext) { 73 if (aRequestorDoc) { 74 glean::web_notification::insecure_context_permission_request.Add(); 75 nsContentUtils::ReportToConsole( 76 nsIScriptError::errorFlag, "DOM"_ns, aRequestorDoc, 77 nsContentUtils::eDOM_PROPERTIES, 78 "NotificationsInsecureRequestIsForbidden"); 79 } 80 return true; 81 } 82 83 const nsString& partitionKey = 84 aEffectiveStoragePrincipal->OriginAttributesRef().mPartitionKey; 85 86 if (aEffectiveStoragePrincipal->OriginAttributesRef() 87 .mPartitionKey.IsEmpty()) { 88 // first party 89 ReportTelemetry(GleanLabel::eFirstParty, aPurpose); 90 return false; 91 } 92 nsString outScheme; 93 nsString outBaseDomain; 94 int32_t outPort; 95 bool outForeignByAncestorContext; 96 OriginAttributes::ParsePartitionKey(partitionKey, outScheme, outBaseDomain, 97 outPort, outForeignByAncestorContext); 98 if (outForeignByAncestorContext) { 99 // nested first party 100 ReportTelemetry(GleanLabel::eNestedFirstParty, aPurpose); 101 return StaticPrefs:: 102 dom_webnotifications_forbid_nested_first_party_enabled(); 103 } 104 105 // third party 106 ReportTelemetry(GleanLabel::eThirdParty, aPurpose); 107 if (aRequestorDoc) { 108 nsContentUtils::ReportToConsole( 109 nsIScriptError::errorFlag, "DOM"_ns, aRequestorDoc, 110 nsContentUtils::eDOM_PROPERTIES, 111 "NotificationsCrossOriginIframeRequestIsForbidden"); 112 } 113 return !StaticPrefs::dom_webnotifications_allowcrossoriginiframe(); 114 } 115 116 NotificationPermission GetRawNotificationPermission(nsIPrincipal* aPrincipal) { 117 AssertIsOnMainThread(); 118 119 uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION; 120 121 nsCOMPtr<nsIPermissionManager> permissionManager = 122 components::PermissionManager::Service(); 123 if (!permissionManager) { 124 return NotificationPermission::Default; 125 } 126 127 permissionManager->TestExactPermissionFromPrincipal( 128 aPrincipal, "desktop-notification"_ns, &permission); 129 130 // Convert the result to one of the enum types. 131 switch (permission) { 132 case nsIPermissionManager::ALLOW_ACTION: 133 return NotificationPermission::Granted; 134 case nsIPermissionManager::DENY_ACTION: 135 return NotificationPermission::Denied; 136 default: 137 return NotificationPermission::Default; 138 } 139 } 140 141 NotificationPermission GetNotificationPermission( 142 nsIPrincipal* aPrincipal, nsIPrincipal* aEffectiveStoragePrincipal, 143 bool isSecureContext, PermissionCheckPurpose aPurpose) { 144 if (IsNotificationAllowedFor(aPrincipal)) { 145 return NotificationPermission::Granted; 146 } 147 if (IsNotificationForbiddenFor(aPrincipal, aEffectiveStoragePrincipal, 148 isSecureContext, aPurpose)) { 149 return NotificationPermission::Denied; 150 } 151 152 return GetRawNotificationPermission(aPrincipal); 153 } 154 155 nsresult GetOrigin(nsIPrincipal* aPrincipal, nsString& aOrigin) { 156 if (!aPrincipal) { 157 return NS_ERROR_FAILURE; 158 } 159 160 nsAutoCString origin; 161 MOZ_TRY(aPrincipal->GetOrigin(origin)); 162 163 CopyUTF8toUTF16(origin, aOrigin); 164 165 return NS_OK; 166 } 167 168 nsCOMPtr<nsINotificationStorage> GetNotificationStorage(bool isPrivate) { 169 return do_GetService(isPrivate ? NS_MEMORY_NOTIFICATION_STORAGE_CONTRACTID 170 : NS_NOTIFICATION_STORAGE_CONTRACTID); 171 } 172 173 class NotificationsCallback : public nsINotificationStorageCallback { 174 public: 175 NS_DECL_ISUPPORTS 176 177 already_AddRefed<NotificationsPromise> Promise() { 178 return mPromiseHolder.Ensure(__func__); 179 } 180 181 NS_IMETHOD Done( 182 const nsTArray<RefPtr<nsINotificationStorageEntry>>& aEntries) final { 183 AssertIsOnMainThread(); 184 185 nsTArray<IPCNotification> notifications(aEntries.Length()); 186 for (const auto& entry : aEntries) { 187 auto result = NotificationStorageEntry::ToIPC(*entry); 188 if (result.isErr()) { 189 continue; 190 } 191 MOZ_ASSERT(!result.inspect().id().IsEmpty()); 192 notifications.AppendElement(result.unwrap()); 193 } 194 195 mPromiseHolder.Resolve(std::move(notifications), __func__); 196 return NS_OK; 197 } 198 199 protected: 200 virtual ~NotificationsCallback() { 201 // We may be shutting down prematurely without getting the result, so make 202 // sure to settle the promise. 203 mPromiseHolder.RejectIfExists(NS_ERROR_DOM_INVALID_STATE_ERR, __func__); 204 }; 205 206 MozPromiseHolder<NotificationsPromise> mPromiseHolder; 207 }; 208 209 NS_IMPL_ISUPPORTS(NotificationsCallback, nsINotificationStorageCallback) 210 211 already_AddRefed<NotificationsPromise> GetStoredNotificationsForScope( 212 nsIPrincipal* aPrincipal, const nsACString& aScope, const nsAString& aTag) { 213 nsString origin; 214 nsresult rv = GetOrigin(aPrincipal, origin); 215 if (NS_WARN_IF(NS_FAILED(rv))) { 216 return NotificationsPromise::CreateAndReject(rv, __func__).forget(); 217 } 218 219 RefPtr<NotificationsCallback> callback = new NotificationsCallback(); 220 RefPtr<NotificationsPromise> promise = callback->Promise(); 221 222 nsCOMPtr<nsINotificationStorage> notificationStorage = 223 GetNotificationStorage(aPrincipal->GetIsInPrivateBrowsing()); 224 if (!notificationStorage) { 225 return NotificationsPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, 226 __func__) 227 .forget(); 228 } 229 230 rv = notificationStorage->Get(origin, NS_ConvertUTF8toUTF16(aScope), aTag, 231 callback); 232 if (NS_WARN_IF(NS_FAILED(rv))) { 233 return NotificationsPromise::CreateAndReject(rv, __func__).forget(); 234 } 235 return promise.forget(); 236 } 237 238 nsresult PersistNotification(nsIPrincipal* aPrincipal, 239 const IPCNotification& aNotification, 240 const nsString& aScope) { 241 nsCOMPtr<nsINotificationStorage> notificationStorage = 242 GetNotificationStorage(aPrincipal->GetIsInPrivateBrowsing()); 243 if (NS_WARN_IF(!notificationStorage)) { 244 return NS_ERROR_UNEXPECTED; 245 } 246 247 nsString origin; 248 nsresult rv = GetOrigin(aPrincipal, origin); 249 if (NS_WARN_IF(NS_FAILED(rv))) { 250 return rv; 251 } 252 253 RefPtr<NotificationStorageEntry> entry = 254 new NotificationStorageEntry(aNotification); 255 256 rv = notificationStorage->Put(origin, entry, aScope); 257 258 if (NS_FAILED(rv)) { 259 return rv; 260 } 261 262 return NS_OK; 263 } 264 265 nsresult UnpersistNotification(nsIPrincipal* aPrincipal, const nsString& aId) { 266 if (!aPrincipal) { 267 return NS_ERROR_FAILURE; 268 } 269 if (nsCOMPtr<nsINotificationStorage> notificationStorage = 270 GetNotificationStorage(aPrincipal->GetIsInPrivateBrowsing())) { 271 nsString origin; 272 MOZ_TRY(GetOrigin(aPrincipal, origin)); 273 return notificationStorage->Delete(origin, aId); 274 } 275 return NS_ERROR_FAILURE; 276 } 277 278 nsresult UnpersistAllNotificationsExcept(const nsTArray<nsString>& aIds) { 279 // Cleanup makes only sense for on-disk storage 280 if (nsCOMPtr<nsINotificationStorage> notificationStorage = 281 GetNotificationStorage(false)) { 282 return notificationStorage->DeleteAllExcept(aIds); 283 } 284 return NS_ERROR_FAILURE; 285 } 286 287 void UnregisterNotification(nsIPrincipal* aPrincipal, const nsString& aId) { 288 UnpersistNotification(aPrincipal, aId); 289 if (nsCOMPtr<nsIAlertsService> alertService = components::Alerts::Service()) { 290 alertService->CloseAlert(aId, /* aContextClosed */ false); 291 } 292 } 293 294 nsresult ShowAlertWithCleanup(nsIAlertNotification* aAlert, 295 nsIObserver* aAlertListener) { 296 nsCOMPtr<nsIAlertsService> alertService = components::Alerts::Service(); 297 if (!gTriedStorageCleanup || 298 StaticPrefs:: 299 dom_webnotifications_testing_force_storage_cleanup_enabled()) { 300 // The below may fail, but retry probably won't make it work 301 gTriedStorageCleanup = true; 302 303 // Get the list of currently displayed notifications known to the 304 // notification backend and unpersist all other notifications from 305 // NotificationDB. 306 // (This won't affect the following persist call by ShowAlert, as the DB 307 // maintains a job queue) 308 // Note that we ignore the result of GetHistory - we still go ahead and 309 // clears notifications even if it fails, as the failure implies there's no 310 // history and thus we should clear everything. 311 nsTArray<nsString> history; 312 (void)alertService->GetHistory(history); 313 UnpersistAllNotificationsExcept(history); 314 } 315 316 MOZ_TRY(alertService->ShowAlert(aAlert, aAlertListener)); 317 return NS_OK; 318 } 319 320 nsresult RemovePermission(nsIPrincipal* aPrincipal) { 321 MOZ_ASSERT(XRE_IsParentProcess()); 322 nsCOMPtr<nsIPermissionManager> permissionManager = 323 mozilla::components::PermissionManager::Service(); 324 if (!permissionManager) { 325 return NS_ERROR_FAILURE; 326 } 327 permissionManager->RemoveFromPrincipal(aPrincipal, "desktop-notification"_ns); 328 return NS_OK; 329 } 330 331 nsresult OpenSettings(nsIPrincipal* aPrincipal) { 332 MOZ_ASSERT(XRE_IsParentProcess()); 333 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); 334 if (!obs) { 335 return NS_ERROR_FAILURE; 336 } 337 // Notify other observers so they can show settings UI. 338 obs->NotifyObservers(aPrincipal, "notifications-open-settings", nullptr); 339 return NS_OK; 340 } 341 342 nsresult AdjustPushQuota(nsIPrincipal* aPrincipal, 343 NotificationStatusChange aChange) { 344 MOZ_ASSERT(XRE_IsParentProcess()); 345 nsCOMPtr<nsIPushQuotaManager> pushQuotaManager = 346 do_GetService("@mozilla.org/push/Service;1"); 347 if (!pushQuotaManager) { 348 return NS_ERROR_FAILURE; 349 } 350 351 nsAutoCString origin; 352 MOZ_TRY(aPrincipal->GetOrigin(origin)); 353 354 if (aChange == NotificationStatusChange::Shown) { 355 return pushQuotaManager->NotificationForOriginShown(origin.get()); 356 } 357 return pushQuotaManager->NotificationForOriginClosed(origin.get()); 358 } 359 360 NS_IMPL_ISUPPORTS(NotificationActionStorageEntry, 361 nsINotificationActionStorageEntry) 362 363 NS_IMETHODIMP NotificationActionStorageEntry::GetName(nsAString& aName) { 364 aName = mIPCAction.name(); 365 return NS_OK; 366 } 367 368 NS_IMETHODIMP NotificationActionStorageEntry::GetTitle(nsAString& aTitle) { 369 aTitle = mIPCAction.title(); 370 return NS_OK; 371 } 372 373 Result<IPCNotificationAction, nsresult> NotificationActionStorageEntry::ToIPC( 374 nsINotificationActionStorageEntry& aEntry) { 375 IPCNotificationAction action; 376 MOZ_TRY(aEntry.GetName(action.name())); 377 MOZ_TRY(aEntry.GetTitle(action.title())); 378 return action; 379 } 380 381 NS_IMPL_ISUPPORTS(NotificationStorageEntry, nsINotificationStorageEntry) 382 383 NS_IMETHODIMP NotificationStorageEntry::GetId(nsAString& aId) { 384 aId = mIPCNotification.id(); 385 return NS_OK; 386 } 387 388 NS_IMETHODIMP NotificationStorageEntry::GetTitle(nsAString& aTitle) { 389 aTitle = mIPCNotification.options().title(); 390 return NS_OK; 391 } 392 393 NS_IMETHODIMP NotificationStorageEntry::GetDir(nsACString& aDir) { 394 aDir = GetEnumString(mIPCNotification.options().dir()); 395 return NS_OK; 396 } 397 398 NS_IMETHODIMP NotificationStorageEntry::GetLang(nsAString& aLang) { 399 aLang = mIPCNotification.options().lang(); 400 return NS_OK; 401 } 402 403 NS_IMETHODIMP NotificationStorageEntry::GetBody(nsAString& aBody) { 404 aBody = mIPCNotification.options().body(); 405 return NS_OK; 406 } 407 408 NS_IMETHODIMP NotificationStorageEntry::GetTag(nsAString& aTag) { 409 aTag = mIPCNotification.options().tag(); 410 return NS_OK; 411 } 412 413 NS_IMETHODIMP NotificationStorageEntry::GetIcon(nsACString& aIcon) { 414 nsIURI* iconUri = mIPCNotification.options().icon(); 415 if (!iconUri) { 416 aIcon.Truncate(); 417 return NS_OK; 418 } 419 iconUri->GetSpec(aIcon); 420 return NS_OK; 421 } 422 423 NS_IMETHODIMP NotificationStorageEntry::GetRequireInteraction( 424 bool* aRequireInteraction) { 425 *aRequireInteraction = mIPCNotification.options().requireInteraction(); 426 return NS_OK; 427 } 428 429 NS_IMETHODIMP NotificationStorageEntry::GetSilent(bool* aSilent) { 430 *aSilent = mIPCNotification.options().silent(); 431 return NS_OK; 432 } 433 434 NS_IMETHODIMP NotificationStorageEntry::GetDataSerialized( 435 nsAString& aDataSerialized) { 436 aDataSerialized = mIPCNotification.options().dataSerialized(); 437 return NS_OK; 438 } 439 440 NS_IMETHODIMP NotificationStorageEntry::GetActions( 441 nsTArray<RefPtr<nsINotificationActionStorageEntry>>& aRetVal) { 442 nsTArray<RefPtr<nsINotificationActionStorageEntry>> actions( 443 mIPCNotification.options().actions().Length()); 444 445 for (const auto& action : mIPCNotification.options().actions()) { 446 actions.AppendElement(new NotificationActionStorageEntry(action)); 447 } 448 449 aRetVal = std::move(actions); 450 451 return NS_OK; 452 } 453 454 NS_IMETHODIMP NotificationStorageEntry::GetServiceWorkerRegistrationScope( 455 nsAString& aScope) { 456 // Scope is only provided from JS, for now 457 // TODO(krosylight): Change nsINotificationStorage::Put to provide scope via 458 // storage entry? 459 aScope.SetIsVoid(true); 460 return NS_OK; 461 } 462 463 Result<IPCNotification, nsresult> NotificationStorageEntry::ToIPC( 464 nsINotificationStorageEntry& aEntry) { 465 IPCNotification notification; 466 IPCNotificationOptions& options = notification.options(); 467 MOZ_TRY(aEntry.GetId(notification.id())); 468 MOZ_TRY(aEntry.GetTitle(options.title())); 469 470 nsCString dir; 471 MOZ_TRY(aEntry.GetDir(dir)); 472 options.dir() = StringToEnum<NotificationDirection>(dir).valueOr( 473 NotificationDirection::Auto); 474 475 MOZ_TRY(aEntry.GetLang(options.lang())); 476 MOZ_TRY(aEntry.GetBody(options.body())); 477 MOZ_TRY(aEntry.GetTag(options.tag())); 478 479 nsAutoCString iconUrl; 480 MOZ_TRY(aEntry.GetIcon(iconUrl)); 481 if (!iconUrl.IsEmpty()) { 482 MOZ_TRY(NS_NewURI(getter_AddRefs(notification.options().icon()), iconUrl)); 483 } 484 485 MOZ_TRY(aEntry.GetRequireInteraction(&options.requireInteraction())); 486 MOZ_TRY(aEntry.GetSilent(&options.silent())); 487 MOZ_TRY(aEntry.GetDataSerialized(options.dataSerialized())); 488 489 nsTArray<RefPtr<nsINotificationActionStorageEntry>> actionEntries; 490 MOZ_TRY(aEntry.GetActions(actionEntries)); 491 nsTArray<IPCNotificationAction> actions(actionEntries.Length()); 492 for (const auto& actionEntry : actionEntries) { 493 IPCNotificationAction action = 494 MOZ_TRY(NotificationActionStorageEntry::ToIPC(*actionEntry)); 495 actions.AppendElement(std::move(action)); 496 } 497 options.actions() = std::move(actions); 498 499 return notification; 500 } 501 502 } // namespace mozilla::dom::notification