// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/modules/manifest/manifest_manager.h"

#include <utility>

#include "base/functional/bind.h"
#include "base/types/expected.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom-blink.h"
#include "third_party/blink/public/mojom/manifest/manifest_manager.mojom-blink.h"
#include "third_party/blink/public/platform/interface_registry.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/frame/frame_console.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/html/html_link_element.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/modules/manifest/manifest_change_notifier.h"
#include "third_party/blink/renderer/modules/manifest/manifest_fetcher.h"
#include "third_party/blink/renderer/modules/manifest/manifest_parser.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_response.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"

namespace blink {

ManifestManager::Result::Result(mojom::blink::ManifestRequestResult result,
                                KURL manifest_url,
                                mojom::blink::ManifestPtr manifest)
    : result_(result),
      manifest_url_(manifest_url),
      manifest_(manifest ? std::move(manifest) : mojom::blink::Manifest::New()),
      debug_info_(mojom::blink::ManifestDebugInfo::New()) {
  // The default constructor for ManifestDebugInfo does not initialize
  // `raw_manifest` with a valid value, so do so here instead.
  debug_info_->raw_manifest = "";
}

ManifestManager::Result::Result(Result&&) = default;
ManifestManager::Result& ManifestManager::Result::operator=(Result&&) = default;

void ManifestManager::Result::SetManifest(mojom::blink::ManifestPtr manifest) {
  CHECK(manifest);
  manifest_ = std::move(manifest);
}

// static
const char ManifestManager::kSupplementName[] = "ManifestManager";

// static
void WebManifestManager::RequestManifestForTesting(WebLocalFrame* web_frame,
                                                   Callback callback) {
  auto* window = To<WebLocalFrameImpl>(web_frame)->GetFrame()->DomWindow();
  ManifestManager* manifest_manager = ManifestManager::From(*window);
  manifest_manager->RequestManifestForTesting(std::move(callback));
}

// static
ManifestManager* ManifestManager::From(LocalDOMWindow& window) {
  auto* manager = Supplement<LocalDOMWindow>::From<ManifestManager>(window);
  if (!manager) {
    manager = MakeGarbageCollected<ManifestManager>(window);
    Supplement<LocalDOMWindow>::ProvideTo(window, manager);
  }
  return manager;
}

ManifestManager::ManifestManager(LocalDOMWindow& window)
    : Supplement<LocalDOMWindow>(window),
      ExecutionContextLifecycleObserver(&window),
      receivers_(this, GetExecutionContext()) {
  if (window.GetFrame()->IsMainFrame()) {
    manifest_change_notifier_ =
        MakeGarbageCollected<ManifestChangeNotifier>(window);
    window.GetFrame()->GetInterfaceRegistry()->AddInterface(BindRepeating(
        &ManifestManager::BindReceiver, WrapWeakPersistent(this)));
  }
}

ManifestManager::~ManifestManager() = default;

void ManifestManager::RequestManifest(RequestManifestCallback callback) {
  RequestManifestImpl(blink::BindOnce(
      [](RequestManifestCallback callback, const Result& result) {
        std::move(callback).Run(result.result(), result.manifest_url(),
                                result.manifest().Clone());
      },
      std::move(callback)));
}

void ManifestManager::RequestManifestAndErrors(
    RequestManifestAndErrorsCallback callback) {
  RequestManifestImpl(blink::BindOnce(
      [](RequestManifestAndErrorsCallback callback, const Result& result) {
        switch (result.result()) {
          case mojom::blink::ManifestRequestResult::kManifestFailedToFetch:
          case mojom::blink::ManifestRequestResult::kManifestFailedToParse:
          case mojom::blink::ManifestRequestResult::kUnexpectedFailure:
          case mojom::blink::ManifestRequestResult::kNoManifestAllowed:
            std::move(callback).Run(
                base::unexpected(mojom::blink::RequestManifestError::New(
                    result.result(),
                    std::move(result.debug_info().Clone()->errors))));
            return;
          case mojom::blink::ManifestRequestResult::kNoManifestSpecified:
          case mojom::blink::ManifestRequestResult::kSuccess:
            std::move(callback).Run(result.manifest().Clone());
            return;
        }
      },
      std::move(callback)));
}

void ManifestManager::RequestManifestDebugInfo(
    RequestManifestDebugInfoCallback callback) {
  RequestManifestImpl(blink::BindOnce(
      [](RequestManifestDebugInfoCallback callback, const Result& result) {
        std::move(callback).Run(result.manifest_url(),
                                result.manifest().Clone(),
                                result.debug_info().Clone());
      },
      std::move(callback)));
}

void ManifestManager::ParseManifestFromString(
    const KURL& document_url,
    const KURL& manifest_url,
    const String& manifest_contents,
    ParseManifestFromStringCallback callback) {
  ManifestParser parser(manifest_contents, manifest_url, document_url,
                        GetExecutionContext());
  parser.Parse();

  mojom::blink::ManifestPtr result;
  if (!parser.failed()) {
    result = parser.TakeManifest();
  }

  std::move(callback).Run(std::move(result));
}

void ManifestManager::RequestManifestForTesting(
    WebManifestManager::Callback callback) {
  RequestManifestImpl(blink::BindOnce(
      [](WebManifestManager::Callback callback, const Result& result) {
        std::move(callback).Run(result.manifest_url());
      },
      std::move(callback)));
}

bool ManifestManager::CanFetchManifest() {
  // Do not fetch the manifest if we are on an opaque origin, or if the document
  // url is an about: url.
  return !GetSupplementable()->GetSecurityOrigin()->IsOpaque() &&
         GetSupplementable()->Url().IsValid() &&
         !GetSupplementable()->Url().ProtocolIsAbout();
}

void ManifestManager::RequestManifestImpl(
    InternalRequestManifestCallback callback) {
  if (!GetSupplementable()->GetFrame()) {
    std::move(callback).Run(
        Result(mojom::blink::ManifestRequestResult::kUnexpectedFailure));
    return;
  }

  if (cached_result_) {
    std::move(callback).Run(*cached_result_);
    return;
  }

  pending_callbacks_.push_back(std::move(callback));
  FetchManifest();
}

void ManifestManager::DidChangeManifest() {
  cached_result_.reset();
  if (manifest_change_notifier_) {
    manifest_change_notifier_->DidChangeManifest();
  }
}

void ManifestManager::FetchManifest() {
  if (!CanFetchManifest()) {
    ResolveCallbacks(
        Result(mojom::blink::ManifestRequestResult::kNoManifestAllowed,
               ManifestURL()));
    return;
  }

  LocalDOMWindow& window = *GetSupplementable();
  KURL manifest_url = ManifestURL();
  if (manifest_url.IsEmpty()) {
    ResolveCallbacks(
        Result(mojom::blink::ManifestRequestResult::kNoManifestSpecified,
               NullUrl(), DefaultManifest()));
    return;
  }

  // Do not trigger a new fetch if an existing fetch is happening for the same
  // `manifest_url`.
  if (current_fetching_manifest_url_ == manifest_url) {
    return;
  }

  current_fetching_manifest_url_ = manifest_url;
  ResourceFetcher* document_fetcher = window.document()->Fetcher();
  fetcher_ = MakeGarbageCollected<ManifestFetcher>(manifest_url);
  fetcher_->Start(window, ManifestUseCredentials(), document_fetcher,
                  BindOnce(&ManifestManager::OnManifestFetchComplete,
                           WrapWeakPersistent(this), window.Url(),
                           *current_fetching_manifest_url_));
}

void ManifestManager::OnManifestFetchComplete(
    const KURL& document_url,
    const KURL& manifest_url_for_fetch,
    const ResourceResponse& response,
    const String& data) {
  // Exit early if the manifest fetch is happening for a request belonging to a
  // stale manifest url.
  if (current_fetching_manifest_url_ != manifest_url_for_fetch) {
    return;
  }

  current_fetching_manifest_url_.reset();
  fetcher_ = nullptr;
  if (response.IsNull() && data.empty()) {
    // The only time we don't produce the default manifest is when there is a
    // resource fetching problem of the manifest link. This allows callers to
    // catch this error appropriately as a network issue instead of using a
    // 'default' manifest that wasn't intended by the developer.
    ResolveCallbacks(
        Result(mojom::blink::ManifestRequestResult::kManifestFailedToFetch,
               response.CurrentRequestUrl(), DefaultManifest()));
    return;
  }

  // 3** range, redirects, should not be considered as failure, load still can
  // be successful. Test to see this:
  // PwaInstallViewBrowserTest.ListedRelatedChromeAppInstalled
  if (response.HttpStatusCode() >= 200 && response.HttpStatusCode() < 400) {
    ParseManifestFromPage(document_url, response.CurrentRequestUrl(), data);
  } else {
    const String message = StrCat(
        {"Manifest fetch from ", response.CurrentRequestUrl().GetString(),
         " failed, code ", String::Number(response.HttpStatusCode())});

    GetSupplementable()->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
        mojom::blink::ConsoleMessageSource::kOther,
        mojom::blink::ConsoleMessageLevel::kError, message,
        CaptureSourceLocation()));

