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

#include "js/Object.h"              // JS::GetClass
#include "js/PropertyAndElement.h"  // JS_DefineProperty
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_extensions.h"
#include "mozilla/dom/CacheBinding.h"
#include "mozilla/dom/CacheStorageBinding.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/InternalRequest.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/Response.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/cache/AutoUtils.h"
#include "mozilla/dom/cache/Cache.h"
#include "mozilla/dom/cache/CacheChild.h"
#include "mozilla/dom/cache/CacheCommon.h"
#include "mozilla/dom/cache/CacheStorageChild.h"
#include "mozilla/dom/cache/CacheWorkerRef.h"
#include "mozilla/dom/cache/PCacheChild.h"
#include "mozilla/dom/cache/ReadStream.h"
#include "mozilla/dom/cache/TypeUtils.h"
#include "mozilla/dom/quota/PrincipalUtils.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "nsContentUtils.h"
#include "nsIGlobalObject.h"
#include "nsMixedContentBlocker.h"
#include "nsURLParsers.h"

namespace mozilla::dom::cache {

using mozilla::ErrorResult;
using mozilla::ipc::BackgroundChild;
using mozilla::ipc::PBackgroundChild;
using mozilla::ipc::PrincipalInfo;
using mozilla::ipc::PrincipalToPrincipalInfo;

NS_IMPL_CYCLE_COLLECTING_ADDREF(mozilla::dom::cache::CacheStorage);
NS_IMPL_CYCLE_COLLECTING_RELEASE(mozilla::dom::cache::CacheStorage);
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(mozilla::dom::cache::CacheStorage,
                                      mGlobal);

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CacheStorage)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

// We cannot reference IPC types in a webidl binding implementation header.  So
// define this in the .cpp.
struct CacheStorage::Entry final {
  RefPtr<Promise> mPromise;
  CacheOpArgs mArgs;
  // We cannot add the requests until after the actor is present.  So store
  // the request data separately for now.
  SafeRefPtr<InternalRequest> mRequest;
};

bool IsTrusted(const PrincipalInfo& aPrincipalInfo, bool aTestingPrefEnabled) {
  // Can happen on main thread or worker thread

  if (aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) {
    return true;
  }

  // Require a ContentPrincipal to avoid null principal, etc.
  QM_TRY(OkIf(aPrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo),
         false);

  // If we're in testing mode, then don't do any more work to determine if
  // the origin is trusted.  We have to run some tests as http.
  if (aTestingPrefEnabled) {
    return true;
  }

  // Now parse the scheme of the principal's origin.  This is a short term
  // method for determining "trust".  In the long term we need to implement
  // the full algorithm here:
  //
  // https://w3c.github.io/webappsec/specs/powerfulfeatures/#settings-secure
  //
  // TODO: Implement full secure setting algorithm. (bug 1177856)

  const nsCString& flatURL = aPrincipalInfo.get_ContentPrincipalInfo().spec();
  const char* const url = flatURL.get();

  // off the main thread URL parsing using nsStdURLParser.
  const nsCOMPtr<nsIURLParser> urlParser = new nsStdURLParser();

  uint32_t schemePos;
  int32_t schemeLen;
  uint32_t authPos;
  int32_t authLen;
  QM_TRY(MOZ_TO_RESULT(urlParser->ParseURL(url, flatURL.Length(), &schemePos,
                                           &schemeLen, &authPos, &authLen,
                                           nullptr, nullptr)),  // ignore path
         false);

  const nsAutoCString scheme(Substring(flatURL, schemePos, schemeLen));
  if (scheme.LowerCaseEqualsLiteral("https") ||
      scheme.LowerCaseEqualsLiteral("file") ||
      scheme.LowerCaseEqualsLiteral("moz-extension")) {
    return true;
  }

  uint32_t hostPos;
  int32_t hostLen;
  QM_TRY(MOZ_TO_RESULT(
             urlParser->ParseAuthority(url + authPos, authLen, nullptr,
                                       nullptr,           // ignore username
                                       nullptr, nullptr,  // ignore password
                                       &hostPos, &hostLen,
                                       nullptr)),  // ignore port
         false);

  return nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackHost(
      nsDependentCSubstring(url + authPos + hostPos, hostLen));
}

