Bug 1880216 - Migrate Fenix docs into Sphinx. r=owlish,geckoview-reviewers,android...
[gecko.git] / dom / push / PushManager.cpp
blob74bb4a3d7cd4e1935dc0b6ca708a00cf7f056393
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/. */
7 #include "mozilla/dom/PushManager.h"
9 #include "mozilla/Base64.h"
10 #include "mozilla/Preferences.h"
11 #include "mozilla/Components.h"
12 #include "mozilla/Unused.h"
13 #include "mozilla/dom/PermissionStatusBinding.h"
14 #include "mozilla/dom/PushManagerBinding.h"
15 #include "mozilla/dom/PushSubscription.h"
16 #include "mozilla/dom/PushSubscriptionOptionsBinding.h"
17 #include "mozilla/dom/PushUtil.h"
18 #include "mozilla/dom/RootedDictionary.h"
19 #include "mozilla/dom/ServiceWorker.h"
20 #include "mozilla/dom/WorkerRunnable.h"
21 #include "mozilla/dom/WorkerScope.h"
23 #include "mozilla/dom/Promise.h"
24 #include "mozilla/dom/PromiseWorkerProxy.h"
26 #include "nsIGlobalObject.h"
27 #include "nsIPermissionManager.h"
28 #include "nsIPrincipal.h"
29 #include "nsIPushService.h"
31 #include "nsComponentManagerUtils.h"
32 #include "nsContentUtils.h"
33 #include "nsServiceManagerUtils.h"
35 namespace mozilla::dom {
37 namespace {
39 nsresult GetPermissionState(nsIPrincipal* aPrincipal, PermissionState& aState) {
40 nsCOMPtr<nsIPermissionManager> permManager =
41 mozilla::components::PermissionManager::Service();
43 if (!permManager) {
44 return NS_ERROR_FAILURE;
46 uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
47 nsresult rv = permManager->TestExactPermissionFromPrincipal(
48 aPrincipal, "desktop-notification"_ns, &permission);
49 if (NS_WARN_IF(NS_FAILED(rv))) {
50 return rv;
53 if (permission == nsIPermissionManager::ALLOW_ACTION ||
54 Preferences::GetBool("dom.push.testing.ignorePermission", false)) {
55 aState = PermissionState::Granted;
56 } else if (permission == nsIPermissionManager::DENY_ACTION) {
57 aState = PermissionState::Denied;
58 } else {
59 aState = PermissionState::Prompt;
62 return NS_OK;
65 nsresult GetSubscriptionParams(nsIPushSubscription* aSubscription,
66 nsAString& aEndpoint,
67 nsTArray<uint8_t>& aRawP256dhKey,
68 nsTArray<uint8_t>& aAuthSecret,
69 nsTArray<uint8_t>& aAppServerKey) {
70 if (!aSubscription) {
71 return NS_OK;
74 nsresult rv = aSubscription->GetEndpoint(aEndpoint);
75 if (NS_WARN_IF(NS_FAILED(rv))) {
76 return rv;
79 rv = aSubscription->GetKey(u"p256dh"_ns, aRawP256dhKey);
80 if (NS_WARN_IF(NS_FAILED(rv))) {
81 return rv;
83 rv = aSubscription->GetKey(u"auth"_ns, aAuthSecret);
84 if (NS_WARN_IF(NS_FAILED(rv))) {
85 return rv;
87 rv = aSubscription->GetKey(u"appServer"_ns, aAppServerKey);
88 if (NS_WARN_IF(NS_FAILED(rv))) {
89 return rv;
92 return NS_OK;
95 class GetSubscriptionResultRunnable final : public WorkerRunnable {
96 public:
97 GetSubscriptionResultRunnable(WorkerPrivate* aWorkerPrivate,
98 RefPtr<PromiseWorkerProxy>&& aProxy,
99 nsresult aStatus, const nsAString& aEndpoint,
100 const nsAString& aScope,
101 Nullable<EpochTimeStamp>&& aExpirationTime,
102 nsTArray<uint8_t>&& aRawP256dhKey,
103 nsTArray<uint8_t>&& aAuthSecret,
104 nsTArray<uint8_t>&& aAppServerKey)
105 : WorkerRunnable(aWorkerPrivate, "GetSubscriptionResultRunnable"),
106 mProxy(std::move(aProxy)),
107 mStatus(aStatus),
108 mEndpoint(aEndpoint),
109 mScope(aScope),
110 mExpirationTime(std::move(aExpirationTime)),
111 mRawP256dhKey(std::move(aRawP256dhKey)),
112 mAuthSecret(std::move(aAuthSecret)),
113 mAppServerKey(std::move(aAppServerKey)) {}
115 bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
116 RefPtr<Promise> promise = mProxy->GetWorkerPromise();
117 // Once Worker had already started shutdown, workerPromise would be nullptr
118 if (!promise) {
119 return true;
121 if (NS_SUCCEEDED(mStatus)) {
122 if (mEndpoint.IsEmpty()) {
123 promise->MaybeResolve(JS::NullHandleValue);
124 } else {
125 RefPtr<PushSubscription> sub = new PushSubscription(
126 nullptr, mEndpoint, mScope, std::move(mExpirationTime),
127 std::move(mRawP256dhKey), std::move(mAuthSecret),
128 std::move(mAppServerKey));
129 promise->MaybeResolve(sub);
131 } else if (NS_ERROR_GET_MODULE(mStatus) == NS_ERROR_MODULE_DOM_PUSH) {
132 promise->MaybeReject(mStatus);
133 } else {
134 promise->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR);
137 mProxy->CleanUp();
139 return true;
142 private:
143 ~GetSubscriptionResultRunnable() = default;
145 RefPtr<PromiseWorkerProxy> mProxy;
146 nsresult mStatus;
147 nsString mEndpoint;
148 nsString mScope;
149 Nullable<EpochTimeStamp> mExpirationTime;
150 nsTArray<uint8_t> mRawP256dhKey;
151 nsTArray<uint8_t> mAuthSecret;
152 nsTArray<uint8_t> mAppServerKey;
155 class GetSubscriptionCallback final : public nsIPushSubscriptionCallback {
156 public:
157 NS_DECL_ISUPPORTS
159 explicit GetSubscriptionCallback(PromiseWorkerProxy* aProxy,
160 const nsAString& aScope)
161 : mProxy(aProxy), mScope(aScope) {}
163 NS_IMETHOD
164 OnPushSubscription(nsresult aStatus,
165 nsIPushSubscription* aSubscription) override {
166 AssertIsOnMainThread();
167 MOZ_ASSERT(mProxy, "OnPushSubscription() called twice?");
169 MutexAutoLock lock(mProxy->Lock());
170 if (mProxy->CleanedUp()) {
171 return NS_OK;
174 nsAutoString endpoint;
175 nsTArray<uint8_t> rawP256dhKey, authSecret, appServerKey;
176 if (NS_SUCCEEDED(aStatus)) {
177 aStatus = GetSubscriptionParams(aSubscription, endpoint, rawP256dhKey,
178 authSecret, appServerKey);
181 WorkerPrivate* worker = mProxy->GetWorkerPrivate();
182 RefPtr<GetSubscriptionResultRunnable> r = new GetSubscriptionResultRunnable(
183 worker, std::move(mProxy), aStatus, endpoint, mScope,
184 std::move(mExpirationTime), std::move(rawP256dhKey),
185 std::move(authSecret), std::move(appServerKey));
186 if (!r->Dispatch()) {
187 return NS_ERROR_UNEXPECTED;
190 return NS_OK;
193 // Convenience method for use in this file.
194 void OnPushSubscriptionError(nsresult aStatus) {
195 Unused << NS_WARN_IF(NS_FAILED(OnPushSubscription(aStatus, nullptr)));
198 protected:
199 ~GetSubscriptionCallback() = default;
201 private:
202 RefPtr<PromiseWorkerProxy> mProxy;
203 nsString mScope;
204 Nullable<EpochTimeStamp> mExpirationTime;
207 NS_IMPL_ISUPPORTS(GetSubscriptionCallback, nsIPushSubscriptionCallback)
209 class GetSubscriptionRunnable final : public Runnable {
210 public:
211 GetSubscriptionRunnable(PromiseWorkerProxy* aProxy, const nsAString& aScope,
212 PushManager::SubscriptionAction aAction,
213 nsTArray<uint8_t>&& aAppServerKey)
214 : Runnable("dom::GetSubscriptionRunnable"),
215 mProxy(aProxy),
216 mScope(aScope),
217 mAction(aAction),
218 mAppServerKey(std::move(aAppServerKey)) {}
220 NS_IMETHOD
221 Run() override {
222 AssertIsOnMainThread();
224 nsCOMPtr<nsIPrincipal> principal;
227 // Bug 1228723: If permission is revoked or an error occurs, the
228 // subscription callback will be called synchronously. This causes
229 // `GetSubscriptionCallback::OnPushSubscription` to deadlock when
230 // it tries to acquire the lock.
231 MutexAutoLock lock(mProxy->Lock());
232 if (mProxy->CleanedUp()) {
233 return NS_OK;
235 principal = mProxy->GetWorkerPrivate()->GetPrincipal();
238 MOZ_ASSERT(principal);
240 RefPtr<GetSubscriptionCallback> callback =
241 new GetSubscriptionCallback(mProxy, mScope);
243 PermissionState state;
244 nsresult rv = GetPermissionState(principal, state);
245 if (NS_FAILED(rv)) {
246 callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
247 return NS_OK;
250 if (state != PermissionState::Granted) {
251 if (mAction == PushManager::GetSubscriptionAction) {
252 callback->OnPushSubscriptionError(NS_OK);
253 return NS_OK;
255 callback->OnPushSubscriptionError(NS_ERROR_DOM_PUSH_DENIED_ERR);
256 return NS_OK;
259 nsCOMPtr<nsIPushService> service =
260 do_GetService("@mozilla.org/push/Service;1");
261 if (NS_WARN_IF(!service)) {
262 callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
263 return NS_OK;
266 if (mAction == PushManager::SubscribeAction) {
267 if (mAppServerKey.IsEmpty()) {
268 rv = service->Subscribe(mScope, principal, callback);
269 } else {
270 rv = service->SubscribeWithKey(mScope, principal, mAppServerKey,
271 callback);
273 } else {
274 MOZ_ASSERT(mAction == PushManager::GetSubscriptionAction);
275 rv = service->GetSubscription(mScope, principal, callback);
278 if (NS_WARN_IF(NS_FAILED(rv))) {
279 callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
280 return NS_OK;
283 return NS_OK;
286 private:
287 ~GetSubscriptionRunnable() = default;
289 RefPtr<PromiseWorkerProxy> mProxy;
290 nsString mScope;
291 PushManager::SubscriptionAction mAction;
292 nsTArray<uint8_t> mAppServerKey;
295 class PermissionResultRunnable final : public WorkerRunnable {
296 public:
297 PermissionResultRunnable(PromiseWorkerProxy* aProxy, nsresult aStatus,
298 PermissionState aState)
299 : WorkerRunnable(aProxy->GetWorkerPrivate(), "PermissionResultRunnable"),
300 mProxy(aProxy),
301 mStatus(aStatus),
302 mState(aState) {
303 AssertIsOnMainThread();
306 bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
307 MOZ_ASSERT(aWorkerPrivate);
308 aWorkerPrivate->AssertIsOnWorkerThread();
309 RefPtr<Promise> promise = mProxy->GetWorkerPromise();
310 if (!promise) {
311 return true;
313 if (NS_SUCCEEDED(mStatus)) {
314 promise->MaybeResolve(mState);
315 } else {
316 promise->MaybeRejectWithUndefined();
319 mProxy->CleanUp();
321 return true;
324 private:
325 ~PermissionResultRunnable() = default;
327 RefPtr<PromiseWorkerProxy> mProxy;
328 nsresult mStatus;
329 PermissionState mState;
332 class PermissionStateRunnable final : public Runnable {
333 public:
334 explicit PermissionStateRunnable(PromiseWorkerProxy* aProxy)
335 : Runnable("dom::PermissionStateRunnable"), mProxy(aProxy) {}
337 NS_IMETHOD
338 Run() override {
339 AssertIsOnMainThread();
340 MutexAutoLock lock(mProxy->Lock());
341 if (mProxy->CleanedUp()) {
342 return NS_OK;
345 PermissionState state;
346 nsresult rv =
347 GetPermissionState(mProxy->GetWorkerPrivate()->GetPrincipal(), state);
349 RefPtr<PermissionResultRunnable> r =
350 new PermissionResultRunnable(mProxy, rv, state);
352 // This can fail if the worker thread is already shutting down, but there's
353 // nothing we can do in that case.
354 Unused << NS_WARN_IF(!r->Dispatch());
356 return NS_OK;
359 private:
360 ~PermissionStateRunnable() = default;
362 RefPtr<PromiseWorkerProxy> mProxy;
365 } // anonymous namespace
367 PushManager::PushManager(nsIGlobalObject* aGlobal, PushManagerImpl* aImpl)
368 : mGlobal(aGlobal), mImpl(aImpl) {
369 AssertIsOnMainThread();
370 MOZ_ASSERT(aImpl);
373 PushManager::PushManager(const nsAString& aScope) : mScope(aScope) {
374 #ifdef DEBUG
375 // There's only one global on a worker, so we don't need to pass a global
376 // object to the constructor.
377 WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
378 MOZ_ASSERT(worker);
379 worker->AssertIsOnWorkerThread();
380 #endif
383 PushManager::~PushManager() = default;
385 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushManager, mGlobal, mImpl)
386 NS_IMPL_CYCLE_COLLECTING_ADDREF(PushManager)
387 NS_IMPL_CYCLE_COLLECTING_RELEASE(PushManager)
388 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushManager)
389 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
390 NS_INTERFACE_MAP_ENTRY(nsISupports)
391 NS_INTERFACE_MAP_END
393 JSObject* PushManager::WrapObject(JSContext* aCx,
394 JS::Handle<JSObject*> aGivenProto) {
395 return PushManager_Binding::Wrap(aCx, this, aGivenProto);
398 // static
399 already_AddRefed<PushManager> PushManager::Constructor(GlobalObject& aGlobal,
400 const nsAString& aScope,
401 ErrorResult& aRv) {
402 if (!NS_IsMainThread()) {
403 RefPtr<PushManager> ret = new PushManager(aScope);
404 return ret.forget();
407 RefPtr<PushManagerImpl> impl =
408 PushManagerImpl::Constructor(aGlobal, aGlobal.Context(), aScope, aRv);
409 if (aRv.Failed()) {
410 return nullptr;
413 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
414 RefPtr<PushManager> ret = new PushManager(global, impl);
416 return ret.forget();
419 bool PushManager::IsEnabled(JSContext* aCx, JSObject* aGlobal) {
420 return StaticPrefs::dom_push_enabled() && ServiceWorkerVisible(aCx, aGlobal);
423 already_AddRefed<Promise> PushManager::Subscribe(
424 const PushSubscriptionOptionsInit& aOptions, ErrorResult& aRv) {
425 if (mImpl) {
426 MOZ_ASSERT(NS_IsMainThread());
427 return mImpl->Subscribe(aOptions, aRv);
430 return PerformSubscriptionActionFromWorker(SubscribeAction, aOptions, aRv);
433 already_AddRefed<Promise> PushManager::GetSubscription(ErrorResult& aRv) {
434 if (mImpl) {
435 MOZ_ASSERT(NS_IsMainThread());
436 return mImpl->GetSubscription(aRv);
439 return PerformSubscriptionActionFromWorker(GetSubscriptionAction, aRv);
442 already_AddRefed<Promise> PushManager::PermissionState(
443 const PushSubscriptionOptionsInit& aOptions, ErrorResult& aRv) {
444 if (mImpl) {
445 MOZ_ASSERT(NS_IsMainThread());
446 return mImpl->PermissionState(aOptions, aRv);
449 WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
450 MOZ_ASSERT(worker);
451 worker->AssertIsOnWorkerThread();
453 nsCOMPtr<nsIGlobalObject> global = worker->GlobalScope();
454 RefPtr<Promise> p = Promise::Create(global, aRv);
455 if (NS_WARN_IF(aRv.Failed())) {
456 return nullptr;
459 RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p);
460 if (!proxy) {
461 p->MaybeRejectWithUndefined();
462 return p.forget();
465 RefPtr<PermissionStateRunnable> r = new PermissionStateRunnable(proxy);
466 NS_DispatchToMainThread(r);
468 return p.forget();
471 already_AddRefed<Promise> PushManager::PerformSubscriptionActionFromWorker(
472 SubscriptionAction aAction, ErrorResult& aRv) {
473 RootedDictionary<PushSubscriptionOptionsInit> options(RootingCx());
474 return PerformSubscriptionActionFromWorker(aAction, options, aRv);
477 already_AddRefed<Promise> PushManager::PerformSubscriptionActionFromWorker(
478 SubscriptionAction aAction, const PushSubscriptionOptionsInit& aOptions,
479 ErrorResult& aRv) {
480 WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
481 MOZ_ASSERT(worker);
482 worker->AssertIsOnWorkerThread();
484 nsCOMPtr<nsIGlobalObject> global = worker->GlobalScope();
485 RefPtr<Promise> p = Promise::Create(global, aRv);
486 if (NS_WARN_IF(aRv.Failed())) {
487 return nullptr;
490 RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p);
491 if (!proxy) {
492 p->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR);
493 return p.forget();
496 nsTArray<uint8_t> appServerKey;
497 if (!aOptions.mApplicationServerKey.IsNull()) {
498 nsresult rv = NormalizeAppServerKey(aOptions.mApplicationServerKey.Value(),
499 appServerKey);
500 if (NS_FAILED(rv)) {
501 p->MaybeReject(rv);
502 return p.forget();
506 RefPtr<GetSubscriptionRunnable> r = new GetSubscriptionRunnable(
507 proxy, mScope, aAction, std::move(appServerKey));
508 MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r));
510 return p.forget();
513 nsresult PushManager::NormalizeAppServerKey(
514 const OwningArrayBufferViewOrArrayBufferOrString& aSource,
515 nsTArray<uint8_t>& aAppServerKey) {
516 if (aSource.IsString()) {
517 NS_ConvertUTF16toUTF8 base64Key(aSource.GetAsString());
518 FallibleTArray<uint8_t> decodedKey;
519 nsresult rv = Base64URLDecode(
520 base64Key, Base64URLDecodePaddingPolicy::Reject, decodedKey);
521 if (NS_FAILED(rv)) {
522 return NS_ERROR_DOM_INVALID_CHARACTER_ERR;
524 aAppServerKey = decodedKey;
525 } else {
526 if (!AppendTypedArrayDataTo(aSource, aAppServerKey)) {
527 return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR;
530 if (aAppServerKey.IsEmpty()) {
531 return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR;
533 return NS_OK;
536 } // namespace mozilla::dom