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

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

#include <stdint.h>
#include <algorithm>
#include <utility>

#include "base/bind.h"
#include "base/logging.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "components/payments/content/payment_manifest_web_data_service.h"
#include "components/payments/content/utility/payment_manifest_parser.h"
#include "components/payments/core/payment_manifest_downloader.h"
#include "components/webdata/common/web_data_results.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/console_message_level.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace payments {
namespace {

const char* const kAllOriginsSupportedIndicator = "*";

// Enables |method_manifest_url| in those |apps| that are from |app_origins|, if
// either |all_origins_supported| is true or |supported_origin_strings| contains
// the origin of the app.
void EnableMethodManifestUrlForSupportedApps(
    const GURL& method_manifest_url,
    const std::vector<std::string>& supported_origin_strings,
    bool all_origins_supported,
    const std::set<url::Origin>& app_origins,
    content::PaymentAppProvider::PaymentApps* apps,
    std::map<GURL, std::set<GURL>>* prohibited_payment_methods) {
  for (const auto& app_origin : app_origins) {
    for (auto& app : *apps) {
      if (app_origin.IsSameOriginWith(
              url::Origin::Create(app.second->scope.GetOrigin()))) {
        app.second->has_explicitly_verified_methods = base::ContainsValue(
            supported_origin_strings, app_origin.Serialize());
        if (all_origins_supported ||
            app.second->has_explicitly_verified_methods) {
          app.second->enabled_methods.emplace_back(method_manifest_url.spec());
          prohibited_payment_methods->at(app.second->scope)
              .erase(method_manifest_url);
        }
      }
    }
  }
}

}  // namespace

ManifestVerifier::ManifestVerifier(content::WebContents* web_contents,
                                   PaymentManifestDownloader* downloader,
                                   PaymentManifestParser* parser,
                                   PaymentManifestWebDataService* cache)
    : dev_tools_(web_contents),
      downloader_(downloader),
      parser_(parser),
      cache_(cache),
      number_of_manifests_to_verify_(0),
      number_of_manifests_to_download_(0),
      weak_ptr_factory_(this) {}

ManifestVerifier::~ManifestVerifier() {
  for (const auto& handle : cache_request_handles_) {
    cache_->CancelRequest(handle.first);
  }
}

void ManifestVerifier::Verify(content::PaymentAppProvider::PaymentApps apps,
                              VerifyCallback finished_verification,
                              base::OnceClosure finished_using_resources) {
  DCHECK(apps_.empty());
  DCHECK(finished_verification_callback_.is_null());
  DCHECK(finished_using_resources_callback_.is_null());

  apps_ = std::move(apps);
  finished_verification_callback_ = std::move(finished_verification);
  finished_using_resources_callback_ = std::move(finished_using_resources);

  std::set<GURL> manifests_to_download;
  for (auto& app : apps_) {
    std::vector<std::string> verified_method_names;
    for (const auto& method : app.second->enabled_methods) {
      // For non-URL payment method names, only names published by W3C should be
      // supported. Keep this in sync with AndroidPaymentAppFinder.java.
      // https://w3c.github.io/payment-method-basic-card/
      // https://w3c.github.io/webpayments/proposals/interledger-payment-method.html
      // https://w3c.github.io/webpayments-methods-credit-transfer-direct-debit/
      if (method == "basic-card" || method == "interledger" ||
          method == "payee-credit-transfer" ||
          method == "payer-credit-transfer") {
        verified_method_names.emplace_back(method);
        continue;
      }

      // GURL constructor may crash with some invalid unicode strings.
      if (!base::IsStringUTF8(method)) {
        dev_tools_.WarnIfPossible("Payment method name \"" + method +
                                  "\" is not valid unicode.");
        continue;
      }

      // All URL payment method names must be HTTPS.
      GURL method_manifest_url = GURL(method);
      if (!method_manifest_url.is_valid() ||
          method_manifest_url.scheme() != "https") {
        dev_tools_.WarnIfPossible("\"" + method +
                                  "\" is not a valid payment method name.");
        continue;
      }

      // Same origin payment methods are always allowed.
      url::Origin app_origin =
          url::Origin::Create(app.second->scope.GetOrigin());
      if (url::Origin::Create(method_manifest_url.GetOrigin())
              .IsSameOriginWith(app_origin)) {
        verified_method_names.emplace_back(method);
        app.second->has_explicitly_verified_methods = true;
        continue;
      }

      manifests_to_download.insert(method_manifest_url);
      manifest_url_to_app_origins_map_[method_manifest_url].insert(app_origin);
      prohibited_payment_methods_[app.second->scope].insert(
          method_manifest_url);
    }

    app.second->enabled_methods.swap(verified_method_names);
  }

  number_of_manifests_to_verify_ = number_of_manifests_to_download_ =
      manifests_to_download.size();
  if (number_of_manifests_to_verify_ == 0) {
    RemoveInvalidPaymentApps();
    std::move(finished_verification_callback_).Run(std::move(apps_));
    std::move(finished_using_resources_callback_).Run();
    return;
  }

  for (const auto& method_manifest_url : manifests_to_download) {
    cache_request_handles_[cache_->GetPaymentMethodManifest(
        method_manifest_url.spec(), this)] = method_manifest_url;
  }
}