// static
already_AddRefed<CacheStorage> CacheStorage::CreateOnMainThread(
    Namespace aNamespace, nsIGlobalObject* aGlobal, nsIPrincipal* aPrincipal,
    bool aForceTrustedOrigin, ErrorResult& aRv) {
  MOZ_DIAGNOSTIC_ASSERT(aGlobal);
  MOZ_DIAGNOSTIC_ASSERT(aPrincipal);
  MOZ_ASSERT(NS_IsMainThread());

  PrincipalInfo principalInfo;
  QM_TRY(MOZ_TO_RESULT(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)),
         nullptr, [&aRv](const nsresult rv) { aRv.Throw(rv); });

  QM_TRY(OkIf(quota::IsPrincipalInfoValid(principalInfo)),
         RefPtr{new CacheStorage(NS_ERROR_DOM_SECURITY_ERR)}.forget(),
         [](const auto) {
           NS_WARNING("CacheStorage not supported on invalid origins.");
         });

  const bool testingEnabled =
      aForceTrustedOrigin ||
      Preferences::GetBool("dom.caches.testing.enabled", false) ||
      StaticPrefs::dom_serviceWorkers_testing_enabled();

  if (!IsTrusted(principalInfo, testingEnabled)) {
    NS_WARNING("CacheStorage not supported on untrusted origins.");
    RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR);
    return ref.forget();
  }

  RefPtr<CacheStorage> ref =
      new CacheStorage(aNamespace, aGlobal, principalInfo, nullptr);
  return ref.forget();
}

// static
already_AddRefed<CacheStorage> CacheStorage::CreateOnWorker(
    Namespace aNamespace, nsIGlobalObject* aGlobal,
    WorkerPrivate* aWorkerPrivate, ErrorResult& aRv) {
  MOZ_DIAGNOSTIC_ASSERT(aGlobal);
  MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate);
  aWorkerPrivate->AssertIsOnWorkerThread();

  if (aWorkerPrivate->GetOriginAttributes().IsPrivateBrowsing() &&
      !StaticPrefs::dom_cache_privateBrowsing_enabled()) {
    NS_WARNING("CacheStorage not supported during private browsing.");
    RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR);
    return ref.forget();
  }

  SafeRefPtr<CacheWorkerRef> workerRef =
      CacheWorkerRef::Create(aWorkerPrivate, CacheWorkerRef::eIPCWorkerRef);
  if (!workerRef) {
    NS_WARNING("Worker thread is shutting down.");
    aRv.Throw(NS_ERROR_FAILURE);
    return nullptr;
  }

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

  QM_TRY(OkIf(quota::IsPrincipalInfoValid(principalInfo)), nullptr,
         [&aRv](const auto) { aRv.Throw(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("CacheStorage not supported on untrusted origins.");
    RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR);
    return ref.forget();
  }

  RefPtr<CacheStorage> ref = new CacheStorage(
      aNamespace, aGlobal, principalInfo, std::move(workerRef));
  return ref.forget();
}

// static
bool CacheStorage::DefineCachesForSandbox(JSContext* aCx,
                                          JS::Handle<JSObject*> aGlobal) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_DIAGNOSTIC_ASSERT(JS::GetClass(aGlobal)->flags & JSCLASS_DOM_GLOBAL,
                        "Passed object is not a global object!");
  js::AssertSameCompartment(aCx, aGlobal);

  if (NS_WARN_IF(!CacheStorage_Binding::CreateAndDefineOnGlobal(aCx) ||
                 !Cache_Binding::CreateAndDefineOnGlobal(aCx))) {
    return false;
  }

  nsIPrincipal* principal = nsContentUtils::ObjectPrincipal(aGlobal);
  MOZ_DIAGNOSTIC_ASSERT(principal);

  ErrorResult rv;
  RefPtr<CacheStorage> storage =
      CreateOnMainThread(DEFAULT_NAMESPACE, xpc::NativeGlobal(aGlobal),
                         principal, true, /* force trusted */
                         rv);
  if (NS_WARN_IF(rv.MaybeSetPendingException(aCx))) {
    return false;
  }

  JS::Rooted<JS::Value> caches(aCx);
  if (NS_WARN_IF(!ToJSValue(aCx, storage, &caches))) {
    return false;
  }

  return JS_DefineProperty(aCx, aGlobal, "caches", caches, JSPROP_ENUMERATE);
}

