/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/dom/PushManager.h"

#include "mozilla/Base64.h"
#include "mozilla/Preferences.h"
#include "mozilla/Components.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/PermissionStatusBinding.h"
#include "mozilla/dom/PushManagerBinding.h"
#include "mozilla/dom/PushSubscription.h"
#include "mozilla/dom/PushSubscriptionOptionsBinding.h"
#include "mozilla/dom/PushUtil.h"
#include "mozilla/dom/RootedDictionary.h"
#include "mozilla/dom/ServiceWorker.h"
#include "mozilla/dom/WorkerRunnable.h"
#include "mozilla/dom/WorkerScope.h"

#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseWorkerProxy.h"

#include "nsIGlobalObject.h"
#include "nsIPermissionManager.h"
#include "nsIPrincipal.h"
#include "nsIPushService.h"

#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsServiceManagerUtils.h"

namespace mozilla::dom {

namespace {

nsresult GetPermissionState(nsIPrincipal* aPrincipal, PermissionState& aState) {
  nsCOMPtr<nsIPermissionManager> permManager =
      mozilla::components::PermissionManager::Service();

  if (!permManager) {
    return NS_ERROR_FAILURE;
  }
  uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
  nsresult rv = permManager->TestExactPermissionFromPrincipal(
      aPrincipal, "desktop-notification"_ns, &permission);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (permission == nsIPermissionManager::ALLOW_ACTION ||
      Preferences::GetBool("dom.push.testing.ignorePermission", false)) {
    aState = PermissionState::Granted;
  } else if (permission == nsIPermissionManager::DENY_ACTION) {
    aState = PermissionState::Denied;
  } else {
    aState = PermissionState::Prompt;
  }

  return NS_OK;
}

nsresult GetSubscriptionParams(nsIPushSubscription* aSubscription,
                               nsAString& aEndpoint,
                               nsTArray<uint8_t>& aRawP256dhKey,
                               nsTArray<uint8_t>& aAuthSecret,
                               nsTArray<uint8_t>& aAppServerKey) {
  if (!aSubscription) {
    return NS_OK;
  }

  nsresult rv = aSubscription->GetEndpoint(aEndpoint);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aSubscription->GetKey(u"p256dh"_ns, aRawP256dhKey);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  rv = aSubscription->GetKey(u"auth"_ns, aAuthSecret);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  rv = aSubscription->GetKey(u"appServer"_ns, aAppServerKey);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

class GetSubscriptionResultRunnable final : public WorkerRunnable {
 public:
  GetSubscriptionResultRunnable(WorkerPrivate* aWorkerPrivate,
                                RefPtr<PromiseWorkerProxy>&& aProxy,
                                nsresult aStatus, const nsAString& aEndpoint,
                                const nsAString& aScope,
                                Nullable<EpochTimeStamp>&& aExpirationTime,
                                nsTArray<uint8_t>&& aRawP256dhKey,
                                nsTArray<uint8_t>&& aAuthSecret,
                                nsTArray<uint8_t>&& aAppServerKey)
      : WorkerRunnable(aWorkerPrivate),
        mProxy(std::move(aProxy)),
        mStatus(aStatus),
        mEndpoint(aEndpoint),
        mScope(aScope),
        mExpirationTime(std::move(aExpirationTime)),
        mRawP256dhKey(std::move(aRawP256dhKey)),
        mAuthSecret(std::move(aAuthSecret)),
        mAppServerKey(std::move(aAppServerKey)) {}

  bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
    RefPtr<Promise> promise = mProxy->WorkerPromise();
    if (NS_SUCCEEDED(mStatus)) {
      if (mEndpoint.IsEmpty()) {
        promise->MaybeResolve(JS::NullHandleValue);
      } else {
        RefPtr<PushSubscription> sub = new PushSubscription(
            nullptr, mEndpoint, mScope, std::move(mExpirationTime),
            std::move(mRawP256dhKey), std::move(mAuthSecret),
            std::move(mAppServerKey));
        promise->MaybeResolve(sub);
      }
    } else if (NS_ERROR_GET_MODULE(mStatus) == NS_ERROR_MODULE_DOM_PUSH) {
      promise->MaybeReject(mStatus);
    } else {
      promise->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR);
    }

    mProxy->CleanUp();

    return true;
  }

 private:
  ~GetSubscriptionResultRunnable() = default;

