// 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/utility/payment_manifest_parser.h"

#include <algorithm>
#include <utility>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "components/payments/content/utility/fingerprint_parser.h"
#include "components/payments/core/error_logger.h"
#include "components/payments/core/url_util.h"
#include "content/public/common/service_manager_connection.h"
#include "net/base/url_util.h"
#include "services/data_decoder/public/cpp/safe_json_parser.h"
#include "url/url_constants.h"

namespace payments {
namespace {

const size_t kMaximumNumberOfItems = 100U;
const size_t kMaximumNumberOfSupportedOrigins = 100000;

const char* const kDefaultApplications = "default_applications";
const char* const kFingerprints = "fingerprints";
const char* const kHttpPrefix = "http://";
const char* const kHttpsPrefix = "https://";
const char* const kId = "id";
const char* const kMinVersion = "min_version";
const char* const kPlatform = "platform";
const char* const kPlay = "play";
const char* const kPreferRelatedApplications = "prefer_related_applications";
const char* const kRelatedApplications = "related_applications";
const char* const kServiceWorkerScope = "scope";
const char* const kServiceWorker = "serviceworker";
const char* const kServiceWorkerSrc = "src";
const char* const kServiceWorkerUseCache = "use_cache";
const char* const kSupportedOrigins = "supported_origins";
const char* const kWebAppIcons = "icons";
const char* const kWebAppIconSizes = "sizes";
const char* const kWebAppIconSrc = "src";
const char* const kWebAppIconType = "type";
const char* const kWebAppName = "name";

// Parses the "default_applications": ["https://some/url"] from |dict| into
// |web_app_manifest_urls|. Returns 'false' for invalid data.
bool ParseDefaultApplications(base::DictionaryValue* dict,
                              std::vector<GURL>* web_app_manifest_urls,
                              const ErrorLogger& log) {
  DCHECK(dict);
  DCHECK(web_app_manifest_urls);

  base::ListValue* list = nullptr;
  if (!dict->GetList(kDefaultApplications, &list)) {
    log.Error(
        base::StringPrintf("\"%s\" must be a list.", kDefaultApplications));
    return false;
  }

  size_t apps_number = list->GetSize();
  if (apps_number > kMaximumNumberOfItems) {
    log.Error(base::StringPrintf("\"%s\" must contain at most %zu entries.",
                                 kDefaultApplications, kMaximumNumberOfItems));
    return false;
  }

  for (size_t i = 0; i < apps_number; ++i) {
    std::string item;
    if (!list->GetString(i, &item) || item.empty() ||
        !base::IsStringUTF8(item) ||
        !(base::StartsWith(item, kHttpsPrefix, base::CompareCase::SENSITIVE) ||
          base::StartsWith(item, kHttpPrefix, base::CompareCase::SENSITIVE))) {
      log.Error(base::StringPrintf(
          "Each entry in \"%s\" must be UTF8 string that starts with \"%s\" or "
          "\"%s\" (for localhost).",
          kDefaultApplications, kHttpsPrefix, kHttpPrefix));
      web_app_manifest_urls->clear();
      return false;
    }

    GURL url(item);
    if (!UrlUtil::IsValidManifestUrl(url)) {
      log.Error(base::StringPrintf(
          "\"%s\" entry in \"%s\" is not a valid URL with HTTPS scheme and is "
          "not a valid localhost URL with HTTP scheme.",
          item.c_str(), kDefaultApplications));
      web_app_manifest_urls->clear();
      return false;
    }

    web_app_manifest_urls->push_back(url);
  }

  return true;
}

// Parses the "supported_origins": "*" (or ["https://some.origin"]) from |dict|
// into |supported_origins| and |all_origins_supported|. Returns 'false' for
// invalid data.
bool ParseSupportedOrigins(base::DictionaryValue* dict,
                           std::vector<url::Origin>* supported_origins,
                           bool* all_origins_supported,
                           const ErrorLogger& log) {
  DCHECK(dict);
  DCHECK(supported_origins);
  DCHECK(all_origins_supported);

  *all_origins_supported = false;

  {
    std::string item;
    if (dict->GetString(kSupportedOrigins, &item)) {
      if (item != "*") {
        log.Error(
            base::StringPrintf("Invalid value for \"%s\". Must be either \"*\" "
                               "or a list of RFC6454 origins.",
                               kSupportedOrigins));
        return false;
      }

      *all_origins_supported = true;
      return true;
    }
  }

  base::ListValue* list = nullptr;
  if (!dict->GetList(kSupportedOrigins, &list)) {
    log.Error(
        base::StringPrintf("\"%s\" must be either \"*\" or a list of origins.",
                           kSupportedOrigins));
    return false;
  }

  size_t supported_origins_number = list->GetSize();
  if (supported_origins_number > kMaximumNumberOfSupportedOrigins) {
    log.Error(base::StringPrintf("\"%s\" must contain at most %zu entires.",
                                 kSupportedOrigins,
                                 kMaximumNumberOfSupportedOrigins));
    return false;
  }

  for (size_t i = 0; i < supported_origins_number; ++i) {
    std::string item;
    if (!list->GetString(i, &item) || item.empty() ||
        !base::IsStringUTF8(item) ||
        !(base::StartsWith(item, kHttpsPrefix, base::CompareCase::SENSITIVE) ||
          base::StartsWith(item, kHttpPrefix, base::CompareCase::SENSITIVE))) {
      supported_origins->clear();
      log.Error(base::StringPrintf(
          "Each entry in \"%s\" must be UTF8 string that starts with \"%s\" or "
          "\"%s\" (for localhost).",
          kSupportedOrigins, kHttpsPrefix, kHttpPrefix));
      return false;
    }

    GURL url(item);
    if (!UrlUtil::IsValidSupportedOrigin(url)) {
      supported_origins->clear();
      log.Error(base::StringPrintf(
          "\"%s\" entry in \"%s\" is not a valid origin with HTTPS scheme and "
          "is not a valid localhost origin with HTTP scheme.",
          item.c_str(), kSupportedOrigins));
      return false;
    }

    supported_origins->push_back(url::Origin::Create(url));
  }

  return true;
}

void ParseIcons(const base::DictionaryValue& dict,
                const ErrorLogger& log,
                std::vector<PaymentManifestParser::WebAppIcon>* icons) {
  DCHECK(icons);

  const base::ListValue* icons_list = nullptr;
  if (!dict.GetList(kWebAppIcons, &icons_list)) {
    log.Warn(
        base::StringPrintf("No \"%s\" list in the manifest.", kWebAppIcons));
    return;
  }

  for (const auto& icon : *icons_list) {
    if (!icon.is_dict()) {
      log.Warn(base::StringPrintf(
          "Each item in the list \"%s\" should be a dictionary.",
          kWebAppIcons));
      continue;
    }

    PaymentManifestParser::WebAppIcon web_app_icon;
    const base::Value* icon_src =
        icon.FindKeyOfType(kWebAppIconSrc, base::Value::Type::STRING);
    if (!icon_src || icon_src->GetString().empty() ||
        !base::IsStringUTF8(icon_src->GetString())) {
      log.Warn(
          base::StringPrintf("Each dictionary in the list \"%s\" should "
                             "contain a non-empty UTF8 string field \"%s\".",
                             kWebAppIcons, kWebAppIconSrc));
      continue;
    }
    web_app_icon.src = icon_src->GetString();

    const base::Value* icon_sizes =
        icon.FindKeyOfType(kWebAppIconSizes, base::Value::Type::STRING);
    if (!icon_sizes || icon_sizes->GetString().empty() ||
        !base::IsStringUTF8(icon_sizes->GetString())) {
      log.Warn(
          base::StringPrintf("Each dictionary in the list \"%s\" should "
                             "contain a non-empty UTF8 string field \"%s\".",
                             kWebAppIcons, kWebAppIconSizes));
    } else {
      web_app_icon.sizes = icon_sizes->GetString();
    }

    const base::Value* icon_type =
        icon.FindKeyOfType(kWebAppIconType, base::Value::Type::STRING);
    if (!icon_type || icon_type->GetString().empty() ||
        !base::IsStringUTF8(icon_type->GetString())) {
      log.Warn(
          base::StringPrintf("Each dictionary in the list \"%s\" should "
                             "contain a non-empty UTF8 string field \"%s\".",
                             kWebAppIcons, kWebAppIconType));
    } else {
      web_app_icon.type = icon_type->GetString();
    }

    icons->emplace_back(web_app_icon);
  }
}

void ParsePreferredRelatedApplicationIdentifiers(
    const base::DictionaryValue& dict,
    const ErrorLogger& log,
    std::vector<std::string>* ids) {
  DCHECK(ids);

  if (!dict.HasKey(kPreferRelatedApplications))
    return;

  bool prefer_related_applications = false;
  if (!dict.GetBoolean(kPreferRelatedApplications,
                       &prefer_related_applications)) {
    log.Warn(base::StringPrintf("The \"%s\" field should be a boolean.",
                                kPreferRelatedApplications));
    return;
  }

  if (!prefer_related_applications)
    return;

  const base::ListValue* related_applications = nullptr;
  if (!dict.GetList(kRelatedApplications, &related_applications)) {
    log.Warn(
        base::StringPrintf("The \"%s\" field should be a list of dictionaries.",
                           kRelatedApplications));
    return;
  }

  size_t size = related_applications->GetSize();
  if (size == 0) {
    log.Warn(base::StringPrintf(
        "Did not find any entries in \"%s\", even though \"%s\" is true.",
        kRelatedApplications, kPreferRelatedApplications));
    return;
  }

  for (size_t i = 0; i < size; ++i) {
    const base::DictionaryValue* related_application = nullptr;
    if (!related_applications->GetDictionary(i, &related_application)) {
      log.Warn(
          base::StringPrintf("Element #%zu in \"%s\" should be a dictionary.",
                             i, kRelatedApplications));
      continue;
    }

    std::string platform;
    if (!related_application->GetString(kPlatform, &platform) ||
        platform != kPlay) {
      continue;
    }

    if (ids->size() >= kMaximumNumberOfItems) {
      log.Warn(base::StringPrintf(
          "The maximum number of items in \"%s\" with \"%s\":\"%s\" should not "
          "exceed %zu.",
          kRelatedApplications, kPlatform, kPlay, kMaximumNumberOfItems));
      break;
    }

    std::string id;
    if (!related_application->GetString(kId, &id)) {
      log.Warn(base::StringPrintf(
          "Elements in \"%s\" with \"%s\":\"%s\" should have \"%s\" field.",
          kRelatedApplications, kPlatform, kPlay, kId));
      continue;
    }

    if (id.empty() || !base::IsStringASCII(id)) {
      log.Warn(base::StringPrintf(
          "\"%s\".\"%s\" should be a non-empty ASCII string.",
          kRelatedApplications, kId));
      continue;
    }

    ids->emplace_back(id);
  }
}

// An object that allows both SafeJsonParser's callbacks (error/success) to run
// the same callback provided to ParsePaymentMethodManifest/ParseWebAppManifest.
// (since Callbacks are movable type, that callback has to be shared)
template <typename Callback>
class JsonParserCallback
    : public base::RefCounted<JsonParserCallback<Callback>> {
 public:
  JsonParserCallback(
      base::OnceCallback<void(Callback,
                              std::unique_ptr<base::Value>,
                              const std::string&)> parser_callback,
      Callback client_callback)
      : parser_callback_(std::move(parser_callback)),
        client_callback_(std::move(client_callback)) {}

  void OnSuccess(base::Value value) {
    std::move(parser_callback_)
        .Run(std::move(client_callback_),
             base::Value::ToUniquePtrValue(std::move(value)),
             /*error_message=*/std::string());
  }

  void OnError(const std::string& error_message) {
    std::move(parser_callback_)
        .Run(std::move(client_callback_), /*value=*/nullptr, error_message);
  }

 private:
  friend class base::RefCounted<JsonParserCallback>;
  ~JsonParserCallback() = default;

  base::OnceCallback<
      void(Callback, std::unique_ptr<base::Value>, const std::string&)>
      parser_callback_;
  Callback client_callback_;
};

}  // namespace

PaymentManifestParser::WebAppIcon::WebAppIcon() = default;

PaymentManifestParser::WebAppIcon::~WebAppIcon() = default;

PaymentManifestParser::PaymentManifestParser(std::unique_ptr<ErrorLogger> log)
    : log_(std::move(log)) {
  DCHECK(log_);
}

PaymentManifestParser::~PaymentManifestParser() = default;

void PaymentManifestParser::ParsePaymentMethodManifest(
    const std::string& content,
    PaymentMethodCallback callback) {
  parse_payment_callback_counter_++;
  DCHECK_GE(10U, parse_payment_callback_counter_);

  scoped_refptr<JsonParserCallback<PaymentMethodCallback>> json_callback =
      new JsonParserCallback<PaymentMethodCallback>(
          base::Bind(&PaymentManifestParser::OnPaymentMethodParse,
                     weak_factory_.GetWeakPtr()),
          std::move(callback));

  data_decoder::SafeJsonParser::Parse(
      content::ServiceManagerConnection::GetForProcess()->GetConnector(),
      content,
      base::BindOnce(&JsonParserCallback<PaymentMethodCallback>::OnSuccess,
                     json_callback),
      base::BindOnce(&JsonParserCallback<PaymentMethodCallback>::OnError,
                     json_callback));
}

void PaymentManifestParser::ParseWebAppManifest(const std::string& content,
                                                WebAppCallback callback) {
  parse_webapp_callback_counter_++;
  DCHECK_GE(10U, parse_webapp_callback_counter_);

  scoped_refptr<JsonParserCallback<WebAppCallback>> parser_callback =
      new JsonParserCallback<WebAppCallback>(
          base::Bind(&PaymentManifestParser::OnWebAppParse,
                     weak_factory_.GetWeakPtr()),
          std::move(callback));

  data_decoder::SafeJsonParser::Parse(
      content::ServiceManagerConnection::GetForProcess()->GetConnector(),
      content,
      base::BindOnce(&JsonParserCallback<WebAppCallback>::OnSuccess,
                     parser_callback),
      base::BindOnce(&JsonParserCallback<WebAppCallback>::OnError,
                     parser_callback));
}

void PaymentManifestParser::ParseWebAppInstallationInfo(
    const std::string& content,
    WebAppInstallationInfoCallback callback) {
  scoped_refptr<JsonParserCallback<WebAppInstallationInfoCallback>>
      sw_parser_callback =
          new JsonParserCallback<WebAppInstallationInfoCallback>(
              base::Bind(&PaymentManifestParser::OnWebAppParseInstallationInfo,
                         weak_factory_.GetWeakPtr()),
              std::move(callback));

  data_decoder::SafeJsonParser::Parse(
      content::ServiceManagerConnection::GetForProcess()->GetConnector(),
      content,
      base::BindOnce(
          &JsonParserCallback<WebAppInstallationInfoCallback>::OnSuccess,
          sw_parser_callback),
      base::BindOnce(
          &JsonParserCallback<WebAppInstallationInfoCallback>::OnError,
          sw_parser_callback));
}

// static
void PaymentManifestParser::ParsePaymentMethodManifestIntoVectors(
    std::unique_ptr<base::Value> value,
    const ErrorLogger& log,
    std::vector<GURL>* web_app_manifest_urls,
    std::vector<url::Origin>* supported_origins,
    bool* all_origins_supported) {
  DCHECK(web_app_manifest_urls);
  DCHECK(supported_origins);
  DCHECK(all_origins_supported);

  *all_origins_supported = false;

  std::unique_ptr<base::DictionaryValue> dict =
      base::DictionaryValue::From(std::move(value));
  if (!dict) {
    log.Error("Payment method manifest must be a JSON dictionary.");
    return;
  }

  if (dict->HasKey(kDefaultApplications) &&
      !ParseDefaultApplications(dict.get(), web_app_manifest_urls, log)) {
    return;
  }

  if (dict->HasKey(kSupportedOrigins) &&
      !ParseSupportedOrigins(dict.get(), supported_origins,
                             all_origins_supported, log)) {
    web_app_manifest_urls->clear();
  }
}

// static
bool PaymentManifestParser::ParseWebAppManifestIntoVector(
    std::unique_ptr<base::Value> value,
    const ErrorLogger& log,
    std::vector<WebAppManifestSection>* output) {
  std::unique_ptr<base::DictionaryValue> dict =
      base::DictionaryValue::From(std::move(value));
  if (!dict) {
    log.Error("Web app manifest must be a JSON dictionary.");
    return false;
  }

  base::ListValue* list = nullptr;
  if (!dict->GetList(kRelatedApplications, &list)) {
    log.Error(
        base::StringPrintf("\"%s\" must be a list.", kRelatedApplications));
    return false;
  }

  size_t related_applications_size = list->GetSize();
  for (size_t i = 0; i < related_applications_size; ++i) {
    base::DictionaryValue* related_application = nullptr;
    if (!list->GetDictionary(i, &related_application) || !related_application) {
      log.Error(base::StringPrintf("\"%s\" must be a list of dictionaries.",
                                   kRelatedApplications));
      output->clear();
      return false;
    }

    std::string platform;
    if (!related_application->GetString(kPlatform, &platform) ||
        platform != kPlay) {
      continue;
    }

    if (output->size() >= kMaximumNumberOfItems) {
      log.Error(base::StringPrintf(
          "\"%s\" must contain at most %zu entries with \"%s\": \"%s\".",
          kRelatedApplications, kMaximumNumberOfItems, kPlatform, kPlay));
      output->clear();
      return false;
    }

    if (!related_application->HasKey(kId) ||
        !related_application->HasKey(kMinVersion) ||
        !related_application->HasKey(kFingerprints)) {
      log.Error(
          base::StringPrintf("Each \"%s\": \"%s\" entry in \"%s\" must contain "
                             "\"%s\", \"%s\", and \"%s\".",
                             kPlatform, kPlay, kRelatedApplications, kId,
                             kMinVersion, kFingerprints));
      return false;
    }

    WebAppManifestSection section;
    section.min_version = 0;

    if (!related_application->GetString(kId, &section.id) ||
        section.id.empty() || !base::IsStringASCII(section.id)) {
      log.Error(
          base::StringPrintf("\"%s\" must be a non-empty ASCII string.", kId));
      output->clear();
      return false;
    }

    std::string min_version;
    if (!related_application->GetString(kMinVersion, &min_version) ||
        min_version.empty() || !base::IsStringASCII(min_version) ||
        !base::StringToInt64(min_version, &section.min_version)) {
      log.Error(base::StringPrintf(
          "\"%s\" must be a string convertible into a number.", kMinVersion));
      output->clear();
      return false;
    }

    base::ListValue* fingerprints_list = nullptr;
    if (!related_application->GetList(kFingerprints, &fingerprints_list) ||
        fingerprints_list->empty() ||
        fingerprints_list->GetSize() > kMaximumNumberOfItems) {
      log.Error(base::StringPrintf(
          "\"%s\" must be a non-empty list of at most %zu items.",
          kFingerprints, kMaximumNumberOfItems));
      output->clear();
      return false;
    }

    size_t fingerprints_size = fingerprints_list->GetSize();
    for (size_t j = 0; j < fingerprints_size; ++j) {
      base::DictionaryValue* fingerprint_dict = nullptr;
      std::string fingerprint_type;
      std::string fingerprint_value;
      if (!fingerprints_list->GetDictionary(j, &fingerprint_dict) ||
          !fingerprint_dict ||
          !fingerprint_dict->GetString("type", &fingerprint_type) ||
          fingerprint_type != "sha256_cert" ||
          !fingerprint_dict->GetString("value", &fingerprint_value) ||
          fingerprint_value.empty() ||
          !base::IsStringASCII(fingerprint_value)) {
        log.Error(base::StringPrintf(
            "Each entry in \"%s\" must be a dictionary with \"type\": "
            "\"sha256_cert\" and a non-empty ASCII string \"value\".",
            kFingerprints));
        output->clear();
        return false;
      }

      std::vector<uint8_t> hash =
          FingerprintStringToByteArray(fingerprint_value, log);
      if (hash.empty()) {
        output->clear();
        return false;
      }

      section.fingerprints.push_back(hash);
    }

    output->push_back(section);
  }

  return true;
}

// static
bool PaymentManifestParser::ParseWebAppInstallationInfoIntoStructs(
    std::unique_ptr<base::Value> value,
    const ErrorLogger& log,
    WebAppInstallationInfo* installation_info,
    std::vector<WebAppIcon>* icons) {
  DCHECK(installation_info);
  DCHECK(icons);

  std::unique_ptr<base::DictionaryValue> dict =
      base::DictionaryValue::From(std::move(value));
  if (!dict) {
    log.Error("Web app manifest must be a JSON dictionary.");
    return false;
  }

  {
    base::DictionaryValue* service_worker_dict = nullptr;
    if (!dict->GetDictionary(kServiceWorker, &service_worker_dict)) {
      log.Error(
          base::StringPrintf("\"%s\" must be a dictionary", kServiceWorker));
      return false;
    }

    if (!service_worker_dict->GetString(kServiceWorkerSrc,
                                        &installation_info->sw_js_url) ||
        installation_info->sw_js_url.empty() ||
        !base::IsStringUTF8(installation_info->sw_js_url)) {
      log.Error(
          base::StringPrintf("\"%s\".\"%s\" must be a non-empty UTF8 string.",
                             kServiceWorker, kServiceWorkerSrc));
      return false;
    }

    service_worker_dict->GetString(kServiceWorkerScope,
                                   &installation_info->sw_scope);

    bool use_cache = false;
    if (service_worker_dict->GetBoolean(kServiceWorkerUseCache, &use_cache)) {
      installation_info->sw_use_cache = use_cache;
    }
  }

  dict->GetString(kWebAppName, &installation_info->name);
  if (installation_info->name.empty()) {
    log.Warn(
        base::StringPrintf("No \"%s\" string in the manifest.", kWebAppName));
  }

  ParseIcons(*dict, log, icons);
  ParsePreferredRelatedApplicationIdentifiers(
      *dict, log, &installation_info->preferred_app_ids);

  return true;
}

void PaymentManifestParser::OnPaymentMethodParse(
    PaymentMethodCallback callback,
    std::unique_ptr<base::Value> value,
    const std::string& json_parser_error) {
  parse_payment_callback_counter_--;

  std::vector<GURL> web_app_manifest_urls;
  std::vector<url::Origin> supported_origins;
  bool all_origins_supported = false;

  if (json_parser_error.empty()) {
    ParsePaymentMethodManifestIntoVectors(
        std::move(value), *log_, &web_app_manifest_urls, &supported_origins,
        &all_origins_supported);
  } else {
    log_->Error(json_parser_error);
  }

  // Can trigger synchronous deletion of this object, so can't access any of
  // the member variables after this statement.
  std::move(callback).Run(web_app_manifest_urls, supported_origins,
                          all_origins_supported);
}

void PaymentManifestParser::OnWebAppParse(
    WebAppCallback callback,
    std::unique_ptr<base::Value> value,
    const std::string& json_parser_error) {
  parse_webapp_callback_counter_--;

  std::vector<WebAppManifestSection> manifest;
  if (json_parser_error.empty()) {
    ParseWebAppManifestIntoVector(std::move(value), *log_, &manifest);
  } else {
    log_->Error(json_parser_error);
  }

  // Can trigger synchronous deletion of this object, so can't access any of
  // the member variables after this statement.
  std::move(callback).Run(manifest);
}

void PaymentManifestParser::OnWebAppParseInstallationInfo(
    WebAppInstallationInfoCallback callback,
    std::unique_ptr<base::Value> value,
    const std::string& json_parser_error) {
  std::unique_ptr<WebAppInstallationInfo> installation_info;
  std::unique_ptr<std::vector<WebAppIcon>> icons;

  if (json_parser_error.empty()) {
    installation_info = std::make_unique<WebAppInstallationInfo>();
    icons = std::make_unique<std::vector<WebAppIcon>>();
    if (!ParseWebAppInstallationInfoIntoStructs(
            std::move(value), *log_, installation_info.get(), icons.get())) {
      installation_info.reset();
      icons.reset();
    }
  } else {
    log_->Error(json_parser_error);
  }

  // Can trigger synchronous deletion of this object, so can't access any of
  // the member variables after this statement.
  std::move(callback).Run(std::move(installation_info), std::move(icons));
}

}  // namespace payments
