tor-browser

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

NotificationParent.cpp (12311B)


      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 "NotificationParent.h"
      8 
      9 #include "NotificationHandler.h"
     10 #include "NotificationUtils.h"
     11 #include "mozilla/AlertNotification.h"
     12 #include "mozilla/StaticPrefs_dom.h"
     13 #include "mozilla/dom/ServiceWorkerManager.h"
     14 #include "mozilla/ipc/Endpoint.h"
     15 #include "nsComponentManagerUtils.h"
     16 #include "nsIServiceWorkerManager.h"
     17 #include "nsThreadUtils.h"
     18 
     19 namespace mozilla::dom::notification {
     20 
     21 NS_IMPL_ISUPPORTS0(NotificationParent)
     22 
     23 // TODO(krosylight): Would be nice to replace nsIObserver with something like:
     24 //
     25 // nsINotificationManager.NotifyClick(notification.id [, notification.action])
     26 class NotificationObserver final : public nsIObserver {
     27 public:
     28  NS_DECL_ISUPPORTS
     29 
     30  NotificationObserver(const nsAString& aScope, nsIPrincipal* aPrincipal,
     31                       IPCNotification aNotification,
     32                       NotificationParent& aParent)
     33      : mScope(aScope),
     34        mPrincipal(aPrincipal),
     35        mNotification(std::move(aNotification)),
     36        mActor(&aParent) {}
     37 
     38  NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic,
     39                        const char16_t* aData) override {
     40    AlertTopic topic = ToAlertTopic(aTopic, aData);
     41 
     42    // These two never fire any content event directly
     43    if (topic == AlertTopic::Disable) {
     44      return RemovePermission(mPrincipal);
     45    }
     46    if (topic == AlertTopic::Settings) {
     47      return OpenSettings(mPrincipal);
     48    }
     49 
     50    RefPtr<NotificationParent> actor(mActor);
     51 
     52    if (actor && actor->CanSend()) {
     53      // The actor is alive, call it to ping the content process and/or to make
     54      // it clean up itself
     55      actor->HandleAlertTopic(topic);
     56      if (mScope.IsEmpty()) {
     57        // The actor covered everything we need.
     58        return NS_OK;
     59      }
     60    } else if (mScope.IsEmpty()) {
     61      if (topic == AlertTopic::Click) {
     62        // No actor there, we need to open up a window ourselves
     63        return OpenWindowFor(mPrincipal);
     64      }
     65      // Nothing to do
     66      return NS_OK;
     67    }
     68 
     69    // We have a Service Worker to call
     70    MOZ_ASSERT(!mScope.IsEmpty());
     71    if (topic == AlertTopic::Show) {
     72      (void)NS_WARN_IF(NS_FAILED(
     73          AdjustPushQuota(mPrincipal, NotificationStatusChange::Shown)));
     74      nsresult rv = PersistNotification(mPrincipal, mNotification, mScope);
     75      if (NS_FAILED(rv)) {
     76        NS_WARNING("Could not persist Notification");
     77      }
     78      return NS_OK;
     79    }
     80 
     81    MOZ_ASSERT(topic == AlertTopic::Click || topic == AlertTopic::Finished ||
     82               topic == AlertTopic::Closed);
     83 
     84    if (topic == AlertTopic::Click) {
     85      nsCOMPtr<nsIAlertAction> action = do_QueryInterface(aSubject);
     86      nsAutoString actionName;
     87      if (action) {
     88        MOZ_TRY(action->GetAction(actionName));
     89      }
     90      return RespondOnClick(mPrincipal, mScope, mNotification, actionName);
     91    }
     92 
     93    RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
     94    if (!swm) {
     95      return NS_ERROR_FAILURE;
     96    }
     97 
     98    nsAutoCString originSuffix;
     99    MOZ_TRY(mPrincipal->GetOriginSuffix(originSuffix));
    100 
    101    MOZ_ASSERT(topic == AlertTopic::Finished || topic == AlertTopic::Closed);
    102    (void)NS_WARN_IF(NS_FAILED(
    103        AdjustPushQuota(mPrincipal, NotificationStatusChange::Closed)));
    104    (void)NS_WARN_IF(
    105        NS_FAILED(UnpersistNotification(mPrincipal, mNotification.id())));
    106    (void)swm->SendNotificationCloseEvent(originSuffix, mScope, mNotification);
    107 
    108    return NS_OK;
    109  }
    110 
    111 private:
    112  virtual ~NotificationObserver() = default;
    113 
    114  static AlertTopic ToAlertTopic(const char* aTopic, const char16_t* aData) {
    115    if (!strcmp("alertdisablecallback", aTopic)) {
    116      return AlertTopic::Disable;
    117    }
    118    if (!strcmp("alertsettingscallback", aTopic)) {
    119      return AlertTopic::Settings;
    120    }
    121    if (!strcmp("alertclickcallback", aTopic)) {
    122      return AlertTopic::Click;
    123    }
    124    if (!strcmp("alertshow", aTopic)) {
    125      return AlertTopic::Show;
    126    }
    127    if (!strcmp("alertfinished", aTopic)) {
    128      if (aData && nsDependentString(aData) == u"close"_ns) {
    129        // Backends with asynchronous system API may hint that they are
    130        // intentionally closing the notification, to disambiguate from an early
    131        // alertfinished which is recognized as an error.
    132        // (Not introducing alertclose for compatibility with existing browser
    133        // script callers.)
    134        return AlertTopic::Closed;
    135      }
    136      return AlertTopic::Finished;
    137    }
    138    MOZ_ASSERT_UNREACHABLE("Unknown alert topic");
    139    return AlertTopic::Finished;
    140  }
    141 
    142  // May want to replace with SWR ID, see bug 1881812
    143  nsString mScope;
    144  nsCOMPtr<nsIPrincipal> mPrincipal;
    145  IPCNotification mNotification;
    146  WeakPtr<NotificationParent> mActor;
    147 };
    148 
    149 NS_IMPL_ISUPPORTS(NotificationObserver, nsIObserver)
    150 
    151 nsresult NotificationParent::HandleAlertTopic(AlertTopic aTopic) {
    152  if (aTopic == AlertTopic::Click) {
    153    return FireClickEvent();
    154  }
    155  if (aTopic == AlertTopic::Show) {
    156    if (!mResolver) {
    157 #ifdef ANDROID
    158      // XXX: This can happen as alertshow happens asynchronously on Android as
    159      // we go through GeckoView.
    160      //
    161      // For example, if two same-tagged notifications are requested at the same
    162      // time, the first one will be canceled but can still fire alertshow,
    163      // while the second one will also fire one, and the handler for the second
    164      // one would get both.
    165      //
    166      // We may want to reintroduce UUID for such asynchronous case, but for now
    167      // it's very edge case and can be ignored.
    168      return NS_OK;
    169 #else
    170      MOZ_ASSERT_UNREACHABLE("Are we getting double show events?");
    171      return NS_ERROR_FAILURE;
    172 #endif
    173    }
    174    mResolver.take().value()(CopyableErrorResult());
    175    return NS_OK;
    176  }
    177  if (mResolver) {
    178    if (aTopic == AlertTopic::Closed) {
    179      // Closing without ever being shown, but intentionally by the backend
    180      mResolver.take().value()(CopyableErrorResult());
    181    } else if (aTopic == AlertTopic::Finished) {
    182      // alertshow happens first before alertfinished, and it should have
    183      // nullified mResolver. If not it means it failed to show and is bailing
    184      // out.
    185      // NOTE(krosylight): The spec does not define what to do when a
    186      // permission-granted notification fails to open, we throw TypeError
    187      // here as that's the error for when permission is denied.
    188      CopyableErrorResult rv;
    189      rv.ThrowTypeError(
    190          "Failed to show notification, potentially because the browser did "
    191          "not have the corresponding OS-level permission."_ns);
    192      mResolver.take().value()(rv);
    193    }
    194  }
    195 
    196  if (aTopic == AlertTopic::Finished || aTopic == AlertTopic::Closed) {
    197    // Unpersisted already and being unregistered already by nsIAlertsService
    198    mDangling = true;
    199    Close();
    200 
    201    return NS_OK;
    202  }
    203 
    204  MOZ_ASSERT_UNREACHABLE("Unknown notification topic");
    205 
    206  return NS_OK;
    207 }
    208 
    209 nsresult NotificationParent::FireClickEvent() {
    210  if (!mArgs.mScope.IsEmpty()) {
    211    return NS_OK;
    212  }
    213  if (SendNotifyClick()) {
    214    return NS_OK;
    215  }
    216  return NS_ERROR_FAILURE;
    217 }
    218 
    219 // Step 4 of
    220 // https://notifications.spec.whatwg.org/#dom-notification-notification
    221 mozilla::ipc::IPCResult NotificationParent::RecvShow(ShowResolver&& aResolver) {
    222  MOZ_ASSERT(mId.IsEmpty(), "ID should not be given for a new notification");
    223 
    224  mResolver.emplace(std::move(aResolver));
    225 
    226  // Step 4.1: If the result of getting the notifications permission state is
    227  // not "granted", then queue a task to fire an event named error on this, and
    228  // abort these steps.
    229  NotificationPermission permission = GetNotificationPermission(
    230      mArgs.mPrincipal, mArgs.mEffectiveStoragePrincipal,
    231      mArgs.mIsSecureContext, PermissionCheckPurpose::NotificationShow);
    232  if (permission != NotificationPermission::Granted) {
    233    CopyableErrorResult rv;
    234    rv.ThrowTypeError("Permission to show Notification denied.");
    235    mResolver.take().value()(rv);
    236    mDangling = true;
    237    return IPC_OK();
    238  }
    239 
    240  // Step 4.2: Run the fetch steps for notification. (Will happen in
    241  // nsIAlertNotification::LoadImage)
    242  // Step 4.3: Run the show steps for notification.
    243  nsresult rv = Show();
    244  // It's possible that we synchronously received a notification while in Show,
    245  // so mResolver may now be empty.
    246  if (NS_FAILED(rv) && mResolver) {
    247    mResolver.take().value()(CopyableErrorResult(rv));
    248  }
    249  // If not failed, the resolver will be called asynchronously by
    250  // NotificationObserver
    251  return IPC_OK();
    252 }
    253 
    254 nsresult NotificationParent::Show() {
    255  // Step 4.3 the show steps, which are almost all about processing `tag` and
    256  // then displaying the notification. Both are handled by
    257  // nsIAlertsService::ShowAlert. The below is all about constructing the
    258  // observer (for show and close events) right and ultimately call the alerts
    259  // service function.
    260 
    261  // In the case of IPC, the parent process uses the cookie to map to
    262  // nsIObserver. Thus the cookie must be unique to differentiate observers.
    263  // XXX(krosylight): This is about ContentChild::mAlertObserver which is not
    264  // useful when called by the parent process. This should be removed when we
    265  // make nsIAlertsService parent process only.
    266  nsString obsoleteCookie = u"notification:"_ns;
    267 
    268  const IPCNotificationOptions& options = mArgs.mNotification.options();
    269 
    270  bool requireInteraction = options.requireInteraction();
    271  if (!StaticPrefs::dom_webnotifications_requireinteraction_enabled()) {
    272    requireInteraction = false;
    273  }
    274 
    275  nsCOMPtr<nsIAlertNotification> alert =
    276      do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
    277  if (!alert) {
    278    return NS_ERROR_NOT_AVAILABLE;
    279  }
    280 
    281  nsCOMPtr<nsIPrincipal> principal = mArgs.mPrincipal;
    282  nsAutoCString iconUrl;
    283  if (RefPtr<nsIURI> iconUri = options.icon()) {
    284    iconUri->GetSpec(iconUrl);
    285  }
    286  MOZ_TRY(alert->Init(options.tag(), NS_ConvertUTF8toUTF16(iconUrl),
    287                      options.title(), options.body(), true, obsoleteCookie,
    288                      NS_ConvertASCIItoUTF16(GetEnumString(options.dir())),
    289                      options.lang(), options.dataSerialized(), principal,
    290                      principal->GetIsInPrivateBrowsing(), requireInteraction,
    291                      options.silent(), options.vibrate()));
    292 
    293  nsTArray<RefPtr<nsIAlertAction>> actions;
    294  MOZ_ASSERT(options.actions().Length() <= kMaxActions);
    295  for (const auto& action : options.actions()) {
    296    actions.AppendElement(new AlertAction(action.name(), action.title()));
    297  }
    298 
    299  alert->SetActions(actions);
    300 
    301  MOZ_TRY(alert->GetId(mId));
    302 
    303  RefPtr<NotificationObserver> observer = new NotificationObserver(
    304      mArgs.mScope, principal, IPCNotification(mId, options), *this);
    305  MOZ_TRY(ShowAlertWithCleanup(alert, observer));
    306 
    307  return NS_OK;
    308 }
    309 
    310 mozilla::ipc::IPCResult NotificationParent::RecvClose() {
    311  Unregister();
    312  Close();
    313  return IPC_OK();
    314 }
    315 
    316 void NotificationParent::Unregister() {
    317  if (mDangling) {
    318    // We had no permission, so nothing to clean up.
    319    return;
    320  }
    321 
    322  mDangling = true;
    323  UnregisterNotification(mArgs.mPrincipal, mId);
    324 }
    325 
    326 nsresult NotificationParent::CreateOnMainThread(
    327    NotificationParentArgs&& mArgs,
    328    Endpoint<PNotificationParent>&& aParentEndpoint,
    329    PBackgroundParent::CreateNotificationParentResolver&& aResolver) {
    330  if (mArgs.mNotification.options().actions().Length() > kMaxActions) {
    331    return NS_ERROR_INVALID_ARG;
    332  }
    333 
    334  nsCOMPtr<nsIThread> thread = NS_GetCurrentThread();
    335 
    336  NS_DispatchToMainThread(NS_NewRunnableFunction(
    337      "NotificationParent::BindToMainThread",
    338      [args = std::move(mArgs), endpoint = std::move(aParentEndpoint),
    339       resolver = std::move(aResolver), thread]() mutable {
    340        RefPtr<NotificationParent> actor =
    341            new NotificationParent(std::move(args));
    342        bool result = endpoint.Bind(actor);
    343        thread->Dispatch(NS_NewRunnableFunction(
    344            "NotificationParent::BindToMainThreadResult",
    345            [result, resolver = std::move(resolver)]() { resolver(result); }));
    346      }));
    347 
    348  return NS_OK;
    349 }
    350 
    351 }  // namespace mozilla::dom::notification