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

#include "FetchPreloader.h"
#include "PreloaderBase.h"
#include "mozilla/Assertions.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/dom/FetchPriority.h"
#include "mozilla/dom/HTMLLinkElement.h"
#include "mozilla/dom/ScriptLoader.h"
#include "mozilla/dom/ReferrerInfo.h"
#include "mozilla/Encoding.h"
#include "mozilla/FontPreloader.h"
#include "mozilla/StaticPrefs_network.h"
#include "nsGenericHTMLElement.h"
#include "nsNetUtil.h"

namespace mozilla {

using namespace dom;

static LazyLogModule sPreloadServiceLog{"PreloadService"};

PreloadService::PreloadService(dom::Document* aDoc) : mDocument(aDoc) {}
PreloadService::~PreloadService() = default;

bool PreloadService::RegisterPreload(const PreloadHashKey& aKey,
                                     PreloaderBase* aPreload) {
  return mPreloads.WithEntryHandle(aKey, [&](auto&& lookup) {
    if (lookup) {
      lookup.Data() = aPreload;
      return true;
    }
    lookup.Insert(aPreload);
    return false;
  });
}

void PreloadService::DeregisterPreload(const PreloadHashKey& aKey) {
  mPreloads.Remove(aKey);
}

void PreloadService::ClearAllPreloads() { mPreloads.Clear(); }

bool PreloadService::PreloadExists(const PreloadHashKey& aKey) {
  return mPreloads.Contains(aKey);
}

already_AddRefed<PreloaderBase> PreloadService::LookupPreload(
    const PreloadHashKey& aKey) const {
  return mPreloads.Get(aKey);
}

already_AddRefed<nsIURI> PreloadService::GetPreloadURI(const nsAString& aURL) {
  nsIURI* base = BaseURIForPreload();
  auto encoding = mDocument->GetDocumentCharacterSet();

  nsCOMPtr<nsIURI> uri;
  nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, encoding, base);
  if (NS_FAILED(rv)) {
    return nullptr;
  }

  return uri.forget();
}

already_AddRefed<PreloaderBase> PreloadService::PreloadLinkElement(
    dom::HTMLLinkElement* aLinkElement, nsContentPolicyType aPolicyType) {
  if (aPolicyType == nsIContentPolicy::TYPE_INVALID) {
    MOZ_ASSERT_UNREACHABLE("Caller should check");
    return nullptr;
  }

  nsAutoString as, charset, crossOrigin, integrity, referrerPolicy,
      fetchPriority, rel, srcset, sizes, type, url;

  nsCOMPtr<nsIURI> uri = aLinkElement->GetURI();
  aLinkElement->GetCharset(charset);
  aLinkElement->GetImageSrcset(srcset);
  aLinkElement->GetImageSizes(sizes);
  aLinkElement->GetHref(url);
  aLinkElement->GetCrossOrigin(crossOrigin);
  aLinkElement->GetReferrerPolicy(referrerPolicy);
  aLinkElement->GetFetchPriority(fetchPriority);
  aLinkElement->GetRel(rel);

  aLinkElement->GetIntegrity(integrity);
  integrity =
      aLinkElement->HasAttr(nsGkAtoms::integrity) ? integrity : VoidString();

  // rel=compression-dictionary fetches default to "anonymous" if no
  // crossorigin=foo parameter is given
  if (rel.LowerCaseEqualsASCII("compression-dictionary") &&
      crossOrigin.IsEmpty()) {
    crossOrigin = u"anonymous"_ns;
  }

  nsAutoString nonce;
  if (nsString* cspNonce =
          static_cast<nsString*>(aLinkElement->GetProperty(nsGkAtoms::nonce))) {
    nonce = *cspNonce;
  }

  if (rel.LowerCaseEqualsASCII("modulepreload")) {
    as = u"script"_ns;
    type = u"module"_ns;
  } else {
    aLinkElement->GetAs(as);
    aLinkElement->GetType(type);
  }

  auto result = PreloadOrCoalesce(uri, url, aPolicyType, as, rel, type, charset,
                                  srcset, sizes, nonce, integrity, crossOrigin,
                                  referrerPolicy, fetchPriority,
                                  /* aFromHeader = */ false, 0);

  if (!result.mPreloader) {
    NotifyNodeEvent(aLinkElement, result.mAlreadyComplete);
    return nullptr;
  }

  result.mPreloader->AddLinkPreloadNode(aLinkElement);
  return result.mPreloader.forget();
}

