// 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.

#ifndef NET_COOKIES_COOKIE_INCLUSION_STATUS_H_
#define NET_COOKIES_COOKIE_INCLUSION_STATUS_H_

#include <stdint.h>

#include <bitset>
#include <cstdint>
#include <ostream>
#include <string>

#include "base/containers/enum_set.h"
#include "net/base/net_export.h"

namespace net {

// This class represents if a cookie was included or excluded in a cookie get or
// set operation, and if excluded why. It holds a set of reasons for
// exclusion, where cookie inclusion is represented by the absence of any
// exclusion reasons. Also marks whether a cookie should be warned about, e.g.
// for deprecation or intervention reasons.
// TODO(crbug.com/40219875): Improve serialization validation comments.
class NET_EXPORT CookieInclusionStatus {
 public:
  // Types of reasons why a cookie might be excluded.
  enum class ExclusionReason {
    EXCLUDE_UNKNOWN_ERROR = 0,

    // Statuses applied when accessing a cookie (either sending or setting):

    // Cookie was HttpOnly, but the attempted access was through a non-HTTP API.
    EXCLUDE_HTTP_ONLY = 1,
    // Cookie was Secure, but the URL was not allowed to access Secure cookies.
    EXCLUDE_SECURE_ONLY = 2,
    // The cookie's domain attribute did not match the domain of the URL
    // attempting access.
    EXCLUDE_DOMAIN_MISMATCH = 3,
    // The cookie's path attribute did not match the path of the URL attempting
    // access.
    EXCLUDE_NOT_ON_PATH = 4,
    // The cookie had SameSite=Strict, and the attempted access did not have an
    // appropriate SameSiteCookieContext.
    EXCLUDE_SAMESITE_STRICT = 5,
    // The cookie had SameSite=Lax, and the attempted access did not have an
    // appropriate SameSiteCookieContext.
    EXCLUDE_SAMESITE_LAX = 6,
    // The cookie did not specify a SameSite attribute, and therefore was
    // treated as if it were SameSite=Lax, and the attempted access did not have
    // an appropriate SameSiteCookieContext.
    EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX = 7,
    // The cookie specified SameSite=None, but it was not Secure.
    EXCLUDE_SAMESITE_NONE_INSECURE = 8,
    // Caller did not allow access to the cookie.
    EXCLUDE_USER_PREFERENCES = 9,

    // Statuses only applied when creating/setting cookies:

    // Cookie was malformed and could not be stored, due to problem(s) while
    // parsing.
    // TODO(crbug.com/40189703): Use more specific reasons for parsing errors.
    EXCLUDE_FAILURE_TO_STORE = 10,
    // Attempted to set a cookie from a scheme that does not support cookies.
    EXCLUDE_NONCOOKIEABLE_SCHEME = 11,
    // Cookie would have overwritten a Secure cookie, and was not allowed to do
    // so. (See "Leave Secure Cookies Alone":
    // https://tools.ietf.org/html/draft-west-leave-secure-cookies-alone-05 )
    EXCLUDE_OVERWRITE_SECURE = 12,
    // Cookie would have overwritten an HttpOnly cookie, and was not allowed to
    // do so.
    EXCLUDE_OVERWRITE_HTTP_ONLY = 13,
    // Cookie was set with an invalid Domain attribute.
    EXCLUDE_INVALID_DOMAIN = 14,
    // Cookie was set with an invalid __Host- or __Secure- prefix.
    EXCLUDE_INVALID_PREFIX = 15,
    /// Cookie was set with an invalid Partitioned attribute, which is only
    // valid if the cookie has a __Host- prefix.
    EXCLUDE_INVALID_PARTITIONED = 16,
    // Cookie exceeded the name/value pair size limit.
    EXCLUDE_NAME_VALUE_PAIR_EXCEEDS_MAX_SIZE = 17,
    // Cookie exceeded the attribute size limit. Note that this exclusion value
    // won't be used by code that parses cookie lines since RFC6265bis
    // indicates that large attributes should be ignored instead of causing the
    // whole cookie to be rejected. There will be a corresponding WarningReason
    // to notify users that an attribute value was ignored in that case.
    EXCLUDE_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE = 18,
    // Cookie was set with a Domain attribute containing non ASCII characters.
    EXCLUDE_DOMAIN_NON_ASCII = 19,
    // Special case for when a cookie is blocked by third-party cookie blocking
    // but the two sites are in the same First-Party Set.
    EXCLUDE_THIRD_PARTY_BLOCKED_WITHIN_FIRST_PARTY_SET = 20,
    // Cookie's source_port did not match the port of the request.
    EXCLUDE_PORT_MISMATCH = 21,
    // Cookie's source_scheme did not match the scheme of the request.
    EXCLUDE_SCHEME_MISMATCH = 22,
    // Cookie is a domain cookie and has the same name as an origin cookie on
    // this origin.
    EXCLUDE_SHADOWING_DOMAIN = 23,
    // Cookie contains ASCII control characters (including the tab character,
    // when it appears in the middle of the cookie name, value, an attribute
    // name, or an attribute value).
    EXCLUDE_DISALLOWED_CHARACTER = 24,
    // Cookie is blocked for third-party cookie phaseout.
    EXCLUDE_THIRD_PARTY_PHASEOUT = 25,
    // Cookie contains no content or only whitespace.
    EXCLUDE_NO_COOKIE_CONTENT = 26,
    // Cookie is unpartitioned and being accessed from an anonymous context
    EXCLUDE_ANONYMOUS_CONTEXT = 27,
    // Cookie was set with an invalid Path attribute (path was modified during
    // canonicalization, indicating the original path was malformed).
    EXCLUDE_INVALID_PATH = 28,
    // Cookie was rejected in parsing due to having an ambiguous serialization.
    // This can result from having an empty name and a value containing an
    // equals sign, such as a cookie line "=Foo=Bar", which is serialized as
    // "Foo=Bar" and could shadow a cookie named "Foo".
    EXCLUDE_AMBIGUOUS_SERIALIZATION = 29,
    // This should be kept last.
    MAX_EXCLUSION_REASON = EXCLUDE_AMBIGUOUS_SERIALIZATION
  };

  // Reason to warn about a cookie. Any information contained in
  // WarningReason of an included cookie may be passed to an untrusted
  // renderer.
  enum class WarningReason {
    // Of the following 3 SameSite warnings, there will be, at most, a single
    // active one.

    // Warn if a cookie with unspecified SameSite attribute is used in a
    // cross-site context.
    WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT = 0,
    // Warn if a cookie with SameSite=None is not Secure.
    WARN_SAMESITE_NONE_INSECURE,
    // Warn if a cookie with unspecified SameSite attribute is defaulted into
    // Lax and is sent on a request with unsafe method, only because it is new
    // enough to activate the Lax-allow-unsafe intervention.
    WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE,

    // Advisory warning attached when a Secure cookie is accessed from (sent to,
    // or set by) a non-cryptographic URL. This can happen if the URL is
    // potentially trustworthy (e.g. a localhost URL, or another URL that
    // the CookieAccessDelegate is configured to allow). This also applies to
    // cookies with secure source schemes when scheme binding is enabled.
    // TODO(chlily): Add metrics for how often and where this occurs.
    WARN_SECURE_ACCESS_GRANTED_NON_CRYPTOGRAPHIC,

    // The cookie would have been included prior to the spec change considering
    // redirects in the SameSite context calculation
    // (https://github.com/httpwg/http-extensions/pull/1348)
    // but would have been excluded after the spec change, due to a cross-site
    // redirect causing the SameSite context calculation to be downgraded.
    // This is applied if and only if the cookie's inclusion was changed by
    // considering redirect chains (and is applied regardless of which context
    // was actually used for the inclusion decision). This is not applied if
    // the context was downgraded but the cookie would have been
    // included/excluded in both cases.
    WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION,

    // The cookie exceeded the attribute size limit. RFC6265bis indicates that
    // large attributes should be ignored instead of causing the whole cookie
    // to be rejected. This is applied by the code that parses cookie lines and
    // notifies the user that an attribute value was ignored.
    WARN_ATTRIBUTE_VALUE_EXCEEDS_MAX_SIZE,