// static
bool CacheStorage::CachesEnabled(JSContext* aCx, JSObject* aObj) {
  return cache::Cache::CachesEnabled(aCx, aObj);
}

CacheStorage::CacheStorage(Namespace aNamespace, nsIGlobalObject* aGlobal,
                           const PrincipalInfo& aPrincipalInfo,
                           SafeRefPtr<CacheWorkerRef> aWorkerRef)
    : mNamespace(aNamespace),
      mGlobal(aGlobal),
      mPrincipalInfo(MakeUnique<PrincipalInfo>(aPrincipalInfo)),
      mActor(nullptr),
      mStatus(NS_OK) {
  MOZ_DIAGNOSTIC_ASSERT(mGlobal);

  // Throw if this process wouldn't be allowed to access storage.
  if (!BackgroundChild::ValidatePrincipalInfo(*mPrincipalInfo, {})) {
    MOZ_ASSERT_UNREACHABLE(
        "ValidatePrincipalInfo failed in CacheStorage constructor");
    mStatus = NS_ERROR_UNEXPECTED;
    return;
  }

  // If the PBackground actor is already initialized then we can
  // immediately use it
  PBackgroundChild* actor = BackgroundChild::GetOrCreateForCurrentThread();
  if (NS_WARN_IF(!actor)) {
    mStatus = NS_ERROR_UNEXPECTED;
    return;
  }

  // WorkerRef ownership is passed to the CacheStorageChild actor and any
  // actors it may create.  The WorkerRef will keep the worker thread alive
  // until the actors can gracefully shutdown.
  CacheStorageChild* newActor =
      new CacheStorageChild(this, std::move(aWorkerRef));
  PCacheStorageChild* constructedActor = actor->SendPCacheStorageConstructor(
      newActor, mNamespace, *mPrincipalInfo);

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

  MOZ_DIAGNOSTIC_ASSERT(constructedActor == newActor);
  mActor = newActor;
}

CacheStorage::CacheStorage(nsresult aFailureResult)
    : mNamespace(INVALID_NAMESPACE), mActor(nullptr), mStatus(aFailureResult) {
  MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(mStatus));
}