void PreloadService::PreloadLinkHeader(
    nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType,
    const nsAString& aAs, const nsAString& aRel, const nsAString& aType,
    const nsAString& aNonce, const nsAString& aIntegrity,
    const nsAString& aSrcset, const nsAString& aSizes, const nsAString& aCORS,
    const nsAString& aReferrerPolicy, uint64_t aEarlyHintPreloaderId,
    const nsAString& aFetchPriority) {
  if (aPolicyType == nsIContentPolicy::TYPE_INVALID) {
    MOZ_ASSERT_UNREACHABLE("Caller should check");
    return;
  }

  // rel=compression-dictionary fetches default to "anonymous" if no
  // crossorigin=foo parameter is given

  PreloadOrCoalesce(
      aURI, aURL, aPolicyType, aAs, aRel, aType, u""_ns, aSrcset, aSizes,
      aNonce, aIntegrity,
      aRel.LowerCaseEqualsASCII("compression-dictionary") && aCORS.IsEmpty()
          ? u"anonymous"_ns
          : aCORS,
      aReferrerPolicy, aFetchPriority,
      /* aFromHeader = */ true, aEarlyHintPreloaderId);
}

// The mapping is specified as implementation-defined, see step 15 of
// <https://fetch.spec.whatwg.org/#concept-fetch>.
// See corresponding preferences in StaticPrefList.yaml for more context.
class SupportsPriorityValueFor {
 public:
  static int32_t LinkRelPreloadFont(const FetchPriority aFetchPriority) {
    int32_t priorityValue = nsISupportsPriority::PRIORITY_HIGH;
    if (!StaticPrefs::network_fetchpriority_enabled()) {
      return priorityValue;
    }

    return priorityValue +
           FETCH_PRIORITY_ADJUSTMENT_FOR(link_preload_font, aFetchPriority);
  }

  static int32_t LinkRelPreloadFetch(const FetchPriority aFetchPriority) {
    int32_t priorityValue = nsISupportsPriority::PRIORITY_NORMAL;
    if (!StaticPrefs::network_fetchpriority_enabled()) {
      return priorityValue;
    }

    return priorityValue +
           FETCH_PRIORITY_ADJUSTMENT_FOR(link_preload_fetch, aFetchPriority);
  }
};