  RefPtr<PromiseWorkerProxy> mProxy;
  nsresult mStatus;
  nsString mEndpoint;
  nsString mScope;
  Nullable<EpochTimeStamp> mExpirationTime;
  nsTArray<uint8_t> mRawP256dhKey;
  nsTArray<uint8_t> mAuthSecret;
  nsTArray<uint8_t> mAppServerKey;
};

class GetSubscriptionCallback final : public nsIPushSubscriptionCallback {
 public:
  NS_DECL_ISUPPORTS

  explicit GetSubscriptionCallback(PromiseWorkerProxy* aProxy,
                                   const nsAString& aScope)
      : mProxy(aProxy), mScope(aScope) {}

  NS_IMETHOD
  OnPushSubscription(nsresult aStatus,
                     nsIPushSubscription* aSubscription) override {
    AssertIsOnMainThread();
    MOZ_ASSERT(mProxy, "OnPushSubscription() called twice?");

    MutexAutoLock lock(mProxy->Lock());
    if (mProxy->CleanedUp()) {
      return NS_OK;
    }

    nsAutoString endpoint;
    nsTArray<uint8_t> rawP256dhKey, authSecret, appServerKey;
    if (NS_SUCCEEDED(aStatus)) {
      aStatus = GetSubscriptionParams(aSubscription, endpoint, rawP256dhKey,
                                      authSecret, appServerKey);
    }

    WorkerPrivate* worker = mProxy->GetWorkerPrivate();
    RefPtr<GetSubscriptionResultRunnable> r = new GetSubscriptionResultRunnable(
        worker, std::move(mProxy), aStatus, endpoint, mScope,
        std::move(mExpirationTime), std::move(rawP256dhKey),
        std::move(authSecret), std::move(appServerKey));
    if (!r->Dispatch()) {
      return NS_ERROR_UNEXPECTED;
    }

    return NS_OK;
  }

  // Convenience method for use in this file.
  void OnPushSubscriptionError(nsresult aStatus) {
    Unused << NS_WARN_IF(NS_FAILED(OnPushSubscription(aStatus, nullptr)));
  }

 protected:
  ~GetSubscriptionCallback() = default;

 private:
  RefPtr<PromiseWorkerProxy> mProxy;
  nsString mScope;
  Nullable<EpochTimeStamp> mExpirationTime;
};

NS_IMPL_ISUPPORTS(GetSubscriptionCallback, nsIPushSubscriptionCallback)

class GetSubscriptionRunnable final : public Runnable {
 public:
  GetSubscriptionRunnable(PromiseWorkerProxy* aProxy, const nsAString& aScope,
                          PushManager::SubscriptionAction aAction,
                          nsTArray<uint8_t>&& aAppServerKey)
      : Runnable("dom::GetSubscriptionRunnable"),
        mProxy(aProxy),
        mScope(aScope),
        mAction(aAction),
        mAppServerKey(std::move(aAppServerKey)) {}

  NS_IMETHOD
  Run() override {
    AssertIsOnMainThread();

    nsCOMPtr<nsIPrincipal> principal;

    {
      // Bug 1228723: If permission is revoked or an error occurs, the
      // subscription callback will be called synchronously. This causes
      // `GetSubscriptionCallback::OnPushSubscription` to deadlock when
      // it tries to acquire the lock.
      MutexAutoLock lock(mProxy->Lock());
      if (mProxy->CleanedUp()) {
        return NS_OK;
      }
      principal = mProxy->GetWorkerPrivate()->GetPrincipal();
    }

    MOZ_ASSERT(principal);

    RefPtr<GetSubscriptionCallback> callback =
        new GetSubscriptionCallback(mProxy, mScope);

    PermissionState state;
    nsresult rv = GetPermissionState(principal, state);
    if (NS_FAILED(rv)) {
      callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
      return NS_OK;
    }

    if (state != PermissionState::Granted) {
      if (mAction == PushManager::GetSubscriptionAction) {
        callback->OnPushSubscriptionError(NS_OK);
        return NS_OK;
      }
      callback->OnPushSubscriptionError(NS_ERROR_DOM_PUSH_DENIED_ERR);
      return NS_OK;
    }

    nsCOMPtr<nsIPushService> service =
        do_GetService("@mozilla.org/push/Service;1");
    if (NS_WARN_IF(!service)) {
      callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
      return NS_OK;
    }

    if (mAction == PushManager::SubscribeAction) {
      if (mAppServerKey.IsEmpty()) {
        rv = service->Subscribe(mScope, principal, callback);
      } else {
        rv = service->SubscribeWithKey(mScope, principal, mAppServerKey,
                                       callback);
      }
    } else {
      MOZ_ASSERT(mAction == PushManager::GetSubscriptionAction);
      rv = service->GetSubscription(mScope, principal, callback);
    }

    if (NS_WARN_IF(NS_FAILED(rv))) {
      callback->OnPushSubscriptionError(NS_ERROR_FAILURE);
      return NS_OK;
    }

    return NS_OK;
  }