ManifestVerifier::DevToolsHelper::DevToolsHelper(
    content::WebContents* web_contents)
    : content::WebContentsObserver(web_contents) {}

ManifestVerifier::DevToolsHelper::~DevToolsHelper() {}

void ManifestVerifier::DevToolsHelper::WarnIfPossible(
    const std::string& message) {
  if (web_contents()) {
    web_contents()->GetMainFrame()->AddMessageToConsole(
        content::CONSOLE_MESSAGE_LEVEL_WARNING, message);
  } else {
    LOG(WARNING) << message;
  }
}

void ManifestVerifier::OnWebDataServiceRequestDone(
    WebDataServiceBase::Handle h,
    std::unique_ptr<WDTypedResult> result) {
  DCHECK_LT(0U, number_of_manifests_to_verify_);

  auto it = cache_request_handles_.find(h);
  if (it == cache_request_handles_.end())
    return;

  GURL method_manifest_url = it->second;
  cache_request_handles_.erase(it);

  const std::vector<std::string>& cached_strings =
      (static_cast<const WDResult<std::vector<std::string>>*>(result.get()))
          ->GetValue();

  bool all_origins_supported = false;
  std::vector<std::string> native_app_ids;
  std::vector<std::string> supported_origin_strings;
  for (const auto& origin_or_id : cached_strings) {
    // The string could be "*", origin or native payment app package Id on
    // Android.
    if (origin_or_id == kAllOriginsSupportedIndicator) {
      all_origins_supported = true;
      continue;
    }

    if (base::IsStringUTF8(origin_or_id) && GURL(origin_or_id).is_valid()) {
      supported_origin_strings.emplace_back(origin_or_id);
    } else {
      native_app_ids.emplace_back(origin_or_id);
    }
  }
  cached_supported_native_app_ids_[method_manifest_url] = native_app_ids;

  EnableMethodManifestUrlForSupportedApps(
      method_manifest_url, supported_origin_strings, all_origins_supported,
      manifest_url_to_app_origins_map_[method_manifest_url], &apps_,
      &prohibited_payment_methods_);

  if (!supported_origin_strings.empty()) {
    cached_manifest_urls_.insert(method_manifest_url);
    if (--number_of_manifests_to_verify_ == 0) {
      RemoveInvalidPaymentApps();
      std::move(finished_verification_callback_).Run(std::move(apps_));
    }
  }

  downloader_->DownloadPaymentMethodManifest(
      method_manifest_url,
      base::BindOnce(&ManifestVerifier::OnPaymentMethodManifestDownloaded,
                     weak_ptr_factory_.GetWeakPtr(), method_manifest_url));
}