PreloadService::PreloadOrCoalesceResult PreloadService::PreloadOrCoalesce(
    nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType,
    const nsAString& aAs, const nsAString& aRel, const nsAString& aType,
    const nsAString& aCharset, const nsAString& aSrcset,
    const nsAString& aSizes, const nsAString& aNonce,
    const nsAString& aIntegrity, const nsAString& aCORS,
    const nsAString& aReferrerPolicy, const nsAString& aFetchPriority,
    bool aFromHeader, uint64_t aEarlyHintPreloaderId) {
  if (!aURI &&
      !(aPolicyType == nsIContentPolicy::TYPE_IMAGE && !aSrcset.IsEmpty())) {
    MOZ_ASSERT_UNREACHABLE("Should not pass null nsIURI");
    return {nullptr, false};
  }

  bool isImgSet = false;
  PreloadHashKey preloadKey;
  nsCOMPtr<nsIURI> uri = aURI;

  if (aAs.LowerCaseEqualsASCII("script")) {
    preloadKey = PreloadHashKey::CreateAsScript(uri, aCORS, aType);
  } else if (aAs.LowerCaseEqualsASCII("style")) {
    preloadKey = PreloadHashKey::CreateAsStyle(
        uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS));
  } else if (aAs.LowerCaseEqualsASCII("image")) {
    uri = mDocument->ResolvePreloadImage(BaseURIForPreload(), aURL, aSrcset,
                                         aSizes, &isImgSet);
    if (!uri) {
      return {nullptr, false};
    }

    preloadKey = PreloadHashKey::CreateAsImage(
        uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS));
  } else if (aAs.LowerCaseEqualsASCII("font")) {
    preloadKey = PreloadHashKey::CreateAsFont(
        uri, dom::Element::StringToCORSMode(aCORS));
  } else if (aAs.LowerCaseEqualsASCII("fetch")) {
    preloadKey = PreloadHashKey::CreateAsFetch(
        uri, dom::Element::StringToCORSMode(aCORS));
  } else if (aRel.LowerCaseEqualsASCII("compression-dictionary")) {
    // compression-dictionary doesn't specify an 'as=' value
    preloadKey = PreloadHashKey::CreateAsFetch(
        uri, dom::Element::StringToCORSMode(aCORS));
  } else {
    return {nullptr, false};
  }

  if (RefPtr<PreloaderBase> preload = LookupPreload(preloadKey)) {
    return {std::move(preload), false};
  }

  if (aAs.LowerCaseEqualsASCII("script")) {
    PreloadScript(uri, aType, aCharset, aCORS, aReferrerPolicy, aNonce,
                  aFetchPriority, aIntegrity, true /* isInHead - TODO */,
                  aEarlyHintPreloaderId);
  } else if (aAs.LowerCaseEqualsASCII("style")) {
    const auto preloadKind = [&] {
      if (aEarlyHintPreloaderId) {
        MOZ_ASSERT(aFromHeader);
        return css::StylePreloadKind::FromEarlyHintsHeader;
      }
      return aFromHeader ? css::StylePreloadKind::FromLinkRelPreloadHeader
                         : css::StylePreloadKind::FromLinkRelPreloadElement;
    }();
    auto status = mDocument->PreloadStyle(
        aURI, Encoding::ForLabel(aCharset), aCORS,
        PreloadReferrerPolicy(aReferrerPolicy), aNonce, aIntegrity, preloadKind,
        aEarlyHintPreloaderId, aFetchPriority);
    switch (status) {
      case dom::SheetPreloadStatus::AlreadyComplete:
        return {nullptr, /* already_complete = */ true};
      case dom::SheetPreloadStatus::Errored:
      case dom::SheetPreloadStatus::InProgress:
        break;
    }
  } else if (aAs.LowerCaseEqualsASCII("image")) {
    PreloadImage(uri, aCORS, aReferrerPolicy, isImgSet, aEarlyHintPreloaderId,
                 aFetchPriority);
  } else if (aAs.LowerCaseEqualsASCII("font")) {
    PreloadFont(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId,
                aFetchPriority);
  } else if (aAs.LowerCaseEqualsASCII("fetch") ||
             aRel.LowerCaseEqualsASCII("compression-dictionary")) {
    PreloadFetch(uri, aCORS, aReferrerPolicy, aEarlyHintPreloaderId,
                 aFetchPriority);
  }

  RefPtr<PreloaderBase> preload = LookupPreload(preloadKey);
  if (preload && aEarlyHintPreloaderId) {
    preload->SetForEarlyHints();
  }

  return {preload, false};
}

void PreloadService::PreloadScript(
    nsIURI* aURI, const nsAString& aType, const nsAString& aCharset,
    const nsAString& aCrossOrigin, const nsAString& aReferrerPolicy,
    const nsAString& aNonce, const nsAString& aFetchPriority,
    const nsAString& aIntegrity, bool aScriptFromHead,
    uint64_t aEarlyHintPreloaderId) {
  if (ScriptLoader* scriptLoader = mDocument->GetScriptLoader()) {
    scriptLoader->PreloadURI(
        aURI, aCharset, aType, aCrossOrigin, aNonce, aFetchPriority, aIntegrity,
        aScriptFromHead, false, false, true,
        PreloadReferrerPolicy(aReferrerPolicy), aEarlyHintPreloaderId);
  }
}

