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

#include "net/cookies/cookie_inclusion_status.h"

#include <initializer_list>
#include <utility>

#include "base/strings/strcat.h"
#include "url/gurl.h"

namespace net {

CookieInclusionStatus::CookieInclusionStatus() = default;

CookieInclusionStatus::CookieInclusionStatus(ExclusionReason reason) {
  exclusion_reasons_[reason] = true;
}

CookieInclusionStatus::CookieInclusionStatus(ExclusionReason reason,
                                             WarningReason warning) {
  exclusion_reasons_[reason] = true;
  warning_reasons_[warning] = true;
}

CookieInclusionStatus::CookieInclusionStatus(WarningReason warning) {
  warning_reasons_[warning] = true;
}

CookieInclusionStatus::CookieInclusionStatus(
    const CookieInclusionStatus& other) = default;

CookieInclusionStatus& CookieInclusionStatus::operator=(
    const CookieInclusionStatus& other) = default;

bool CookieInclusionStatus::operator==(
    const CookieInclusionStatus& other) const {
  return exclusion_reasons_ == other.exclusion_reasons_ &&
         warning_reasons_ == other.warning_reasons_;
}

bool CookieInclusionStatus::operator!=(
    const CookieInclusionStatus& other) const {
  return !operator==(other);
}

bool CookieInclusionStatus::IsInclude() const {
  return exclusion_reasons_.none();
}

bool CookieInclusionStatus::HasExclusionReason(ExclusionReason reason) const {
  return exclusion_reasons_[reason];
}

bool CookieInclusionStatus::HasOnlyExclusionReason(
    ExclusionReason reason) const {
  return exclusion_reasons_[reason] && exclusion_reasons_.count() == 1;
}

void CookieInclusionStatus::AddExclusionReason(ExclusionReason reason) {
  exclusion_reasons_[reason] = true;
  // If the cookie would be excluded for reasons other than the new SameSite
  // rules, don't bother warning about it.
  MaybeClearSameSiteWarning();
}

void CookieInclusionStatus::RemoveExclusionReason(ExclusionReason reason) {
  exclusion_reasons_[reason] = false;
}

void CookieInclusionStatus::RemoveExclusionReasons(
    const std::vector<ExclusionReason>& reasons) {
  exclusion_reasons_ = ExclusionReasonsWithout(reasons);
}

CookieInclusionStatus::ExclusionReasonBitset
CookieInclusionStatus::ExclusionReasonsWithout(
    const std::vector<ExclusionReason>& reasons) const {
  CookieInclusionStatus::ExclusionReasonBitset result(exclusion_reasons_);
  for (const ExclusionReason reason : reasons) {
    result[reason] = false;
  }
  return result;
}

void CookieInclusionStatus::MaybeClearSameSiteWarning() {
  if (ExclusionReasonsWithout({
          EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX,
          EXCLUDE_SAMESITE_NONE_INSECURE,
      }) != 0u) {
    RemoveWarningReason(WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT);
    RemoveWarningReason(WARN_SAMESITE_NONE_INSECURE);
    RemoveWarningReason(WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE);
  }

  if (!ShouldRecordDowngradeMetrics()) {
    RemoveWarningReason(WARN_STRICT_LAX_DOWNGRADE_STRICT_SAMESITE);
    RemoveWarningReason(WARN_STRICT_CROSS_DOWNGRADE_STRICT_SAMESITE);
    RemoveWarningReason(WARN_STRICT_CROSS_DOWNGRADE_LAX_SAMESITE);
    RemoveWarningReason(WARN_LAX_CROSS_DOWNGRADE_STRICT_SAMESITE);
    RemoveWarningReason(WARN_LAX_CROSS_DOWNGRADE_LAX_SAMESITE);

    RemoveWarningReason(WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION);
  }
}

bool CookieInclusionStatus::ShouldRecordDowngradeMetrics() const {
  return ExclusionReasonsWithout({
             EXCLUDE_SAMESITE_STRICT,
             EXCLUDE_SAMESITE_LAX,
             EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX,
         }) == 0u;
}

bool CookieInclusionStatus::ShouldWarn() const {
  return warning_reasons_.any();
}

bool CookieInclusionStatus::HasWarningReason(WarningReason reason) const {
  return warning_reasons_[reason];
}

bool CookieInclusionStatus::HasSchemefulDowngradeWarning(
    CookieInclusionStatus::WarningReason* reason) const {
  if (!ShouldWarn())
    return false;

  const CookieInclusionStatus::WarningReason kDowngradeWarnings[] = {
      WARN_STRICT_LAX_DOWNGRADE_STRICT_SAMESITE,
      WARN_STRICT_CROSS_DOWNGRADE_STRICT_SAMESITE,
      WARN_STRICT_CROSS_DOWNGRADE_LAX_SAMESITE,
      WARN_LAX_CROSS_DOWNGRADE_STRICT_SAMESITE,
      WARN_LAX_CROSS_DOWNGRADE_LAX_SAMESITE,
  };

  for (auto warning : kDowngradeWarnings) {
    if (!HasWarningReason(warning))
      continue;

    if (reason)
      *reason = warning;

    return true;
  }

  return false;
}

void CookieInclusionStatus::AddWarningReason(WarningReason reason) {
  warning_reasons_[reason] = true;
}

void CookieInclusionStatus::RemoveWarningReason(WarningReason reason) {
  warning_reasons_[reason] = false;
}

CookieInclusionStatus::ContextDowngradeMetricValues
CookieInclusionStatus::GetBreakingDowngradeMetricsEnumValue(
    const GURL& url) const {
  bool url_is_secure = url.SchemeIsCryptographic();

  // Start the |reason| as something other than the downgrade warnings.
  WarningReason reason = WarningReason::NUM_WARNING_REASONS;

  // Don't bother checking the return value because the default switch case
  // will handle if no reason was found.
  HasSchemefulDowngradeWarning(&reason);

  switch (reason) {
    case WarningReason::WARN_STRICT_LAX_DOWNGRADE_STRICT_SAMESITE:
      return url_is_secure
                 ? ContextDowngradeMetricValues::kStrictLaxStrictSecure
                 : ContextDowngradeMetricValues::kStrictLaxStrictInsecure;
    case WarningReason::WARN_STRICT_CROSS_DOWNGRADE_STRICT_SAMESITE:
      return url_is_secure
                 ? ContextDowngradeMetricValues::kStrictCrossStrictSecure
                 : ContextDowngradeMetricValues::kStrictCrossStrictInsecure;
    case WarningReason::WARN_STRICT_CROSS_DOWNGRADE_LAX_SAMESITE:
      return url_is_secure
                 ? ContextDowngradeMetricValues::kStrictCrossLaxSecure
                 : ContextDowngradeMetricValues::kStrictCrossLaxInsecure;
    case WarningReason::WARN_LAX_CROSS_DOWNGRADE_STRICT_SAMESITE:
      return url_is_secure
                 ? ContextDowngradeMetricValues::kLaxCrossStrictSecure
                 : ContextDowngradeMetricValues::kLaxCrossStrictInsecure;
    case WarningReason::WARN_LAX_CROSS_DOWNGRADE_LAX_SAMESITE:
      return url_is_secure ? ContextDowngradeMetricValues::kLaxCrossLaxSecure
                           : ContextDowngradeMetricValues::kLaxCrossLaxInsecure;
    default:
      return url_is_secure ? ContextDowngradeMetricValues::kNoDowngradeSecure
                           : ContextDowngradeMetricValues::kNoDowngradeInsecure;
  }
}

std::string CookieInclusionStatus::GetDebugString() const {
  std::string out;

  if (IsInclude())
    base::StrAppend(&out, {"INCLUDE, "});
  for (const auto& reason :
       std::initializer_list<std::pair<ExclusionReason, std::string>>{
           {EXCLUDE_UNKNOWN_ERROR, "EXCLUDE_UNKNOWN_ERROR"},
           {EXCLUDE_HTTP_ONLY, "EXCLUDE_HTTP_ONLY"},
           {EXCLUDE_SECURE_ONLY, "EXCLUDE_SECURE_ONLY"},
           {EXCLUDE_DOMAIN_MISMATCH, "EXCLUDE_DOMAIN_MISMATCH"},
           {EXCLUDE_NOT_ON_PATH, "EXCLUDE_NOT_ON_PATH"},
           {EXCLUDE_SAMESITE_STRICT, "EXCLUDE_SAMESITE_STRICT"},
           {EXCLUDE_SAMESITE_LAX, "EXCLUDE_SAMESITE_LAX"},
           {EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX,
            "EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX"},
           {EXCLUDE_SAMESITE_NONE_INSECURE, "EXCLUDE_SAMESITE_NONE_INSECURE"},
           {EXCLUDE_USER_PREFERENCES, "EXCLUDE_USER_PREFERENCES"},
           {EXCLUDE_SAMEPARTY_CROSS_PARTY_CONTEXT,
            "EXCLUDE_SAMEPARTY_CROSS_PARTY_CONTEXT"},
           {EXCLUDE_FAILURE_TO_STORE, "EXCLUDE_FAILURE_TO_STORE"},
           {EXCLUDE_NONCOOKIEABLE_SCHEME, "EXCLUDE_NONCOOKIEABLE_SCHEME"},
           {EXCLUDE_OVERWRITE_SECURE, "EXCLUDE_OVERWRITE_SECURE"},
           {EXCLUDE_OVERWRITE_HTTP_ONLY, "EXCLUDE_OVERWRITE_HTTP_ONLY"},
           {EXCLUDE_INVALID_DOMAIN, "EXCLUDE_INVALID_DOMAIN"},
           {EXCLUDE_INVALID_PREFIX, "EXCLUDE_INVALID_PREFIX"},
           {EXCLUDE_INVALID_SAMEPARTY, "EXCLUDE_INVALID_SAMEPARTY"},
           {EXCLUDE_INVALID_PARTITIONED, "EXCLUDE_INVALID_PARTITIONED"},
           {EXCLUDE_NAME_VALUE_PAIR_EXCEEDS_MAX_SIZE,
            "EXCLUDE_NAME_VALUE_PAIR_EXCEEDS_MAX_SIZE"},
           {EXCLUDE_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE,
            "EXCLUDE_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE"},
           {EXCLUDE_DOMAIN_NON_ASCII, "EXCLUDE_DOMAIN_NON_ASCII"},
           {EXCLUDE_PORT_MISMATCH, "EXCLUDE_PORT_MISMATCH"},
           {EXCLUDE_SCHEME_MISMATCH, "EXCLUDE_SCHEME_MISMATCH"},
           {EXCLUDE_SHADOWING_DOMAIN, "EXCLUDE_SHADOWING_DOMAIN"},
       }) {
    if (HasExclusionReason(reason.first))
      base::StrAppend(&out, {reason.second, ", "});
  }

  // Add warning
  if (!ShouldWarn()) {
    base::StrAppend(&out, {"DO_NOT_WARN"});
    return out;
  }

  for (const auto& reason :
       std::initializer_list<std::pair<WarningReason, std::string>>{
           {WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT,
            "WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT"},
           {WARN_SAMESITE_NONE_INSECURE, "WARN_SAMESITE_NONE_INSECURE"},
           {WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE,
            "WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE"},
           {WARN_STRICT_LAX_DOWNGRADE_STRICT_SAMESITE,
            "WARN_STRICT_LAX_DOWNGRADE_STRICT_SAMESITE"},
           {WARN_STRICT_CROSS_DOWNGRADE_STRICT_SAMESITE,
            "WARN_STRICT_CROSS_DOWNGRADE_STRICT_SAMESITE"},
           {WARN_STRICT_CROSS_DOWNGRADE_LAX_SAMESITE,
            "WARN_STRICT_CROSS_DOWNGRADE_LAX_SAMESITE"},
           {WARN_LAX_CROSS_DOWNGRADE_STRICT_SAMESITE,
            "WARN_LAX_CROSS_DOWNGRADE_STRICT_SAMESITE"},
           {WARN_LAX_CROSS_DOWNGRADE_LAX_SAMESITE,
            "WARN_LAX_CROSS_DOWNGRADE_LAX_SAMESITE"},
           {WARN_SECURE_ACCESS_GRANTED_NON_CRYPTOGRAPHIC,
            "WARN_SECURE_ACCESS_GRANTED_NON_CRYPTOGRAPHIC"},
           {WARN_SAMEPARTY_EXCLUSION_OVERRULED_SAMESITE,
            "WARN_SAMEPARTY_EXCLUSION_OVERRULED_SAMESITE"},
           {WARN_SAMEPARTY_INCLUSION_OVERRULED_SAMESITE,
            "WARN_SAMEPARTY_INCLUSION_OVERRULED_SAMESITE"},
           {WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION,
            "WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION"},
           {WARN_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE,
            "WARN_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE"},
           {WARN_DOMAIN_NON_ASCII, "WARN_DOMAIN_NON_ASCII"},
           {WARN_PORT_MISMATCH, "WARN_PORT_MISMATCH"},
           {WARN_SCHEME_MISMATCH, "WARN_SCHEME_MISMATCH"},
           {WARN_TENTATIVELY_ALLOWING_SECURE_SOURCE_SCHEME,
            "WARN_TENTATIVELY_ALLOWING_SECURE_SOURCE_SCHEME"},
           {WARN_SHADOWING_DOMAIN, "WARN_SHADOWING_DOMAIN"},
       }) {
    if (HasWarningReason(reason.first))
      base::StrAppend(&out, {reason.second, ", "});
  }

  // Strip trailing comma and space.
  out.erase(out.end() - 2, out.end());

  return out;
}

bool CookieInclusionStatus::HasExactlyExclusionReasonsForTesting(
    std::vector<CookieInclusionStatus::ExclusionReason> reasons) const {
  CookieInclusionStatus expected = MakeFromReasonsForTesting(reasons);
  return expected.exclusion_reasons_ == exclusion_reasons_;
}

bool CookieInclusionStatus::HasExactlyWarningReasonsForTesting(
    std::vector<WarningReason> reasons) const {
  CookieInclusionStatus expected = MakeFromReasonsForTesting({}, reasons);
  return expected.warning_reasons_ == warning_reasons_;
}

// static
bool CookieInclusionStatus::ValidateExclusionAndWarningFromWire(
    uint32_t exclusion_reasons,
    uint32_t warning_reasons) {
  uint32_t exclusion_mask =
      static_cast<uint32_t>(~0ul << ExclusionReason::NUM_EXCLUSION_REASONS);
  uint32_t warning_mask =
      static_cast<uint32_t>(~0ul << WarningReason::NUM_WARNING_REASONS);
  return (exclusion_reasons & exclusion_mask) == 0 &&
         (warning_reasons & warning_mask) == 0;
}

CookieInclusionStatus CookieInclusionStatus::MakeFromReasonsForTesting(
    std::vector<ExclusionReason> reasons,
    std::vector<WarningReason> warnings) {
  CookieInclusionStatus status;
  for (ExclusionReason reason : reasons) {
    status.AddExclusionReason(reason);
  }
  for (WarningReason warning : warnings) {
    status.AddWarningReason(warning);
  }
  return status;
}

bool CookieInclusionStatus::ExcludedByUserPreferences() const {
  if (HasOnlyExclusionReason(ExclusionReason::EXCLUDE_USER_PREFERENCES))
    return true;
  return exclusion_reasons_.count() == 2 &&
         exclusion_reasons_[ExclusionReason::EXCLUDE_USER_PREFERENCES] &&
         exclusion_reasons_
             [ExclusionReason::
                  EXCLUDE_THIRD_PARTY_BLOCKED_WITHIN_FIRST_PARTY_SET];
}

}  // namespace net
