// Copyright 2020 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 "net/cookies/cookie_inclusion_status.h"

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

namespace net {

namespace {

uint32_t GetExclusionBitmask(CookieInclusionStatus::ExclusionReason reason) {
  return 1u << static_cast<uint32_t>(reason);
}

uint32_t GetWarningBitmask(CookieInclusionStatus::WarningReason reason) {
  return 1u << static_cast<uint32_t>(reason);
}

}  // namespace

CookieInclusionStatus::CookieInclusionStatus()
    : exclusion_reasons_(0u), warning_reasons_(0u) {}

CookieInclusionStatus::CookieInclusionStatus(ExclusionReason reason)
    : exclusion_reasons_(GetExclusionBitmask(reason)) {}

CookieInclusionStatus::CookieInclusionStatus(ExclusionReason reason,
                                             WarningReason warning)
    : exclusion_reasons_(GetExclusionBitmask(reason)),
      warning_reasons_(GetWarningBitmask(warning)) {}

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_ == 0u;
}

bool CookieInclusionStatus::HasExclusionReason(ExclusionReason reason) const {
  return exclusion_reasons_ & GetExclusionBitmask(reason);
}

bool CookieInclusionStatus::HasOnlyExclusionReason(
    ExclusionReason reason) const {
  return exclusion_reasons_ == GetExclusionBitmask(reason);
}

void CookieInclusionStatus::AddExclusionReason(ExclusionReason reason) {
  exclusion_reasons_ |= GetExclusionBitmask(reason);
  // 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_ &= ~(GetExclusionBitmask(reason));
}

void CookieInclusionStatus::MaybeClearSameSiteWarning() {
  uint32_t samesite_reasons_mask =
      GetExclusionBitmask(EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX) |
      GetExclusionBitmask(EXCLUDE_SAMESITE_NONE_INSECURE);
  if (exclusion_reasons_ & ~samesite_reasons_mask) {
    RemoveWarningReason(
        CookieInclusionStatus::WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT);
    RemoveWarningReason(CookieInclusionStatus::WARN_SAMESITE_NONE_INSECURE);
    RemoveWarningReason(
        CookieInclusionStatus::WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE);
  }

  uint32_t context_reasons_mask =
      GetExclusionBitmask(EXCLUDE_SAMESITE_STRICT) |
      GetExclusionBitmask(EXCLUDE_SAMESITE_LAX) |
      GetExclusionBitmask(EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX);
  if (exclusion_reasons_ & ~context_reasons_mask) {
    RemoveWarningReason(
        CookieInclusionStatus::WARN_STRICT_LAX_DOWNGRADE_STRICT_SAMESITE);
    RemoveWarningReason(
        CookieInclusionStatus::WARN_STRICT_CROSS_DOWNGRADE_STRICT_SAMESITE);
    RemoveWarningReason(
        CookieInclusionStatus::WARN_STRICT_CROSS_DOWNGRADE_LAX_SAMESITE);
    RemoveWarningReason(
        CookieInclusionStatus::WARN_LAX_CROSS_DOWNGRADE_STRICT_SAMESITE);
    RemoveWarningReason(
        CookieInclusionStatus::WARN_LAX_CROSS_DOWNGRADE_LAX_SAMESITE);
  }
}

bool CookieInclusionStatus::ShouldRecordDowngradeMetrics() const {
  uint32_t context_reasons_mask =
      GetExclusionBitmask(EXCLUDE_SAMESITE_STRICT) |
      GetExclusionBitmask(EXCLUDE_SAMESITE_LAX) |
      GetExclusionBitmask(EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX);

  return (exclusion_reasons_ & ~context_reasons_mask) == 0u;
}

bool CookieInclusionStatus::ShouldWarn() const {
  return warning_reasons_ != 0u;
}

bool CookieInclusionStatus::HasWarningReason(WarningReason reason) const {
  return warning_reasons_ & GetWarningBitmask(reason);
}

bool CookieInclusionStatus::HasDowngradeWarning(
    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_ |= GetWarningBitmask(reason);
}