    // The cookie was set with a Domain attribute containing non ASCII
    // characters.
    WARN_DOMAIN_NON_ASCII,
    // The cookie's source_port did not match the port of the request.
    WARN_PORT_MISMATCH,
    // The cookie's source_scheme did not match the scheme of the request.
    WARN_SCHEME_MISMATCH,
    // The cookie's creation url is non-cryptographic but it specified the
    // "Secure" attribute. A trustworthy url may be setting this cookie, but we
    // can't confirm/deny that at the time of creation.
    WARN_TENTATIVELY_ALLOWING_SECURE_SOURCE_SCHEME,
    // Cookie is a domain cookie and has the same name as an origin cookie on
    // this origin. This cookie would be blocked if shadowing protection was
    // enabled.
    WARN_SHADOWING_DOMAIN,

    // This cookie will be blocked for third-party cookie phaseout.
    WARN_THIRD_PARTY_PHASEOUT,

    // This should be kept last.
    MAX_WARNING_REASON = WARN_THIRD_PARTY_PHASEOUT
  };

  // Types of reasons why a cookie should-have-been-blocked by 3pcd got
  // exempted and included.
  enum class ExemptionReason {
    // The default exemption reason. The cookie with this reason could either be
    // included, or blocked due to 3pcd-unrelated reasons.
    kNone = 0,
    // For user explicit settings, including User bypass.
    kUserSetting = 1,
    // k3PCDMetadata = 2,  // Deprecated
    // k3PCDDeprecationTrial = 3,  // Deprecated
    // kTopLevel3PCDDeprecationTrial = 4,  // Deprecated
    // k3PCDHeuristics = 5,  // Deprecated
    // For Enterprise Policy : CookieAllowedForUrls and BlockThirdPartyCookies.
    kEnterprisePolicy = 6,
    kStorageAccess = 7,
    kTopLevelStorageAccess = 8,
    // Allowed by the scheme.
    kScheme = 9,
    // Allowed by the sandbox 'allow-same-site-none-cookies' value.
    kSameSiteNoneCookiesInSandbox = 10,

    // Keep last.
    kMaxValue = kSameSiteNoneCookiesInSandbox
  };

  using ExclusionReasonBitset =
      base::EnumSet<ExclusionReason,
                    ExclusionReason::EXCLUDE_UNKNOWN_ERROR,
                    ExclusionReason::MAX_EXCLUSION_REASON>;
  // Mojom and some tests assume that all the exclusion reasons will fit within
  // a uint64_t. Once that's not longer true those assumptions need to be
  // updated (along with this assert).
  static_assert(ExclusionReasonBitset::kValueCount <= 64,
                "Expanding ExclusionReasons past 64 reasons requires updating "
                "usage assumptions.");
  using WarningReasonBitset =
      base::EnumSet<WarningReason,
                    WarningReason::WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT,
                    WarningReason::MAX_WARNING_REASON>;
  // Mojom and some tests assume that all the warning reasons will fit within
  // a uint64_t. Once that's not longer true those assumptions need to be
  // updated (along with this assert).
  static_assert(WarningReasonBitset::kValueCount <= 64,
                "Expanding WarningReasons past 64 reasons requires updating "
                "usage assumptions.");

  // Makes a status that says include and should not warn.
  CookieInclusionStatus();

  // Copyable.
  CookieInclusionStatus(const CookieInclusionStatus& other);
  CookieInclusionStatus& operator=(const CookieInclusionStatus& other);

  bool operator==(const CookieInclusionStatus& other) const;

  // Whether the status is to include the cookie, and has no other reasons for
  // exclusion.
  bool IsInclude() const;

  // Whether the given reason for exclusion is present.
  bool HasExclusionReason(ExclusionReason status_type) const;

  // Whether the given reason for exclusion is present, and is the ONLY reason
  // for exclusion.
  bool HasOnlyExclusionReason(ExclusionReason status_type) const;

  // Add an exclusion reason. CHECKs if `status_type` is out of range.
  void AddExclusionReason(ExclusionReason status_type);