    ResolveCallbacks(
        Result(mojom::blink::ManifestRequestResult::kManifestFailedToFetch,
               response.CurrentRequestUrl(), DefaultManifest()));
  }
}

void ManifestManager::ParseManifestFromPage(const KURL& document_url,
                                            std::optional<KURL> manifest_url,
                                            const String& data) {
  CHECK(document_url.IsValid());
  // We are using the document as our FeatureContext for checking origin trials.
  // Note that any origin trials delivered in the manifest HTTP headers will be
  // ignored, only ones associated with the page will be used.
  // For default manifests, the manifest_url is `std::nullopt`, so use the
  // document_url instead for the parsing algorithm.
  ManifestParser parser(data, manifest_url.value_or(document_url), document_url,
                        GetExecutionContext());

  // Monitoring whether the manifest has comments is temporary. Once
  // warning/deprecation period is over, we should remove this as it's
  // technically incorrect JSON syntax anyway. See crbug.com/1264024
  bool has_comments = parser.Parse();
  if (has_comments) {
    UseCounter::Count(GetSupplementable(),
                      WebFeature::kWebAppManifestHasComments);
  }

  const bool failed = parser.failed();
  Result result(
      failed ? mojom::blink::ManifestRequestResult::kManifestFailedToParse
             : mojom::blink::ManifestRequestResult::kSuccess,
      manifest_url.value_or(KURL()));

  result.debug_info().raw_manifest = data.IsNull() ? "" : data;
  parser.TakeErrors(&result.debug_info().errors);

  for (const auto& error : result.debug_info().errors) {
    auto* location = MakeGarbageCollected<SourceLocation>(
        ManifestURL().GetString(), String(), error->line, error->column,
        nullptr, 0);

    GetSupplementable()->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
        mojom::blink::ConsoleMessageSource::kOther,
        error->critical ? mojom::blink::ConsoleMessageLevel::kError
                        : mojom::blink::ConsoleMessageLevel::kWarning,
        StrCat({"Manifest: ", error->message}), std::move(location)));
  }

  // Having errors while parsing the manifest doesn't mean the manifest parsing
  // failed. Some properties might have been ignored but some others kept.
  if (failed) {
    result.SetManifest(DefaultManifest());
    ResolveCallbacks(std::move(result));
    return;
  }

  result.SetManifest(parser.TakeManifest());

  // We should always have a start_url, manifest_id, and scope, as any errors
  // still have fallbacks back to the document_url.
  CHECK(!result.manifest().start_url.IsEmpty() &&
        result.manifest().start_url.IsValid());
  CHECK(!result.manifest().id.IsEmpty() && result.manifest().id.IsValid());
  CHECK(!result.manifest().scope.IsEmpty() &&
        result.manifest().scope.IsValid());

  // At this point, the manifest is validly parsed, and is not the default one.
  UseCounter::CountWebDXFeature(GetSupplementable(), WebDXFeature::kManifest);
  ResolveCallbacks(std::move(result));
}