void CookieInclusionStatus::RemoveWarningReason(WarningReason reason) {
  warning_reasons_ &= ~(GetWarningBitmask(reason));
}

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.
  HasDowngradeWarning(&reason);

  switch (reason) {
    case WarningReason::WARN_STRICT_LAX_DOWNGRADE_STRICT_SAMESITE:
      return url_is_secure
                 ? ContextDowngradeMetricValues::STRICT_LAX_STRICT_SECURE
                 : ContextDowngradeMetricValues::STRICT_LAX_STRICT_INSECURE;
    case WarningReason::WARN_STRICT_CROSS_DOWNGRADE_STRICT_SAMESITE:
      return url_is_secure
                 ? ContextDowngradeMetricValues::STRICT_CROSS_STRICT_SECURE
                 : ContextDowngradeMetricValues::STRICT_CROSS_STRICT_INSECURE;
    case WarningReason::WARN_STRICT_CROSS_DOWNGRADE_LAX_SAMESITE:
      return url_is_secure
                 ? ContextDowngradeMetricValues::STRICT_CROSS_LAX_SECURE
                 : ContextDowngradeMetricValues::STRICT_CROSS_LAX_INSECURE;
    case WarningReason::WARN_LAX_CROSS_DOWNGRADE_STRICT_SAMESITE:
      return url_is_secure
                 ? ContextDowngradeMetricValues::LAX_CROSS_STRICT_SECURE
                 : ContextDowngradeMetricValues::LAX_CROSS_STRICT_INSECURE;
    case WarningReason::WARN_LAX_CROSS_DOWNGRADE_LAX_SAMESITE:
      return url_is_secure
                 ? ContextDowngradeMetricValues::LAX_CROSS_LAX_SECURE
                 : ContextDowngradeMetricValues::LAX_CROSS_LAX_INSECURE;
    default:
      return url_is_secure
                 ? ContextDowngradeMetricValues::NO_DOWNGRADE_SECURE
                 : ContextDowngradeMetricValues::NO_DOWNGRADE_INSECURE;
  }
}

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

  // Inclusion/exclusion
  if (IsInclude())
    base::StrAppend(&out, {"INCLUDE, "});
  if (HasExclusionReason(EXCLUDE_UNKNOWN_ERROR))
    base::StrAppend(&out, {"EXCLUDE_UNKNOWN_ERROR, "});
  if (HasExclusionReason(EXCLUDE_HTTP_ONLY))
    base::StrAppend(&out, {"EXCLUDE_HTTP_ONLY, "});
  if (HasExclusionReason(EXCLUDE_SECURE_ONLY))
    base::StrAppend(&out, {"EXCLUDE_SECURE_ONLY, "});
  if (HasExclusionReason(EXCLUDE_DOMAIN_MISMATCH))
    base::StrAppend(&out, {"EXCLUDE_DOMAIN_MISMATCH, "});
  if (HasExclusionReason(EXCLUDE_NOT_ON_PATH))
    base::StrAppend(&out, {"EXCLUDE_NOT_ON_PATH, "});
  if (HasExclusionReason(EXCLUDE_SAMESITE_STRICT))
    base::StrAppend(&out, {"EXCLUDE_SAMESITE_STRICT, "});
  if (HasExclusionReason(EXCLUDE_SAMESITE_LAX))
    base::StrAppend(&out, {"EXCLUDE_SAMESITE_LAX, "});
  if (HasExclusionReason(EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX))
    base::StrAppend(&out, {"EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX, "});
  if (HasExclusionReason(EXCLUDE_SAMESITE_NONE_INSECURE))
    base::StrAppend(&out, {"EXCLUDE_SAMESITE_NONE_INSECURE, "});
  if (HasExclusionReason(EXCLUDE_USER_PREFERENCES))
    base::StrAppend(&out, {"EXCLUDE_USER_PREFERENCES, "});
  if (HasExclusionReason(EXCLUDE_FAILURE_TO_STORE))
    base::StrAppend(&out, {"EXCLUDE_FAILURE_TO_STORE, "});
  if (HasExclusionReason(EXCLUDE_NONCOOKIEABLE_SCHEME))
    base::StrAppend(&out, {"EXCLUDE_NONCOOKIEABLE_SCHEME, "});
  if (HasExclusionReason(EXCLUDE_OVERWRITE_SECURE))
    base::StrAppend(&out, {"EXCLUDE_OVERWRITE_SECURE, "});
  if (HasExclusionReason(EXCLUDE_OVERWRITE_HTTP_ONLY))
    base::StrAppend(&out, {"EXCLUDE_OVERWRITE_HTTP_ONLY, "});
  if (HasExclusionReason(EXCLUDE_INVALID_DOMAIN))
    base::StrAppend(&out, {"EXCLUDE_INVALID_DOMAIN, "});
  if (HasExclusionReason(EXCLUDE_INVALID_PREFIX))
    base::StrAppend(&out, {"EXCLUDE_INVALID_PREFIX, "});

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

  if (HasWarningReason(WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT))
    base::StrAppend(&out, {"WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT, "});
  if (HasWarningReason(WARN_SAMESITE_NONE_INSECURE))
    base::StrAppend(&out, {"WARN_SAMESITE_NONE_INSECURE, "});
  if (HasWarningReason(WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE))
    base::StrAppend(&out, {"WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE, "});
  if (HasWarningReason(WARN_STRICT_LAX_DOWNGRADE_STRICT_SAMESITE))
    base::StrAppend(&out, {"WARN_STRICT_LAX_DOWNGRADE_STRICT_SAMESITE, "});
  if (HasWarningReason(WARN_STRICT_CROSS_DOWNGRADE_STRICT_SAMESITE))
    base::StrAppend(&out, {"WARN_STRICT_CROSS_DOWNGRADE_STRICT_SAMESITE, "});
  if (HasWarningReason(WARN_STRICT_CROSS_DOWNGRADE_LAX_SAMESITE))
    base::StrAppend(&out, {"WARN_STRICT_CROSS_DOWNGRADE_LAX_SAMESITE, "});
  if (HasWarningReason(WARN_LAX_CROSS_DOWNGRADE_STRICT_SAMESITE))
    base::StrAppend(&out, {"WARN_LAX_CROSS_DOWNGRADE_STRICT_SAMESITE, "});
  if (HasWarningReason(WARN_LAX_CROSS_DOWNGRADE_LAX_SAMESITE))
    base::StrAppend(&out, {"WARN_LAX_CROSS_DOWNGRADE_LAX_SAMESITE, "});
  if (HasWarningReason(WARN_SAMESITE_COMPAT_PAIR))
    base::StrAppend(&out, {"WARN_SAMESITE_COMPAT_PAIR, "});

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

  return out;
}

bool CookieInclusionStatus::IsValid() const {
  // Bit positions where there should not be any true bits.
  uint32_t exclusion_mask = ~0u << static_cast<int>(NUM_EXCLUSION_REASONS);
  uint32_t warning_mask = ~0u << static_cast<int>(NUM_WARNING_REASONS);
  return (exclusion_mask & exclusion_reasons_) == 0u &&
         (warning_mask & warning_reasons_) == 0u;
}

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
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;
}

}  // namespace net