 private:
  ~GetSubscriptionRunnable() = default;

  RefPtr<PromiseWorkerProxy> mProxy;
  nsString mScope;
  PushManager::SubscriptionAction mAction;
  nsTArray<uint8_t> mAppServerKey;
};

class PermissionResultRunnable final : public WorkerRunnable {
 public:
  PermissionResultRunnable(PromiseWorkerProxy* aProxy, nsresult aStatus,
                           PermissionState aState)
      : WorkerRunnable(aProxy->GetWorkerPrivate()),
        mProxy(aProxy),
        mStatus(aStatus),
        mState(aState) {
    AssertIsOnMainThread();
  }

  bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
    MOZ_ASSERT(aWorkerPrivate);
    aWorkerPrivate->AssertIsOnWorkerThread();

    RefPtr<Promise> promise = mProxy->WorkerPromise();
    if (NS_SUCCEEDED(mStatus)) {
      promise->MaybeResolve(mState);
    } else {
      promise->MaybeRejectWithUndefined();
    }

    mProxy->CleanUp();

    return true;
  }

 private:
  ~PermissionResultRunnable() = default;

  RefPtr<PromiseWorkerProxy> mProxy;
  nsresult mStatus;
  PermissionState mState;
};

class PermissionStateRunnable final : public Runnable {
 public:
  explicit PermissionStateRunnable(PromiseWorkerProxy* aProxy)
      : Runnable("dom::PermissionStateRunnable"), mProxy(aProxy) {}

  NS_IMETHOD
  Run() override {
    AssertIsOnMainThread();
    MutexAutoLock lock(mProxy->Lock());
    if (mProxy->CleanedUp()) {
      return NS_OK;
    }

    PermissionState state;
    nsresult rv =
        GetPermissionState(mProxy->GetWorkerPrivate()->GetPrincipal(), state);

    RefPtr<PermissionResultRunnable> r =
        new PermissionResultRunnable(mProxy, rv, state);

    // This can fail if the worker thread is already shutting down, but there's
    // nothing we can do in that case.
    Unused << NS_WARN_IF(!r->Dispatch());

    return NS_OK;
  }

 private:
  ~PermissionStateRunnable() = default;

  RefPtr<PromiseWorkerProxy> mProxy;
};

}  // anonymous namespace

PushManager::PushManager(nsIGlobalObject* aGlobal, PushManagerImpl* aImpl)
    : mGlobal(aGlobal), mImpl(aImpl) {
  AssertIsOnMainThread();
  MOZ_ASSERT(aImpl);
}

PushManager::PushManager(const nsAString& aScope) : mScope(aScope) {
#ifdef DEBUG
  // There's only one global on a worker, so we don't need to pass a global
  // object to the constructor.
  WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
  MOZ_ASSERT(worker);
  worker->AssertIsOnWorkerThread();
#endif
}

PushManager::~PushManager() = default;

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushManager, mGlobal, mImpl)
NS_IMPL_CYCLE_COLLECTING_ADDREF(PushManager)
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushManager)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushManager)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

JSObject* PushManager::WrapObject(JSContext* aCx,
                                  JS::Handle<JSObject*> aGivenProto) {
  return PushManager_Binding::Wrap(aCx, this, aGivenProto);
}

// static
already_AddRefed<PushManager> PushManager::Constructor(GlobalObject& aGlobal,
                                                       const nsAString& aScope,
                                                       ErrorResult& aRv) {
  if (!NS_IsMainThread()) {
    RefPtr<PushManager> ret = new PushManager(aScope);
    return ret.forget();
  }

  RefPtr<PushManagerImpl> impl =
      PushManagerImpl::Constructor(aGlobal, aGlobal.Context(), aScope, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }

  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  RefPtr<PushManager> ret = new PushManager(global, impl);

  return ret.forget();
}

bool PushManager::IsEnabled(JSContext* aCx, JSObject* aGlobal) {
  return StaticPrefs::dom_push_enabled() && ServiceWorkerVisible(aCx, aGlobal);
}