void ManifestManager::ResolveCallbacks(Result result) {
  Vector<InternalRequestManifestCallback> callbacks;
  callbacks.swap(pending_callbacks_);

  // URLs that are too long are silently truncated by the mojo serialization.
  // Since that might violate invariants the manifest is expected to have, check
  // if any URLs would be too long and return an error instead if that is the
  // case.
  const bool has_overlong_urls =
      result.manifest().manifest_url.GetString().length() > url::kMaxURLChars ||
      result.manifest().id.GetString().length() > url::kMaxURLChars ||
      result.manifest().start_url.GetString().length() > url::kMaxURLChars ||
      result.manifest().scope.GetString().length() > url::kMaxURLChars;
  if (has_overlong_urls) {
    result = Result(mojom::blink::ManifestRequestResult::kUnexpectedFailure);
  }

  const Result* result_ptr = nullptr;
  if (result.result() == mojom::blink::ManifestRequestResult::kSuccess) {
    cached_result_ = std::move(result);
    result_ptr = &cached_result_.value();
  } else {
    result_ptr = &result;
  }

  for (auto& callback : callbacks) {
    std::move(callback).Run(*result_ptr);
  }
}

KURL ManifestManager::ManifestURL() const {
  HTMLLinkElement* link_element =
      GetSupplementable()->document()->LinkManifest();
  if (!link_element)
    return KURL();
  return link_element->Href();
}