already_AddRefed<Promise> CacheStorage::Match(
    JSContext* aCx, const RequestOrUTF8String& aRequest,
    const MultiCacheQueryOptions& aOptions, ErrorResult& aRv) {
  NS_ASSERT_OWNINGTHREAD(CacheStorage);

  if (!HasStorageAccess(eUseCounter_custom_PrivateBrowsingCachesMatch,
                        UseCounterWorker::Custom_PrivateBrowsingCachesMatch)) {
    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
    return nullptr;
  }

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

  SafeRefPtr<InternalRequest> request =
      ToInternalRequest(aCx, aRequest, IgnoreBody, aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
  if (NS_WARN_IF(!promise)) {
    return nullptr;
  }

  CacheQueryParams params;
  ToCacheQueryParams(params, aOptions);

  auto entry = MakeUnique<Entry>();
  entry->mPromise = promise;
  entry->mArgs = StorageMatchArgs(CacheRequest(), params, GetOpenMode());
  entry->mRequest = std::move(request);

  RunRequest(std::move(entry));

  return promise.forget();
}

already_AddRefed<Promise> CacheStorage::Has(const nsAString& aKey,
                                            ErrorResult& aRv) {
  NS_ASSERT_OWNINGTHREAD(CacheStorage);

  if (!HasStorageAccess(eUseCounter_custom_PrivateBrowsingCachesHas,
                        UseCounterWorker::Custom_PrivateBrowsingCachesHas)) {
    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
    return nullptr;
  }

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

  RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
  if (NS_WARN_IF(!promise)) {
    return nullptr;
  }

  auto entry = MakeUnique<Entry>();
  entry->mPromise = promise;
  entry->mArgs = StorageHasArgs(nsString(aKey));

  RunRequest(std::move(entry));

  return promise.forget();
}

already_AddRefed<Promise> CacheStorage::Open(const nsAString& aKey,
                                             ErrorResult& aRv) {
  NS_ASSERT_OWNINGTHREAD(CacheStorage);

  if (!HasStorageAccess(eUseCounter_custom_PrivateBrowsingCachesOpen,
                        UseCounterWorker::Custom_PrivateBrowsingCachesOpen)) {
    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
    return nullptr;
  }

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

  RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
  if (NS_WARN_IF(!promise)) {
    return nullptr;
  }

  auto entry = MakeUnique<Entry>();
  entry->mPromise = promise;
  entry->mArgs = StorageOpenArgs(nsString(aKey));

  RunRequest(std::move(entry));

  return promise.forget();
}

already_AddRefed<Promise> CacheStorage::Delete(const nsAString& aKey,
                                               ErrorResult& aRv) {
  NS_ASSERT_OWNINGTHREAD(CacheStorage);

  if (!HasStorageAccess(eUseCounter_custom_PrivateBrowsingCachesDelete,
                        UseCounterWorker::Custom_PrivateBrowsingCachesDelete)) {
    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
    return nullptr;
  }

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

  RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
  if (NS_WARN_IF(!promise)) {
    return nullptr;
  }

  auto entry = MakeUnique<Entry>();
  entry->mPromise = promise;
  entry->mArgs = StorageDeleteArgs(nsString(aKey));

  RunRequest(std::move(entry));

  return promise.forget();
}

already_AddRefed<Promise> CacheStorage::Keys(ErrorResult& aRv) {
  NS_ASSERT_OWNINGTHREAD(CacheStorage);

  if (!HasStorageAccess(eUseCounter_custom_PrivateBrowsingCachesKeys,
                        UseCounterWorker::Custom_PrivateBrowsingCachesKeys)) {
    aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
    return nullptr;
  }

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

  RefPtr<Promise> promise = Promise::Create(mGlobal, aRv);
  if (NS_WARN_IF(!promise)) {
    return nullptr;
  }

  auto entry = MakeUnique<Entry>();
  entry->mPromise = promise;
  entry->mArgs = StorageKeysArgs();

  RunRequest(std::move(entry));

  return promise.forget();
}

// static
already_AddRefed<CacheStorage> CacheStorage::Constructor(
    const GlobalObject& aGlobal, CacheStorageNamespace aNamespace,
    nsIPrincipal* aPrincipal, ErrorResult& aRv) {
  if (NS_WARN_IF(!NS_IsMainThread())) {
    aRv.Throw(NS_ERROR_FAILURE);
    return nullptr;
  }

  // TODO: remove Namespace in favor of CacheStorageNamespace
  static_assert(DEFAULT_NAMESPACE == (uint32_t)CacheStorageNamespace::Content,
                "Default namespace should match webidl Content enum");
  static_assert(
      CHROME_ONLY_NAMESPACE == (uint32_t)CacheStorageNamespace::Chrome,
      "Chrome namespace should match webidl Chrome enum");
  static_assert(
      NUMBER_OF_NAMESPACES == ContiguousEnumSize<CacheStorageNamespace>::value,
      "Number of namespace should match webidl count");

  Namespace ns = static_cast<Namespace>(aNamespace);
  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());

  bool privateBrowsing = false;
  if (nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global)) {
    RefPtr<Document> doc = window->GetExtantDoc();
    if (doc) {
      nsCOMPtr<nsILoadContext> loadContext = doc->GetLoadContext();
      privateBrowsing = loadContext && loadContext->UsePrivateBrowsing();
    }
  }

  if (privateBrowsing && !StaticPrefs::dom_cache_privateBrowsing_enabled()) {
    RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR);
    return ref.forget();
  }

  // Create a CacheStorage object bypassing the trusted origin checks
  // since this is a chrome-only constructor.
  return CreateOnMainThread(ns, global, aPrincipal,
                            true /* force trusted origin */, aRv);
}