already_AddRefed<Promise> PushManager::Subscribe(
    const PushSubscriptionOptionsInit& aOptions, ErrorResult& aRv) {
  if (mImpl) {
    MOZ_ASSERT(NS_IsMainThread());
    return mImpl->Subscribe(aOptions, aRv);
  }

  return PerformSubscriptionActionFromWorker(SubscribeAction, aOptions, aRv);
}

already_AddRefed<Promise> PushManager::GetSubscription(ErrorResult& aRv) {
  if (mImpl) {
    MOZ_ASSERT(NS_IsMainThread());
    return mImpl->GetSubscription(aRv);
  }

  return PerformSubscriptionActionFromWorker(GetSubscriptionAction, aRv);
}

already_AddRefed<Promise> PushManager::PermissionState(
    const PushSubscriptionOptionsInit& aOptions, ErrorResult& aRv) {
  if (mImpl) {
    MOZ_ASSERT(NS_IsMainThread());
    return mImpl->PermissionState(aOptions, aRv);
  }

  WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
  MOZ_ASSERT(worker);
  worker->AssertIsOnWorkerThread();

  nsCOMPtr<nsIGlobalObject> global = worker->GlobalScope();
  RefPtr<Promise> p = Promise::Create(global, aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p);
  if (!proxy) {
    p->MaybeRejectWithUndefined();
    return p.forget();
  }

  RefPtr<PermissionStateRunnable> r = new PermissionStateRunnable(proxy);
  NS_DispatchToMainThread(r);

  return p.forget();
}

already_AddRefed<Promise> PushManager::PerformSubscriptionActionFromWorker(
    SubscriptionAction aAction, ErrorResult& aRv) {
  RootedDictionary<PushSubscriptionOptionsInit> options(RootingCx());
  return PerformSubscriptionActionFromWorker(aAction, options, aRv);
}

already_AddRefed<Promise> PushManager::PerformSubscriptionActionFromWorker(
    SubscriptionAction aAction, const PushSubscriptionOptionsInit& aOptions,
    ErrorResult& aRv) {
  WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
  MOZ_ASSERT(worker);
  worker->AssertIsOnWorkerThread();

  nsCOMPtr<nsIGlobalObject> global = worker->GlobalScope();
  RefPtr<Promise> p = Promise::Create(global, aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, p);
  if (!proxy) {
    p->MaybeReject(NS_ERROR_DOM_PUSH_ABORT_ERR);
    return p.forget();
  }

  nsTArray<uint8_t> appServerKey;
  if (!aOptions.mApplicationServerKey.IsNull()) {
    nsresult rv = NormalizeAppServerKey(aOptions.mApplicationServerKey.Value(),
                                        appServerKey);
    if (NS_FAILED(rv)) {
      p->MaybeReject(rv);
      return p.forget();
    }
  }

  RefPtr<GetSubscriptionRunnable> r = new GetSubscriptionRunnable(
      proxy, mScope, aAction, std::move(appServerKey));
  MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r));

  return p.forget();
}

nsresult PushManager::NormalizeAppServerKey(
    const OwningArrayBufferViewOrArrayBufferOrString& aSource,
    nsTArray<uint8_t>& aAppServerKey) {
  if (aSource.IsString()) {
    NS_ConvertUTF16toUTF8 base64Key(aSource.GetAsString());
    FallibleTArray<uint8_t> decodedKey;
    nsresult rv = Base64URLDecode(
        base64Key, Base64URLDecodePaddingPolicy::Reject, decodedKey);
    if (NS_FAILED(rv)) {
      return NS_ERROR_DOM_INVALID_CHARACTER_ERR;
    }
    aAppServerKey = decodedKey;
  } else if (aSource.IsArrayBuffer()) {
    if (!PushUtil::CopyArrayBufferToArray(aSource.GetAsArrayBuffer(),
                                          aAppServerKey)) {
      return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR;
    }
  } else if (aSource.IsArrayBufferView()) {
    if (!PushUtil::CopyArrayBufferViewToArray(aSource.GetAsArrayBufferView(),
                                              aAppServerKey)) {
      return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR;
    }
  } else {
    MOZ_CRASH("Uninitialized union: expected string, buffer, or view");
  }
  if (aAppServerKey.IsEmpty()) {
    return NS_ERROR_DOM_PUSH_INVALID_KEY_ERR;
  }
  return NS_OK;
}

}  // namespace mozilla::dom