bool ManifestManager::ManifestUseCredentials() const {
  HTMLLinkElement* link_element =
      GetSupplementable()->document()->LinkManifest();
  if (!link_element)
    return false;
  return EqualIgnoringAsciiCase(
      link_element->FastGetAttribute(html_names::kCrossoriginAttr),
      "use-credentials");
}

void ManifestManager::BindReceiver(
    mojo::PendingReceiver<mojom::blink::ManifestManager> receiver) {
  receivers_.Add(std::move(receiver),
                 GetSupplementable()->GetTaskRunner(TaskType::kNetworking));
}

mojom::blink::ManifestPtr ManifestManager::DefaultManifest() {
  // Generate the default manifest for failures, and use the current window url
  // as the manifest_url for resolving resources in the default manifest.
  LocalDOMWindow& window = *GetSupplementable();
  ManifestParser parser(/*data=*/"{ }", /*manifest_url=*/window.Url(),
                        /*document_url=*/window.Url(), GetExecutionContext());
  parser.Parse();
  CHECK(!parser.failed());
  auto result = parser.TakeManifest();
  // Reset manifest_url in the parsed manifest, as the window url isn't really
  // the url for this manifest.
  result->manifest_url = KURL();
  return result;
}

void ManifestManager::ContextDestroyed() {
  if (fetcher_)
    fetcher_->Cancel();

  // Consumers in the browser process will not receive this message but they
  // will be aware of the RenderFrame dying and should act on that. Consumers
  // in the renderer process should be correctly notified.
  ResolveCallbacks(
      Result(mojom::blink::ManifestRequestResult::kUnexpectedFailure));
}

void ManifestManager::Trace(Visitor* visitor) const {
  visitor->Trace(fetcher_);
  visitor->Trace(manifest_change_notifier_);
  visitor->Trace(receivers_);
  Supplement<LocalDOMWindow>::Trace(visitor);
  ExecutionContextLifecycleObserver::Trace(visitor);
}

}  // namespace blink