nsISupports* CacheStorage::GetParentObject() const { return mGlobal; }

JSObject* CacheStorage::WrapObject(JSContext* aContext,
                                   JS::Handle<JSObject*> aGivenProto) {
  return mozilla::dom::CacheStorage_Binding::Wrap(aContext, this, aGivenProto);
}

void CacheStorage::OnActorDestroy(CacheStorageChild* aActor) {
  NS_ASSERT_OWNINGTHREAD(CacheStorage);
  MOZ_DIAGNOSTIC_ASSERT(mActor);
  MOZ_DIAGNOSTIC_ASSERT(mActor == aActor);
  MOZ_DIAGNOSTIC_ASSERT(!NS_FAILED(mStatus));
  mActor->ClearListener();
  mActor = nullptr;
  mStatus = NS_ERROR_UNEXPECTED;

  // Note that we will never get an actor again in case another request is
  // made before this object is destructed.
}

nsIGlobalObject* CacheStorage::GetGlobalObject() const { return mGlobal; }

#ifdef DEBUG
void CacheStorage::AssertOwningThread() const {
  NS_ASSERT_OWNINGTHREAD(CacheStorage);
}
#endif

CacheStorage::~CacheStorage() {
  NS_ASSERT_OWNINGTHREAD(CacheStorage);
  if (mActor) {
    mActor->StartDestroyFromListener();
    // OnActorDestroy() is called synchronously by StartDestroyFromListener().
    // So we should have already cleared the mActor.
    MOZ_DIAGNOSTIC_ASSERT(!mActor);
  }
}

void CacheStorage::RunRequest(UniquePtr<Entry> aEntry) {
  MOZ_ASSERT(mActor);

  AutoChildOpArgs args(this, aEntry->mArgs, 1);

  if (aEntry->mRequest) {
    ErrorResult rv;
    args.Add(*aEntry->mRequest, IgnoreBody, IgnoreInvalidScheme, rv);
    if (NS_WARN_IF(rv.Failed())) {
      aEntry->mPromise->MaybeReject(std::move(rv));
      return;
    }
  }

  mActor->ExecuteOp(mGlobal, aEntry->mPromise, this, args.SendAsOpArgs());
}

OpenMode CacheStorage::GetOpenMode() const {
  return mNamespace == CHROME_ONLY_NAMESPACE ? OpenMode::Eager : OpenMode::Lazy;
}

bool CacheStorage::HasStorageAccess(UseCounter aLabel,
                                    UseCounterWorker aLabelWorker) const {
  NS_ASSERT_OWNINGTHREAD(CacheStorage);
  if (NS_WARN_IF(!mGlobal)) {
    return false;
  }

  StorageAccess access = mGlobal->GetStorageAccess();
  if (access == StorageAccess::ePrivateBrowsing) {
    if (NS_IsMainThread()) {
      SetUseCounter(mGlobal->GetGlobalJSObject(), aLabel);
    } else {
      SetUseCounter(aLabelWorker);
    }
  }

  // Deny storage access for private browsing unless pref is toggled on.
  if (nsIPrincipal* principal = mGlobal->PrincipalOrNull()) {
    if (!principal->IsSystemPrincipal() &&
        principal->GetPrivateBrowsingId() !=
            nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID &&
        !StaticPrefs::dom_cache_privateBrowsing_enabled()) {
      return false;
    }
  }

  return access > StorageAccess::eDeny || ShouldPartitionStorage(access);
}

}  // namespace mozilla::dom::cache
