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

#include "components/payments/content/installable_payment_app_crawler.h"

#include <limits>
#include <utility>

#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/payments/content/icon/icon_size.h"
#include "components/payments/core/features.h"
#include "components/payments/core/native_error_strings.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/manifest_icon_downloader.h"
#include "content/public/browser/payment_app_provider_util.h"
#include "content/public/browser/permission_controller.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/permission_result.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "third_party/blink/public/common/manifest/manifest_icon_selector.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "ui/gfx/geometry/size.h"
#include "url/gurl.h"

namespace payments {

RefetchedMetadata::RefetchedMetadata() = default;
RefetchedMetadata::~RefetchedMetadata() = default;

// TODO(crbug.com/40548519): Use cache to accelerate crawling procedure.
InstallablePaymentAppCrawler::InstallablePaymentAppCrawler(
    const url::Origin& merchant_origin,
    content::RenderFrameHost* initiator_render_frame_host,
    PaymentManifestDownloader* downloader,
    PaymentManifestParser* parser,
    WebPaymentsWebDataService* cache)
    : log_(content::WebContents::FromRenderFrameHost(
          initiator_render_frame_host)),
      merchant_origin_(merchant_origin),
      initiator_frame_routing_id_(initiator_render_frame_host->GetGlobalId()),
      downloader_(downloader),
      parser_(parser),
      number_of_payment_method_manifest_to_download_(0),
      number_of_web_app_manifest_to_download_(0),
      number_of_web_app_icons_to_download_and_decode_(0) {}

InstallablePaymentAppCrawler::~InstallablePaymentAppCrawler() = default;

void InstallablePaymentAppCrawler::Start(
    const std::vector<mojom::PaymentMethodDataPtr>& requested_method_data,
    std::set<GURL> method_manifest_urls_for_metadata_refresh,
    FinishedCrawlingCallback callback,
    base::OnceClosure finished_using_resources) {
  callback_ = std::move(callback);
  finished_using_resources_ = std::move(finished_using_resources);

  std::set<GURL> manifests_to_download;
  if (method_manifest_urls_for_metadata_refresh.empty()) {
    // Crawl for JIT installable web apps.
    crawling_mode_ = CrawlingMode::kJustInTimeInstallation;
    for (const auto& method_data : requested_method_data) {
      if (!base::IsStringUTF8(method_data->supported_method))
        continue;
      GURL url = GURL(method_data->supported_method);
      if (url.is_valid()) {
        manifests_to_download.insert(url);
      }
    }
  } else {
    // Crawl to refresh metadata for already installed apps.
    crawling_mode_ = CrawlingMode::kInstalledAppMetadataRefresh;
    method_manifest_urls_for_metadata_refresh_ =
        std::move(method_manifest_urls_for_metadata_refresh);
    for (const auto& method : method_manifest_urls_for_metadata_refresh_) {
      DCHECK(method.is_valid());
      manifests_to_download.insert(method);
    }
  }

  if (manifests_to_download.empty()) {
    // Post the result back asynchronously.
    PostTaskToFinishCrawlingPaymentAppsIfReady();
    return;
  }

  // May cause this InstallablePaymentAppCrawler object to be synchronously
  // deleted in the last iteration, so no code should come after the loop.
  number_of_payment_method_manifest_to_download_ = manifests_to_download.size();
  for (const auto& url : manifests_to_download) {
    downloader_->DownloadPaymentMethodManifest(
        merchant_origin_, url,
        base::BindOnce(
            &InstallablePaymentAppCrawler::OnPaymentMethodManifestDownloaded,
            weak_ptr_factory_.GetWeakPtr(), url));
  }
}

void InstallablePaymentAppCrawler::IgnorePortInOriginComparisonForTesting() {
  ignore_port_in_origin_comparison_for_testing_ = true;
}

bool InstallablePaymentAppCrawler::IsSameOriginWith(const GURL& a,
                                                    const GURL& b) {
  if (ignore_port_in_origin_comparison_for_testing_) {
    GURL::Replacements replacements;
    replacements.ClearPort();
    return url::IsSameOriginWith(a.ReplaceComponents(replacements),
                                 b.ReplaceComponents(replacements));
  }
  return url::IsSameOriginWith(a, b);
}

void InstallablePaymentAppCrawler::OnPaymentMethodManifestDownloaded(
    const GURL& method_manifest_url,
    const GURL& method_manifest_url_after_redirects,
    const std::string& content,
    const std::string& error_message) {
  // Enforced in PaymentManifestDownloader.
  DCHECK(net::registry_controlled_domains::SameDomainOrHost(
      method_manifest_url, method_manifest_url_after_redirects,
      net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES));

  number_of_payment_method_manifest_to_download_--;
  if (content.empty()) {
    SetFirstError(error_message);
    FinishCrawlingPaymentAppsIfReady();
    return;
  }

  auto* rfh = content::RenderFrameHost::FromID(initiator_frame_routing_id_);
  if (!rfh)
    return;

  std::vector<GURL> default_applications;
  std::vector<url::Origin> supported_origins;
  parser_->ParsePaymentMethodManifest(method_manifest_url_after_redirects,
                                      content, &default_applications,
                                      &supported_origins);

  content::PermissionController* permission_controller =
      rfh->GetBrowserContext()->GetPermissionController();
  DCHECK(permission_controller);

  // If there are no valid entries in default_applications, this task will
  // finish the crawling.
  PostTaskToFinishCrawlingPaymentAppsIfReady();

  // The `DownloadWebAppManifest()` method may synchronously call
  // `OnPaymentWebAppManifestDownloaded()`, e.g., if the owning page has gone
  // away already. This may result in this InstallablePaymentAppCrawler object
  // to be deleted, so no code should be run after this loop.
  //
  // Note that only the last iteration of the loop can result in a deletion, as
  // `number_of_web_app_manifest_to_download_` must be zero for this to happen.
  number_of_web_app_manifest_to_download_ += default_applications.size();
  for (const auto& web_app_manifest_url : default_applications) {
    if (downloaded_web_app_manifests_.find(web_app_manifest_url) !=
        downloaded_web_app_manifests_.end()) {
      // Do not download the same web app manifest again since a web app could
      // be the default application of multiple payment methods.
      number_of_web_app_manifest_to_download_--;
      continue;
    }

    if (!IsSameOriginWith(method_manifest_url_after_redirects,
                          web_app_manifest_url)) {
      number_of_web_app_manifest_to_download_--;
      std::string cross_origin_error_message = base::ReplaceStringPlaceholders(
          errors::kCrossOriginWebAppManifestNotAllowed,
          {web_app_manifest_url.spec(),
           method_manifest_url_after_redirects.spec()},
          nullptr);
      SetFirstError(cross_origin_error_message);
      continue;
    }

    if (permission_controller
            ->GetPermissionResultForOriginWithoutContext(
                content::PermissionDescriptorUtil::
                    CreatePermissionDescriptorForPermissionType(
                        blink::PermissionType::PAYMENT_HANDLER),
                url::Origin::Create(web_app_manifest_url))
            .status != blink::mojom::PermissionStatus::GRANTED) {
      // Do not download the web app manifest if it is blocked.
      number_of_web_app_manifest_to_download_--;
      continue;
    }

    downloaded_web_app_manifests_.insert(web_app_manifest_url);

    if (method_manifest_url_after_redirects == web_app_manifest_url) {
      // For this to happen, the payment manifest must have been valid which
      // means its content should have been non-empty. If somehow we get here
      // but content is empty, it would cause a synchronous deletion of 'this',
      // so guard against that.
      CHECK(!content.empty());
      OnPaymentWebAppManifestDownloaded(
          method_manifest_url, web_app_manifest_url, web_app_manifest_url,
          content, /*error_message=*/"");
      continue;
    }

    // May cause this InstallablePaymentAppCrawler object to be synchronously
    // deleted in the last iteration of the loop.
    downloader_->DownloadWebAppManifest(
        url::Origin::Create(method_manifest_url_after_redirects),
        web_app_manifest_url,
        base::BindOnce(
            &InstallablePaymentAppCrawler::OnPaymentWebAppManifestDownloaded,
            weak_ptr_factory_.GetWeakPtr(), method_manifest_url,
            web_app_manifest_url));
  }
}

void InstallablePaymentAppCrawler::OnPaymentWebAppManifestDownloaded(
    const GURL& method_manifest_url,
    const GURL& web_app_manifest_url,
    const GURL& web_app_manifest_url_after_redirects,
    const std::string& content,
    const std::string& error_message) {
#if DCHECK_IS_ON()
  GURL::Replacements replacements;
  if (ignore_port_in_origin_comparison_for_testing_)
    replacements.ClearPort();

  // Enforced in PaymentManifestDownloader.
  DCHECK_EQ(
      web_app_manifest_url.ReplaceComponents(replacements),
      web_app_manifest_url_after_redirects.ReplaceComponents(replacements));
#endif  // DCHECK_IS_ON()

  number_of_web_app_manifest_to_download_--;
  if (content.empty()) {
    SetFirstError(error_message);
    FinishCrawlingPaymentAppsIfReady();
    return;
  }

  PaymentManifestParser::WebAppInstallationInfoResult result =
      parser_->ParseWebAppInstallationInfo(content);

  // Only download and decode payment app's icon if it is valid and stored.
  if (CompleteAndStorePaymentWebAppInfoIfValid(
          method_manifest_url, web_app_manifest_url,
          std::move(result.installation_info))) {
    if (!DownloadAndDecodeWebAppIcon(method_manifest_url, web_app_manifest_url,
                                     std::move(result.icons)) &&
        crawling_mode_ == CrawlingMode::kJustInTimeInstallation &&
        !base::FeatureList::IsEnabled(
            features::kAllowJITInstallationWhenAppIconIsMissing)) {
      std::string icon_error_message = base::ReplaceStringPlaceholders(
          errors::kInvalidWebAppIcon, {web_app_manifest_url.spec()}, nullptr);
      SetFirstError(icon_error_message);
      // App without a valid icon is not JIT installable.
      installable_apps_.erase(method_manifest_url);
    }
  }

  FinishCrawlingPaymentAppsIfReady();
}

bool InstallablePaymentAppCrawler::CompleteAndStorePaymentWebAppInfoIfValid(
    const GURL& method_manifest_url,
    const GURL& web_app_manifest_url,
    std::unique_ptr<WebAppInstallationInfo> app_info) {
  if (app_info == nullptr)
    return false;

  if (app_info->sw_js_url.empty() || !base::IsStringUTF8(app_info->sw_js_url)) {
    SetFirstError(errors::kInvalidServiceWorkerUrl);
    return false;
  }

  // Check and complete relative url.
  if (!GURL(app_info->sw_js_url).is_valid()) {
    GURL absolute_url = web_app_manifest_url.Resolve(app_info->sw_js_url);
    if (!absolute_url.is_valid()) {
      SetFirstError(base::ReplaceStringPlaceholders(
          errors::kCannotResolveServiceWorkerUrl,
          {app_info->sw_js_url, web_app_manifest_url.spec()}, nullptr));
      return false;
    }
    app_info->sw_js_url = absolute_url.spec();
  }

  if (!IsSameOriginWith(web_app_manifest_url, GURL(app_info->sw_js_url))) {
    SetFirstError(base::ReplaceStringPlaceholders(
        errors::kCrossOriginServiceWorkerUrlNotAllowed,
        {app_info->sw_js_url, web_app_manifest_url.spec()}, nullptr));
    return false;
  }

  if (!app_info->sw_scope.empty() && !base::IsStringUTF8(app_info->sw_scope)) {
    SetFirstError(errors::kInvalidServiceWorkerScope);
    return false;
  }

  if (!GURL(app_info->sw_scope).is_valid()) {
    GURL absolute_scope =
        web_app_manifest_url.GetWithoutFilename().Resolve(app_info->sw_scope);
    if (!absolute_scope.is_valid()) {
      SetFirstError(base::ReplaceStringPlaceholders(
          errors::kCannotResolveServiceWorkerScope,
          {app_info->sw_scope, web_app_manifest_url.spec()}, nullptr));
      return false;
    }
    app_info->sw_scope = absolute_scope.spec();
  }

  if (!IsSameOriginWith(web_app_manifest_url, GURL(app_info->sw_scope))) {
    SetFirstError(base::ReplaceStringPlaceholders(
        errors::kCrossOriginServiceWorkerScopeNotAllowed,
        {app_info->sw_scope, web_app_manifest_url.spec()}, nullptr));
    return false;
  }

  std::string error_message;
  if (!content::PaymentAppProviderUtil::IsValidInstallablePaymentApp(
          web_app_manifest_url, GURL(app_info->sw_js_url),
          GURL(app_info->sw_scope), &error_message)) {
    SetFirstError(error_message);
    return false;
  }

  // TODO(crbug.com/40548519): Support multiple installable payment apps for a
  // payment method.
  if (installable_apps_.find(method_manifest_url) != installable_apps_.end()) {
    SetFirstError(errors::kInstallingMultipleDefaultAppsNotSupported);
    return false;
  }

  // If this is called by tests, convert service worker URLs back to test server
  // URLs so the service worker code can be fetched and installed. This is done
  // last so same-origin checks are performed on the "logical" URLs instead of
  // real test URLs with ports.
  if (ignore_port_in_origin_comparison_for_testing_) {
    app_info->sw_js_url =
        downloader_->FindTestServerURL(GURL(app_info->sw_js_url)).spec();
    app_info->sw_scope =
        downloader_->FindTestServerURL(GURL(app_info->sw_scope)).spec();
  }

  switch (crawling_mode_) {
    case CrawlingMode::kJustInTimeInstallation:
      installable_apps_[method_manifest_url] = std::move(app_info);
      break;
    case CrawlingMode::kInstalledAppMetadataRefresh: {
      auto refetched_metadata = std::make_unique<RefetchedMetadata>();
      refetched_metadata->method_name = method_manifest_url.spec();
      refetched_metadata->supported_delegations =
          app_info->supported_delegations;
      refetched_app_metadata_.insert(
          std::make_pair(web_app_manifest_url, std::move(refetched_metadata)));
      break;
    }
  }

  return true;
}

bool InstallablePaymentAppCrawler::DownloadAndDecodeWebAppIcon(
    const GURL& method_manifest_url,
    const GURL& web_app_manifest_url,
    std::unique_ptr<std::vector<PaymentManifestParser::WebAppIcon>> icons) {
  if (icons == nullptr || icons->empty()) {
    log_.Warn(base::StrCat(
        {"No valid icon information for installable payment handler found in "
         "web app manifest \"",
         web_app_manifest_url.spec(), "\" for payment handler manifest \"",
         method_manifest_url.spec(), "\"."}));
    return false;
  }

  std::vector<blink::Manifest::ImageResource> manifest_icons;
  for (const auto& icon : *icons) {
    if (icon.src.empty() || !base::IsStringUTF8(icon.src)) {
      log_.Warn(base::StrCat(
          {"The installable payment handler's icon src URL is not a non-empty "
           "UTF8 string in web app manifest \"",
           web_app_manifest_url.spec(), "\" for payment handler manifest \"",
           method_manifest_url.spec(), "\"."}));
      continue;
    }

    GURL icon_src = GURL(icon.src);
    if (!icon_src.is_valid()) {
      icon_src = web_app_manifest_url.Resolve(icon.src);
      if (!icon_src.is_valid()) {
        log_.Warn(base::StrCat(
            {"Failed to resolve the installable payment handler's icon src url "
             "\"",
             icon.src, "\" in web app manifest \"", web_app_manifest_url.spec(),
             "\" for payment handler manifest \"", method_manifest_url.spec(),
             "\"."}));
        continue;
      }
    }

    blink::Manifest::ImageResource manifest_icon;
    manifest_icon.src = icon_src;
    manifest_icon.type = base::UTF8ToUTF16(icon.type);
    manifest_icon.purpose.emplace_back(
        blink::mojom::ManifestImageResource_Purpose::ANY);
    // TODO(crbug.com/40548519): Parse icon sizes.
    manifest_icon.sizes.emplace_back(gfx::Size());
    manifest_icons.emplace_back(manifest_icon);
  }

  if (manifest_icons.empty()) {
    log_.Warn(base::StrCat({"No valid icons found in web app manifest \"" +
                                web_app_manifest_url.spec(),
                            "\" for payment handler manifest \"",
                            method_manifest_url.spec(), "\"."}));
    return false;
  }

  // If the initiator frame doesn't exists any more, e.g. the frame has
  // navigated away, don't download the icon.
  // TODO(crbug.com/40121328): Move this sanity check to ManifestIconDownloader
  // after DownloadImage refactor is done.
  auto* rfh = content::RenderFrameHost::FromID(initiator_frame_routing_id_);
  auto* web_contents = rfh && rfh->IsActive()
                           ? content::WebContents::FromRenderFrameHost(rfh)
                           : nullptr;
  if (!web_contents) {
    log_.Warn(base::StrCat(
        {"Cannot download icons after the webpage has been closed (web app "
         "manifest \"",
         web_app_manifest_url.spec(), "\" for payment handler manifest \"",
         method_manifest_url.spec(), "\")."}));
    // Post the result back asynchronously.
    PostTaskToFinishCrawlingPaymentAppsIfReady();
    return false;
  }

  gfx::NativeView native_view = web_contents->GetNativeView();
  blink::ManifestIconSelectorParams params;
  params.ideal_icon_size_in_px =
      IconSizeCalculator::IdealIconHeight(native_view);
  params.minimum_icon_size_in_px = IconSizeCalculator::MinimumIconHeight();
  params.max_width_to_height_ratio =
      content::ManifestIconDownloader::kMaxWidthToHeightRatio;
  params.purpose = blink::mojom::ManifestImageResource_Purpose::ANY;
  std::optional<blink::ManifestIconSelectorResult> result =
      blink::ManifestIconSelector::FindBestMatchingIcon(manifest_icons, params);
  if (!result) {
    log_.Warn(base::StrCat({"No suitable icon found in web app manifest \"",
                            web_app_manifest_url.spec(),
                            "\" for payment handler manifest \"",
                            method_manifest_url.spec(), "\"."}));
    return false;
  }
  GURL best_icon_url = result->icon_url;

  number_of_web_app_icons_to_download_and_decode_++;

  bool can_download_icon = content::ManifestIconDownloader::Download(
      web_contents, downloader_->FindTestServerURL(best_icon_url),
      IconSizeCalculator::IdealIconHeight(native_view),
      IconSizeCalculator::MinimumIconHeight(),
      /* maximum_icon_size_in_px= */ std::numeric_limits<int>::max(),
      base::BindOnce(
          &InstallablePaymentAppCrawler::OnPaymentWebAppIconDownloadAndDecoded,
          weak_ptr_factory_.GetWeakPtr(), method_manifest_url,
          web_app_manifest_url),
      false, /* square_only */
      initiator_frame_routing_id_);
  DCHECK(can_download_icon);
  return can_download_icon;
}

void InstallablePaymentAppCrawler::OnPaymentWebAppIconDownloadAndDecoded(
    const GURL& method_manifest_url,
    const GURL& web_app_manifest_url,
    const SkBitmap& icon) {
  number_of_web_app_icons_to_download_and_decode_--;

  switch (crawling_mode_) {
    case CrawlingMode::kJustInTimeInstallation: {
      auto it = installable_apps_.find(method_manifest_url);
      CHECK(it != installable_apps_.end());
      DCHECK(
          IsSameOriginWith(GURL(it->second->sw_scope), web_app_manifest_url));
      if (icon.drawsNothing() &&
          !base::FeatureList::IsEnabled(
              features::kAllowJITInstallationWhenAppIconIsMissing)) {
        log_.Error(base::StrCat(
            {"Failed to download or decode the icon from web app manifest \"",
             web_app_manifest_url.spec(), "\" for payment handler manifest \"",
             method_manifest_url.spec(), "\"."}));
        std::string error_message = base::ReplaceStringPlaceholders(
            errors::kInvalidWebAppIcon, {web_app_manifest_url.spec()}, nullptr);
        SetFirstError(error_message);
        installable_apps_.erase(it);
      } else {
        it->second->icon = std::make_unique<SkBitmap>(icon);
      }
      break;
    }
    case CrawlingMode::kInstalledAppMetadataRefresh: {
      auto it =
          method_manifest_urls_for_metadata_refresh_.find(method_manifest_url);
      CHECK(it != method_manifest_urls_for_metadata_refresh_.end());
      if (icon.drawsNothing()) {
        log_.Warn(base::StrCat(
            {"Failed to refetch a valid icon from web app manifest \"",
             web_app_manifest_url.spec(), "\" for payment handler manifest \"",
             method_manifest_url.spec(), "\"."}));
      } else {
        auto refetched_app_metadata_it =
            refetched_app_metadata_.find(web_app_manifest_url);
        CHECK(refetched_app_metadata_it != refetched_app_metadata_.end());
        refetched_app_metadata_it->second->icon =
            std::make_unique<SkBitmap>(icon);
      }
      break;
    }
  }

  FinishCrawlingPaymentAppsIfReady();
}

void InstallablePaymentAppCrawler::
    PostTaskToFinishCrawlingPaymentAppsIfReady() {
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          &InstallablePaymentAppCrawler::FinishCrawlingPaymentAppsIfReady,
          weak_ptr_factory_.GetWeakPtr()));
}

void InstallablePaymentAppCrawler::FinishCrawlingPaymentAppsIfReady() {
  if (number_of_payment_method_manifest_to_download_ != 0 ||
      number_of_web_app_manifest_to_download_ != 0 ||
      number_of_web_app_icons_to_download_and_decode_ != 0) {
    return;
  }

  std::move(callback_).Run(std::move(installable_apps_),
                           std::move(refetched_app_metadata_),
                           first_error_message_);
  std::move(finished_using_resources_).Run();
}

void InstallablePaymentAppCrawler::SetFirstError(
    const std::string& error_message) {
  log_.Error(error_message);
  if (first_error_message_.empty())
    first_error_message_ = error_message;
}

}  // namespace payments.