  // Remove an exclusion reason. CHECKs if `reason` is out of range.
  void RemoveExclusionReason(ExclusionReason reason);

  // Remove multiple exclusion reasons.
  void RemoveExclusionReasons(ExclusionReasonBitset reasons);

  // Only updates exemption reason if the cookie was not already excluded and
  // doesn't already have an exemption reason.
  void MaybeSetExemptionReason(ExemptionReason reason);

  ExemptionReason exemption_reason() const { return exemption_reason_; }

  // If the cookie would have been excluded for reasons other than
  // SameSite-related reasons, don't bother warning about it (clear the
  // warning).
  void MaybeClearSameSiteWarning();

  // Whether to record the breaking downgrade metrics if the cookie is included
  // or if it's only excluded because of insufficient same-site context.
  bool ShouldRecordDowngradeMetrics() const;

  // Whether the cookie should be warned about.
  bool ShouldWarn() const;

  // Whether the given reason for warning is present.
  bool HasWarningReason(WarningReason reason) const;

  // Add an warning reason. CHECKs if `reason` is out of range.
  void AddWarningReason(WarningReason reason);

  // Remove an warning reason. CHECKs if `reason` is out of range.
  void RemoveWarningReason(WarningReason reason);

  // Used for serialization/deserialization.
  ExclusionReasonBitset exclusion_reasons() const { return exclusion_reasons_; }

  WarningReasonBitset warning_reasons() const { return warning_reasons_; }

  // Get exclusion reason(s) and warning in string format.
  std::string GetDebugString() const;

  // Checks whether the exclusion reasons are exactly the set of exclusion
  // reasons in the set. (Ignores warnings.)
  bool HasExactlyExclusionReasonsForTesting(
      ExclusionReasonBitset reasons) const;

  // Checks whether the warning reasons are exactly the set of warning
  // reasons in the set. (Ignores exclusions.)
  bool HasExactlyWarningReasonsForTesting(WarningReasonBitset reasons) const;

  // Makes a status that contains the given reasons. If the given reasons are
  // self-inconsistent, CHECKs.
  static CookieInclusionStatus MakeFromReasonsForTesting(
      ExclusionReasonBitset exclusions,
      WarningReasonBitset warnings = WarningReasonBitset(),
      ExemptionReason exemption = ExemptionReason::kNone);

  static std::optional<CookieInclusionStatus> MakeFromComponents(
      ExclusionReasonBitset exclusions,
      WarningReasonBitset warnings,
      ExemptionReason exemption);

  // Returns true if the cookie was excluded because of user preferences or
  // 3PCD.
  bool ExcludedByUserPreferencesOrTPCD() const;

  void ResetForTesting() {
    exclusion_reasons_.Clear();
    warning_reasons_.Clear();
    exemption_reason_ = ExemptionReason::kNone;
  }

 private:
  // Returns the `exclusion_reasons_` with the given `reasons` unset.
  ExclusionReasonBitset ExclusionReasonsWithout(
      ExclusionReasonBitset reasons) const;

  // If the cookie would have been excluded by reasons that are not
  // Third-party cookie phaseout related, clear the Third-party cookie phaseout
  // warning/exclusion reason in this case.
  void MaybeClearThirdPartyPhaseoutReason();

  // A bitset of the applicable exclusion reasons.
  ExclusionReasonBitset exclusion_reasons_;

  // A bitset of the applicable warning reasons.
  WarningReasonBitset warning_reasons_;

  // A cookie can only have at most one exemption reason.
  ExemptionReason exemption_reason_ = ExemptionReason::kNone;
};

NET_EXPORT inline std::ostream& operator<<(
    std::ostream& os,
    const CookieInclusionStatus& status) {
  return os << status.GetDebugString();
}

// Provided to allow gtest to create more helpful error messages, instead of
// printing hex.
inline void PrintTo(const CookieInclusionStatus& cis, std::ostream* os) {
  *os << cis;
}

}  // namespace net

#endif  // NET_COOKIES_COOKIE_INCLUSION_STATUS_H_
