tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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