void PreloadService::PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin,
                                  const nsAString& aImageReferrerPolicy,
                                  bool aIsImgSet,
                                  uint64_t aEarlyHintPreloaderId,
                                  const nsAString& aFetchPriority) {
  mDocument->PreLoadImage(
      aURI, aCrossOrigin, PreloadReferrerPolicy(aImageReferrerPolicy),
      aIsImgSet, true, aEarlyHintPreloaderId, aFetchPriority);
}

void PreloadService::PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin,
                                 const nsAString& aReferrerPolicy,
                                 uint64_t aEarlyHintPreloaderId,
                                 const nsAString& aFetchPriority) {
  CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin);
  auto key = PreloadHashKey::CreateAsFont(aURI, cors);

  if (PreloadExists(key)) {
    return;
  }

  const auto fetchPriority =
      nsGenericHTMLElement::ToFetchPriority(aFetchPriority);
  const auto supportsPriorityValue =
      SupportsPriorityValueFor::LinkRelPreloadFont(fetchPriority);
  LogPriorityMapping(sPreloadServiceLog, fetchPriority, supportsPriorityValue);

  RefPtr preloader = MakeRefPtr<FontPreloader>();
  dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy);
  preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument,
                         aEarlyHintPreloaderId, supportsPriorityValue);
}

void PreloadService::PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin,
                                  const nsAString& aReferrerPolicy,
                                  uint64_t aEarlyHintPreloaderId,
                                  const nsAString& aFetchPriority) {
  CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin);
  auto key = PreloadHashKey::CreateAsFetch(aURI, cors);

  if (PreloadExists(key)) {
    return;
  }

  RefPtr preloader = MakeRefPtr<FetchPreloader>();
  dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy);

  const auto fetchPriority =
      nsGenericHTMLElement::ToFetchPriority(aFetchPriority);
  const int32_t supportsPriorityValue =
      SupportsPriorityValueFor::LinkRelPreloadFetch(fetchPriority);
  if (supportsPriorityValue) {
    LogPriorityMapping(sPreloadServiceLog, fetchPriority,
                       supportsPriorityValue);
  }

  preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument,
                         aEarlyHintPreloaderId, supportsPriorityValue);
}

// static
void PreloadService::NotifyNodeEvent(nsINode* aNode, bool aSuccess) {
  if (!aNode->IsInComposedDoc()) {
    return;
  }

  // We don't dispatch synchronously since |node| might be in a DocGroup
  // that we're not allowed to touch. (Our network request happens in the
  // DocGroup of one of the mSources nodes--not necessarily this one).

  RefPtr dispatcher = MakeRefPtr<AsyncEventDispatcher>(
      aNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo);

  dispatcher->RequireNodeInDocument();
  dispatcher->PostDOMEvent();
}

dom::ReferrerPolicy PreloadService::PreloadReferrerPolicy(
    const nsAString& aReferrerPolicy) {
  dom::ReferrerPolicy referrerPolicy =
      dom::ReferrerInfo::ReferrerPolicyAttributeFromString(aReferrerPolicy);
  if (referrerPolicy == dom::ReferrerPolicy::_empty) {
    referrerPolicy = mDocument->GetPreloadReferrerInfo()->ReferrerPolicy();
  }

  return referrerPolicy;
}

nsIURI* PreloadService::BaseURIForPreload() {
  nsIURI* documentURI = mDocument->GetDocumentURI();
  nsIURI* documentBaseURI = mDocument->GetDocBaseURI();
  return (documentURI == documentBaseURI)
             ? (mSpeculationBaseURI ? mSpeculationBaseURI.get() : documentURI)
             : documentBaseURI;
}

}  // namespace mozilla
