/* 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 "BoundStorageKey.h"

#include "BoundStorageKeyCache.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/cache/AutoUtils.h"
#include "mozilla/dom/cache/Cache.h"
#include "mozilla/dom/cache/CacheStorageChild.h"
#include "mozilla/dom/cache/CacheWorkerRef.h"
#include "mozilla/dom/quota/PrincipalUtils.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"

namespace mozilla::dom::cache {
using mozilla::ipc::BackgroundChild;
using mozilla::ipc::PrincipalInfo;
NS_IMPL_ISUPPORTS(BoundStorageKey, nsISupports)

template <typename PromiseType>
struct BoundStorageKeyCacheStorage::Entry final {
  RefPtr<PromiseType> mPromise;
  CacheOpArgs mArgs;
};

nsresult BoundStorageKey::Init(Namespace aNamespace,
                               const PrincipalInfo& aPrincipalInfo,
                               nsISerialEventTarget* aTarget) {
  MOZ_DIAGNOSTIC_ASSERT(aTarget);

  if (!BackgroundChild::ValidatePrincipalInfo(aPrincipalInfo, {})) {
    MOZ_ASSERT_UNREACHABLE("BoundStorageKey failed to validate principal");
    return NS_ERROR_UNEXPECTED;
  }

  // setup child and parent actors and retarget to aTarget
  auto* actor = new BoundStorageKeyChild(this);
  MOZ_ASSERT(actor);

  {
    ::mozilla::ipc::Endpoint<PBoundStorageKeyChild> childEP;
    ::mozilla::ipc::Endpoint<PBoundStorageKeyParent> parentEP;

    auto rv = PBoundStorageKey::CreateEndpoints(&parentEP, &childEP);
    (void)NS_WARN_IF(NS_FAILED(rv));

    PBackgroundChild* bgActor = BackgroundChild::GetOrCreateForCurrentThread();
    if (NS_WARN_IF(!bgActor)) {
      NS_WARNING("BoundStorageKey failed to obtain bgActor");
      return NS_ERROR_UNEXPECTED;
    }

    bgActor->SendCreateBoundStorageKeyParent(std::move(parentEP), aNamespace,
                                             aPrincipalInfo);

    if (NS_WARN_IF(!(childEP.Bind(actor, aTarget)))) {
      NS_WARNING("BoundStorageKeyChildActor failed to bind to target.");
      return NS_ERROR_UNEXPECTED;
    }
  }

  mActor = actor;
  return NS_OK;
}

void BoundStorageKey::OnActorDestroy(BoundStorageKeyChild* aActor) {
  MOZ_DIAGNOSTIC_ASSERT(mActor);
  MOZ_DIAGNOSTIC_ASSERT(mActor == aActor);
  MOZ_DIAGNOSTIC_ASSERT(!NS_FAILED(mStatus));

  // Note that we will never get an actor again in case another request is
  // made before this object is destructed.
  mActor->ClearListener();
  mActor = nullptr;
  mStatus = NS_ERROR_UNEXPECTED;
}

BoundStorageKey::~BoundStorageKey() {
  if (mActor) {
    mActor->StartDestroyFromListener();
  }
  MOZ_DIAGNOSTIC_ASSERT(!mActor);
}

// static
already_AddRefed<BoundStorageKeyCacheStorage>
BoundStorageKeyCacheStorage::Create(Namespace aNamespace,
                                    nsIGlobalObject* aGlobal,
                                    WorkerPrivate* aWorkerPrivate,
                                    nsISerialEventTarget* aActorTarget,
                                    ErrorResult& aRv) {
  MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate);

  if (aWorkerPrivate->GetOriginAttributes().IsPrivateBrowsing() &&
      !StaticPrefs::dom_cache_privateBrowsing_enabled()) {
    NS_WARNING("BoundStorageKey not supported during private browsing.");

    aRv = NS_ERROR_DOM_SECURITY_ERR;
    return nullptr;
  }

  const PrincipalInfo& principalInfo =
      aWorkerPrivate->GetEffectiveStoragePrincipalInfo();

  QM_TRY(OkIf(quota::IsPrincipalInfoValid(principalInfo)), nullptr,
         [&aRv](const auto) { aRv = NS_ERROR_FAILURE; });

  // We have a number of cases where we want to skip the https scheme
  // validation:
  //
  // 1) Any worker when dom.caches.testing.enabled pref is true.
  // 2) Any worker when dom.serviceWorkers.testing.enabled pref is true.  This
  //    is mainly because most sites using SWs will expect Cache to work if
  //    SWs are enabled.
  // 3) If the window that created this worker has the devtools SW testing
  //    option enabled.  Same reasoning as (2).
  // 4) If the worker itself is a ServiceWorker, then we always skip the
  //    origin checks.  The ServiceWorker has its own trusted origin checks
  //    that are better than ours.  In addition, we don't have information
  //    about the window any more, so we can't do our own checks.
  bool testingEnabled = StaticPrefs::dom_caches_testing_enabled() ||
                        StaticPrefs::dom_serviceWorkers_testing_enabled() ||
                        aWorkerPrivate->ServiceWorkersTestingInWindow() ||
                        aWorkerPrivate->IsServiceWorker();

  if (!IsTrusted(principalInfo, testingEnabled)) {
    NS_WARNING("BoundStorageKey not supported on untrusted origins.");

    aRv = NS_ERROR_UNEXPECTED;
    return nullptr;
  }

  RefPtr<BoundStorageKeyCacheStorage> ref =
      new BoundStorageKeyCacheStorage(aNamespace, aGlobal, principalInfo);

  auto rv = ref->Init(aWorkerPrivate, aNamespace, principalInfo, aActorTarget);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    aRv = rv;
    return nullptr;
  }

  MOZ_ASSERT(ref->mActor);
  return ref.forget();
}

nsresult BoundStorageKeyCacheStorage::Init(WorkerPrivate* aWorkerPrivate,
                                           Namespace aNamespace,
                                           const PrincipalInfo& aPrincipalInfo,
                                           nsISerialEventTarget* aTarget) {
  auto rc = BoundStorageKey::Init(aNamespace, aPrincipalInfo, aTarget);
  if (NS_FAILED(rc)) {
    return rc;
  }

  mCacheStorageChild = CreateCacheStorageChild(aWorkerPrivate);
  MOZ_ASSERT(mCacheStorageChild);

  return mCacheStorageChild ? NS_OK : NS_ERROR_FAILURE;
}

BoundStorageKeyCacheStorage::BoundStorageKeyCacheStorage(
    Namespace aNamespace, nsIGlobalObject* aGlobal,
    const mozilla::ipc::PrincipalInfo& aPrincipalInfo)
    : mGlobal(aGlobal),
      mPrincipalInfo(MakeUnique<PrincipalInfo>(aPrincipalInfo)),
      mNamespace(aNamespace) {}

void BoundStorageKeyCacheStorage::OnActorDestroy(CacheStorageChild* aActor) {
  AssertOwningThread();

  MOZ_DIAGNOSTIC_ASSERT(mActor);
  MOZ_DIAGNOSTIC_ASSERT(mCacheStorageChild.get() == aActor);

  mCacheStorageChild->ClearListener();
  mCacheStorageChild = nullptr;
}

BoundStorageKeyCacheStorage::~BoundStorageKeyCacheStorage() {
  AssertOwningThread();
  if (mCacheStorageChild) {
    mCacheStorageChild->StartDestroyFromListener();
  }
}

already_AddRefed<CacheStorageChild>
BoundStorageKeyCacheStorage::CreateCacheStorageChild(
    WorkerPrivate* aWorkerPrivate) {
  SafeRefPtr<CacheWorkerRef> workerRef;
  if (aWorkerPrivate) {
    aWorkerPrivate->AssertIsOnWorkerThread();
    workerRef =
        CacheWorkerRef::Create(aWorkerPrivate, CacheWorkerRef::eIPCWorkerRef);
    if (NS_WARN_IF(!workerRef)) {
      return nullptr;
    }
  }

  RefPtr<CacheStorageChild> newActor =
      new CacheStorageChild(this, std::move(workerRef), mActor.get());
  auto* constructedActor = mActor->SendPCacheStorageConstructor(
      newActor, mNamespace, *mPrincipalInfo);

  if (NS_WARN_IF(!constructedActor)) {
    mStatus = NS_ERROR_UNEXPECTED;
    return nullptr;
  }

  MOZ_DIAGNOSTIC_ASSERT(newActor == constructedActor);
  return newActor.forget();
}

template <typename EntryType>
void BoundStorageKeyCacheStorage::RunRequest(EntryType&& aEntry) {
  MOZ_ASSERT(mActor);
  MOZ_ASSERT(mCacheStorageChild);

  AutoChildOpArgs args(this, aEntry.mArgs, 1);
  RefPtr<CacheStoragePromise> p{aEntry.mPromise.forget()};
  mCacheStorageChild->ExecuteOp(mGlobal, p, this, args.SendAsOpArgs());
}

auto BoundStorageKeyCacheStorage::Open(const nsAString& aKey, ErrorResult& aRv)
    -> already_AddRefed<CacheStoragePromise> {
  AssertOwningThread();

  if (NS_WARN_IF(NS_FAILED(mStatus))) {
    aRv = mStatus;
    return nullptr;
  }

  RefPtr<OpenResultPromise::Private> promise{
      new OpenResultPromise::Private(__func__)};
  RunRequest(Entry<OpenResultPromise::Private>{
      promise, StorageOpenArgs{nsString(aKey)}});

  return promise.forget();
}

auto BoundStorageKeyCacheStorage::Has(const nsAString& aKey, ErrorResult& aRv)
    -> already_AddRefed<CacheStoragePromise> {
  AssertOwningThread();

  if (NS_WARN_IF(NS_FAILED(mStatus))) {
    aRv = mStatus;
    return nullptr;
  }

  RefPtr<HasResultPromise::Private> promise{
      new HasResultPromise::Private(__func__)};
  RunRequest(Entry<HasResultPromise::Private>{promise,
                                              StorageHasArgs(nsString(aKey))});

  return promise.forget();
}

auto BoundStorageKeyCacheStorage::Delete(const nsAString& aKey,
                                         ErrorResult& aRv)
    -> already_AddRefed<CacheStoragePromise> {
  AssertOwningThread();

  if (NS_WARN_IF(NS_FAILED(mStatus))) {
    aRv = mStatus;
    return nullptr;
  }

  RefPtr<DeleteResultPromise::Private> promise{
      new DeleteResultPromise::Private(__func__)};
  RunRequest(Entry<DeleteResultPromise::Private>{
      promise, StorageDeleteArgs{nsString(aKey)}});

  return promise.forget();
}

auto BoundStorageKeyCacheStorage::Keys(ErrorResult& aRv)
    -> already_AddRefed<CacheStoragePromise> {
  AssertOwningThread();

  if (NS_WARN_IF(NS_FAILED(mStatus))) {
    aRv = mStatus;
    return nullptr;
  }

  RefPtr<KeysResultPromise::Private> promise{
      new KeysResultPromise::Private(__func__)};
  RunRequest(Entry<KeysResultPromise::Private>{promise, StorageKeysArgs()});

  return promise.forget();
}

}  // namespace mozilla::dom::cache