void ManifestVerifier::OnPaymentMethodManifestDownloaded(
    const GURL& method_manifest_url,
    const std::string& content) {
  DCHECK_LT(0U, number_of_manifests_to_download_);

  if (content.empty()) {
    if (cached_manifest_urls_.find(method_manifest_url) ==
            cached_manifest_urls_.end() &&
        --number_of_manifests_to_verify_ == 0) {
      RemoveInvalidPaymentApps();
      std::move(finished_verification_callback_).Run(std::move(apps_));
    }

    if (--number_of_manifests_to_download_ == 0)
      std::move(finished_using_resources_callback_).Run();

    return;
  }

  parser_->ParsePaymentMethodManifest(
      content,
      base::BindOnce(&ManifestVerifier::OnPaymentMethodManifestParsed,
                     weak_ptr_factory_.GetWeakPtr(), method_manifest_url));
}

void ManifestVerifier::OnPaymentMethodManifestParsed(
    const GURL& method_manifest_url,
    const std::vector<GURL>& default_applications,
    const std::vector<url::Origin>& supported_origins,
    bool all_origins_supported) {
  DCHECK_LT(0U, number_of_manifests_to_download_);

  std::vector<std::string> supported_origin_strings(supported_origins.size());
  std::transform(supported_origins.begin(), supported_origins.end(),
                 supported_origin_strings.begin(),
                 [](const auto& origin) { return origin.Serialize(); });

  if (cached_manifest_urls_.find(method_manifest_url) ==
      cached_manifest_urls_.end()) {
    EnableMethodManifestUrlForSupportedApps(
        method_manifest_url, supported_origin_strings, all_origins_supported,
        manifest_url_to_app_origins_map_[method_manifest_url], &apps_,
        &prohibited_payment_methods_);

    if (--number_of_manifests_to_verify_ == 0) {
      RemoveInvalidPaymentApps();
      std::move(finished_verification_callback_).Run(std::move(apps_));
    }
  }

  if (all_origins_supported)
    supported_origin_strings.emplace_back(kAllOriginsSupportedIndicator);

  // Keep Android native payment app Ids in cache.
  std::map<GURL, std::vector<std::string>>::const_iterator it =
      cached_supported_native_app_ids_.find(method_manifest_url);
  if (it != cached_supported_native_app_ids_.end()) {
    supported_origin_strings.insert(supported_origin_strings.end(),
                                    it->second.begin(), it->second.end());
  }

  cache_->AddPaymentMethodManifest(method_manifest_url.spec(),
                                   supported_origin_strings);

  if (--number_of_manifests_to_download_ == 0)
    std::move(finished_using_resources_callback_).Run();
}

void ManifestVerifier::RemoveInvalidPaymentApps() {
  // Notify the web developer that a payment app cannot use certain payment
  // methods.
  for (const auto& it : prohibited_payment_methods_) {
    DCHECK(it.first.is_valid());
    std::string app_scope = it.first.spec();
    std::string app_origin = it.first.GetOrigin().spec();
    const std::set<GURL>& methods = it.second;
    for (const GURL& method : methods) {
      DCHECK(method.is_valid());
      dev_tools_.WarnIfPossible(
          "The payment handler \"" + app_scope +
          "\" is not allowed to use payment method \"" + method.spec() +
          "\", because the payment handler origin \"" + app_origin +
          "\" is different from the payment method origin \"" +
          method.GetOrigin().spec() +
          "\" and the \"supported_origins\" field in the payment method "
          "manifest for \"" +
          method.spec() + "\" is not \"*\" and is not a list that includes \"" +
          app_origin + "\".");
    }
  }

  // Remove apps without enabled methods.
  for (auto it = apps_.begin(); it != apps_.end();) {
    if (it->second->enabled_methods.empty())
      it = apps_.erase(it);
    else
      ++it;
  }
}

}  // namespace payments
