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