// 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 "content/browser/renderer_host/cookie_utils.h"

#include <algorithm>
#include <cstddef>
#include <ostream>
#include <string>

#include "base/feature_list.h"
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/unguessable_token.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/navigation_or_document_handle.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/page_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/cookie_access_details.h"
#include "content/public/browser/legacy_tech_cookie_issue_details.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "net/cookies/cookie_constants.h"
#include "net/cookies/cookie_inclusion_status.h"
#include "net/cookies/cookie_setting_override.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom-shared.h"
#include "url/gurl.h"

namespace content {

namespace {

void PotentiallyRecordNonAsciiCookieNameValue(
    RenderFrameHost* rfh,
    CookieAccessDetails::Type access_type,
    const std::string& name,
    const std::string& value) {
  CHECK(rfh);

  if (access_type != CookieAccessDetails::Type::kChange) {
    return;
  }

  // Our data collection policy disallows collecting UKMs while prerendering.
  // See //content/browser/preloading/prerender/README.md and ask the team to
  // explore options to record data for prerendering pages if we need to
  // support the case.
  if (rfh->IsInLifecycleState(RenderFrameHost::LifecycleState::kPrerendering)) {
    return;
  }

  bool name_has_non_ascii = !base::IsStringASCII(name);
  bool value_has_non_ascii = !base::IsStringASCII(value);

  if (name_has_non_ascii || value_has_non_ascii) {
    ukm::SourceId source_id = rfh->GetPageUkmSourceId();

    auto event = ukm::builders::CookieHasNonAsciiCharacter(source_id);

    // The event itself is what we're interested in, the value of "true" here
    // can be ignored.
    if (name_has_non_ascii) {
      event.SetName(true);
    }

    if (value_has_non_ascii) {
      event.SetValue(true);
    }

    event.Record(ukm::UkmRecorder::Get());
  }
}

void PotentiallyRecordCookieOriginMismatch(
    RenderFrameHost* rfh,
    CookieAccessDetails::Type access_type,
    const net::CookieInclusionStatus& status) {
  CHECK(rfh);

  if (access_type != CookieAccessDetails::Type::kRead) {
    return;
  }

  // Our data collection policy disallows collecting UKMs while prerendering.
  // See //content/browser/preloading/prerender/README.md and ask the team to
  // explore options to record data for prerendering pages if we need to
  // support the case.
  if (rfh->IsInLifecycleState(RenderFrameHost::LifecycleState::kPrerendering)) {
    return;
  }
  const bool port_mismatch =
      status.HasWarningReason(
          net::CookieInclusionStatus::WarningReason::WARN_PORT_MISMATCH) ||
      status.HasExclusionReason(
          net::CookieInclusionStatus::ExclusionReason::EXCLUDE_PORT_MISMATCH);

  const bool scheme_mismatch =
      status.HasWarningReason(
          net::CookieInclusionStatus::WarningReason::WARN_SCHEME_MISMATCH) ||
      status.HasExclusionReason(
          net::CookieInclusionStatus::ExclusionReason::EXCLUDE_SCHEME_MISMATCH);

  if (port_mismatch || scheme_mismatch) {
    ukm::SourceId source_id = rfh->GetPageUkmSourceId();

    auto event = ukm::builders::Cookies_Blocked_DueToOriginMismatch(source_id);

    // The event itself is what we're interested in, the value of "true" here
    // can be ignored.
    if (port_mismatch) {
      event.SetPortMismatch(true);
    }

    if (scheme_mismatch) {
      event.SetSchemeMismatch(true);
    }

    event.Record(ukm::UkmRecorder::Get());
  }
}

void RecordCookiesExemptedByTopLevelStorage(ukm::SourceId source_id,
                                            int affected_cookies_in_request) {
  ukm::builders::RequestStorageAccessFor_TopLevelStorageIsExemptionReason(
      source_id)
      .SetNumberOfCookies(ukm::GetExponentialBucketMin(
          affected_cookies_in_request, /*bucket_spacing=*/2.0))
      .Record(ukm::UkmRecorder::Get());
}


// Records the PartitionedCookiePresentV3 UKM. It ignores prerendering pages.
void RecordPartitionedCookieUseV3UKM(RenderFrameHost* rfh,
                                     const ukm::SourceId& source_id) {
  if (rfh->IsInLifecycleState(RenderFrameHost::LifecycleState::kPrerendering)) {
    return;
  }
  ukm::builders::PartitionedCookiePresentV3(source_id)
      .SetPartitionedCookiePresentV3(true)
      .Record(ukm::UkmRecorder::Get());
}


void RecordRedirectContextDowngradeUKM(RenderFrameHost* rfh,
                                       CookieAccessDetails::Type access_type,
                                       const net::CanonicalCookie& cookie,
                                       const GURL& url) {
  CHECK(rfh);

  // Our data collection policy disallows collecting UKMs while prerendering.
  // See //content/browser/preloading/prerender/README.md and ask the team to
  // explore options to record data for prerendering pages if we need to
  // support the case.
  if (rfh->IsInLifecycleState(RenderFrameHost::LifecycleState::kPrerendering)) {
    return;
  }

  ukm::SourceId source_id = rfh->GetPageUkmSourceId();

  int64_t samesite_value = static_cast<int64_t>(cookie.SameSite());
  if (access_type == CookieAccessDetails::Type::kRead) {
    base::TimeDelta cookie_age = base::Time::Now() - cookie.CreationDate();

    ukm::builders::SamesiteRedirectContextDowngrade(source_id)
        .SetSamesiteValueReadPerCookie(samesite_value)
        .SetAgePerCookie(
            ukm::GetExponentialBucketMinForUserTiming(cookie_age.InMinutes()))
        .Record(ukm::UkmRecorder::Get());
  } else {
    CHECK(access_type == CookieAccessDetails::Type::kChange);
    ukm::builders::SamesiteRedirectContextDowngrade(source_id)
        .SetSamesiteValueWritePerCookie(samesite_value)
        .Record(ukm::UkmRecorder::Get());
  }
}

bool ShouldReportDevToolsIssueForStatus(
    const net::CookieInclusionStatus& status) {
  return status.ShouldWarn() ||
         status.HasExclusionReason(net::CookieInclusionStatus::ExclusionReason::
                                       EXCLUDE_DOMAIN_NON_ASCII) ||
         status.HasExclusionReason(net::CookieInclusionStatus::ExclusionReason::
                                       EXCLUDE_PORT_MISMATCH) ||
         status.HasExclusionReason(net::CookieInclusionStatus::ExclusionReason::
                                       EXCLUDE_SCHEME_MISMATCH) ||
         status.HasExclusionReason(
             net::CookieInclusionStatus::ExclusionReason::
                 EXCLUDE_THIRD_PARTY_BLOCKED_WITHIN_FIRST_PARTY_SET) ||
         status.HasExclusionReason(net::CookieInclusionStatus::ExclusionReason::
                                       EXCLUDE_THIRD_PARTY_PHASEOUT);
}

}  // namespace

void SplitCookiesIntoAllowedAndBlocked(
    const network::mojom::CookieAccessDetailsPtr& cookie_details,
    CookieAccessDetails::Source source,
    CookieAccessDetails* allowed,
    CookieAccessDetails* blocked) {
  // For some cases `site_for_cookies` representative url is empty when
  // OnCookieAccess is triggered for a third party. For example iframe third
  // party accesses cookies when TPCD Metadata allows third party cookie access.
  //
  // Make `first_party_url` considering both `top_frame_origin` and
  // `site_for_cookies` which is similar with GetFirstPartyURL() in
  // components/content_settings/core/common/cookie_settings_base.h.
  // If the `top_frame_origin` is non-opaque, it is chosen; otherwise, the
  // `site_for_cookies` representative url is used.
  const GURL first_party_url =
      cookie_details->top_frame_origin.opaque()
          ? cookie_details->site_for_cookies.RepresentativeUrl()
          : cookie_details->top_frame_origin.GetURL();

  *allowed = CookieAccessDetails({cookie_details->type,
                                  cookie_details->url,
                                  first_party_url,
                                  {},
                                  /* blocked_by_policy=*/false,
                                  cookie_details->is_ad_tagged,
                                  cookie_details->cookie_setting_overrides,
                                  cookie_details->site_for_cookies,
                                  source});
  int allowed_count = std::ranges::count_if(
      cookie_details->cookie_list,
      [](const network::mojom::CookieOrLineWithAccessResultPtr&
             cookie_and_access_result) {
        // "Included" cookies have no exclusion reasons so we don't also have to
        // check for
        // !(net::CookieInclusionStatus::ExclusionReason::EXCLUDE_USER_PREFERENCES).
        return cookie_and_access_result->access_result.status.IsInclude();
      });
  allowed->cookie_access_result_list.reserve(allowed_count);

  *blocked = CookieAccessDetails({cookie_details->type,
                                  cookie_details->url,
                                  first_party_url,
                                  {},
                                  /* blocked_by_policy=*/true,
                                  cookie_details->is_ad_tagged,
                                  cookie_details->cookie_setting_overrides,
                                  cookie_details->site_for_cookies,
                                  source});
  int blocked_count = std::ranges::count_if(
      cookie_details->cookie_list,
      [](const network::mojom::CookieOrLineWithAccessResultPtr&
             cookie_and_access_result) {
        return cookie_and_access_result->access_result.status
            .ExcludedByUserPreferencesOrTPCD();
      });
  blocked->cookie_access_result_list.reserve(blocked_count);

  for (const auto& cookie_and_access_result : cookie_details->cookie_list) {
    if (cookie_and_access_result->access_result.status
            .ExcludedByUserPreferencesOrTPCD()) {
      blocked->cookie_access_result_list.emplace_back(
          std::move(cookie_and_access_result->cookie_or_line->get_cookie()),
          cookie_and_access_result->access_result);
    } else if (cookie_and_access_result->access_result.status.IsInclude()) {
      allowed->cookie_access_result_list.emplace_back(
          std::move(cookie_and_access_result->cookie_or_line->get_cookie()),
          cookie_and_access_result->access_result);
    }
  }
}

// Logs cookie issues to DevTools Issues Panel and logs events to UseCounters
// and UKM for a single cookie-accessed event.
// TODO(crbug.com/40632967): Remove when no longer needed.
void EmitCookieWarningsAndMetrics(
    RenderFrameHostImpl* rfh,
    const network::mojom::CookieAccessDetailsPtr& cookie_details) {
  RenderFrameHostImpl* root_frame_host = rfh->GetMainFrame();

  if (!root_frame_host->IsActive())
    return;

  bool samesite_treated_as_lax_cookies = false;
  bool samesite_none_insecure_cookies = false;
  bool lax_allow_unsafe_cookies = false;

  bool samesite_cookie_inclusion_changed_by_cross_site_redirect = false;

  bool partitioned_cookies_exist = false;
  bool valid_partitioned_cookies_for_ukm_exist = false;

  absl::flat_hash_set<std::string> partitioned_non_httponly_cookie_names;
  absl::flat_hash_set<std::string> httponly_cookie_names;

  bool cookie_has_not_been_refreshed_in_201_to_300_days = false;
  bool cookie_has_not_been_refreshed_in_301_to_350_days = false;
  bool cookie_has_not_been_refreshed_in_351_to_400_days = false;

  bool cookie_has_domain_non_ascii = false;

  bool cookie_set_has_empty_name = false;
  bool cookie_set_has_empty_name_and_ambiguous_value = false;

  int cookies_exempted_by_top_level_storage_access = 0;

  for (const network::mojom::CookieOrLineWithAccessResultPtr& cookie :
       cookie_details->cookie_list) {
    const net::CookieInclusionStatus& status = cookie->access_result.status;
    if (ShouldReportDevToolsIssueForStatus(status)) {
      std::optional<std::string> devtools_issue_id;
      if (status.HasExclusionReason(
              net::CookieInclusionStatus::ExclusionReason::
                  EXCLUDE_THIRD_PARTY_PHASEOUT) ||
          status.HasWarningReason(net::CookieInclusionStatus::WarningReason::
                                      WARN_THIRD_PARTY_PHASEOUT)) {
        devtools_issue_id = base::UnguessableToken::Create().ToString();
      }
      devtools_instrumentation::ReportCookieIssue(
          root_frame_host, cookie, cookie_details->url,
          cookie_details->site_for_cookies,
          cookie_details->type == CookieAccessDetails::Type::kRead
              ? blink::mojom::CookieOperation::kReadCookie
              : blink::mojom::CookieOperation::kSetCookie,
          cookie_details->devtools_request_id, devtools_issue_id);
    }

    if (cookie->access_result.status.ShouldWarn()) {
      samesite_treated_as_lax_cookies =
          samesite_treated_as_lax_cookies ||
          status.HasWarningReason(
              net::CookieInclusionStatus::WarningReason::
                  WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT) ||
          status.HasWarningReason(
              net::CookieInclusionStatus::WarningReason::
                  WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE);

      samesite_none_insecure_cookies =
          samesite_none_insecure_cookies ||
          status.HasWarningReason(net::CookieInclusionStatus::WarningReason::
                                      WARN_SAMESITE_NONE_INSECURE);

      lax_allow_unsafe_cookies =
          lax_allow_unsafe_cookies ||
          status.HasWarningReason(
              net::CookieInclusionStatus::WarningReason::
                  WARN_SAMESITE_UNSPECIFIED_LAX_ALLOW_UNSAFE);

      samesite_cookie_inclusion_changed_by_cross_site_redirect =
          samesite_cookie_inclusion_changed_by_cross_site_redirect ||
          status.HasWarningReason(
              net::CookieInclusionStatus::WarningReason::
                  WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION);
    }

    cookie_has_domain_non_ascii =
        cookie_has_domain_non_ascii ||
        status.HasWarningReason(
            net::CookieInclusionStatus::WarningReason::WARN_DOMAIN_NON_ASCII) ||
        status.HasExclusionReason(net::CookieInclusionStatus::ExclusionReason::
                                      EXCLUDE_DOMAIN_NON_ASCII);

    bool is_partitioned_cookie =
        (cookie->cookie_or_line->is_cookie() &&
         cookie->cookie_or_line->get_cookie().IsPartitioned() &&
         // Ignore nonced partition keys since this metric is meant to track
         // usage of the Partitioned attribute.
         !cookie->cookie_or_line->get_cookie().PartitionKey()->nonce());
    if (is_partitioned_cookie) {
      if (!cookie->cookie_or_line->get_cookie().IsHttpOnly() &&
          cookie->access_result.status.IsInclude()) {
        partitioned_non_httponly_cookie_names.insert(
            cookie->cookie_or_line->get_cookie().Name());
      }
      partitioned_cookies_exist = true;
      if (cookie->cookie_or_line->get_cookie().Name() !=
          "receive-cookie-deprecation") {
        valid_partitioned_cookies_for_ukm_exist = true;
      }
    }

    if (cookie->cookie_or_line->is_cookie() &&
        cookie->cookie_or_line->get_cookie().IsHttpOnly() &&
        !cookie->cookie_or_line->get_cookie().IsPartitioned()) {
      httponly_cookie_names.insert(cookie->cookie_or_line->get_cookie().Name());
    }

    if (cookie->access_result.status.exemption_reason() ==
        net::CookieInclusionStatus::ExemptionReason::kTopLevelStorageAccess) {
      cookies_exempted_by_top_level_storage_access++;
    }

    if (status.HasWarningReason(
            net::CookieInclusionStatus::WarningReason::
                WARN_CROSS_SITE_REDIRECT_DOWNGRADE_CHANGES_INCLUSION) &&
        cookie->cookie_or_line->is_cookie()) {
      RecordRedirectContextDowngradeUKM(rfh, cookie_details->type,
                                        cookie->cookie_or_line->get_cookie(),
                                        cookie_details->url);
    }

    if (cookie->cookie_or_line->is_cookie()) {
      PotentiallyRecordNonAsciiCookieNameValue(
          rfh, cookie_details->type,
          cookie->cookie_or_line->get_cookie().Name(),
          cookie->cookie_or_line->get_cookie().Value());
    }

    PotentiallyRecordCookieOriginMismatch(rfh, cookie_details->type, status);

    // In order to anticipate the potential effects of the expiry limit in
    // rfc6265bis, we need to check how long it's been since the cookie was
    // refreshed (if LastUpdateDate is populated). These three buckets were
    // picked so we could engage sites with some granularity around urgency.
    // We ignore the space under 200 days as these cookies are not at risk
    // of expiring and we ignore the space over 400 days as these cookies
    // have already expired. Metrics will take 200 days from M103 to populate.
    base::Time last_update_date =
        cookie->cookie_or_line->is_cookie()
            ? cookie->cookie_or_line->get_cookie().LastUpdateDate()
            : base::Time();
    if (!last_update_date.is_null()) {
      int days_since_refresh = (base::Time::Now() - last_update_date).InDays();
      cookie_has_not_been_refreshed_in_201_to_300_days |=
          days_since_refresh > 200 && days_since_refresh <= 300;
      cookie_has_not_been_refreshed_in_301_to_350_days |=
          days_since_refresh > 300 && days_since_refresh <= 350;
      cookie_has_not_been_refreshed_in_351_to_400_days |=
          days_since_refresh > 350 && days_since_refresh <= 400;
    }

    if (cookie_details->type == CookieAccessDetails::Type::kChange &&
        cookie->cookie_or_line->is_cookie() &&
        cookie->cookie_or_line->get_cookie().Name().empty()) {
      cookie_set_has_empty_name = true;
      if (cookie->cookie_or_line->get_cookie().Value().contains('=')) {
        cookie_set_has_empty_name_and_ambiguous_value = true;
      }
    }
  }

  if (samesite_treated_as_lax_cookies) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh, blink::mojom::WebFeature::kCookieNoSameSite);
  }

  if (samesite_none_insecure_cookies) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh, blink::mojom::WebFeature::kCookieInsecureAndSameSiteNone);
  }

  if (lax_allow_unsafe_cookies) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh, blink::mojom::WebFeature::kLaxAllowingUnsafeCookies);
  }

  if (samesite_cookie_inclusion_changed_by_cross_site_redirect) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh, blink::mojom::WebFeature::
                 kSameSiteCookieInclusionChangedByCrossSiteRedirect);
  }

  if (partitioned_cookies_exist) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh, blink::mojom::WebFeature::kPartitionedCookies);
  }
  if (valid_partitioned_cookies_for_ukm_exist &&
      !rfh->IsInLifecycleState(
          RenderFrameHost::LifecycleState::kPrerendering)) {
    PageImpl& page = rfh->GetPage();
    if (!page.has_recorded_partitioned_cookie_use()) {
      RecordPartitionedCookieUseV3UKM(rfh, rfh->GetPageUkmSourceId());
      page.set_has_recorded_partitioned_cookie_use(true);
    }
  }

  if (partitioned_cookies_exist && !httponly_cookie_names.empty()) {
    bool has_shadowed_cookie = false;
    for (const auto& cookie_name : httponly_cookie_names) {
      if (partitioned_non_httponly_cookie_names.contains(cookie_name)) {
        has_shadowed_cookie = true;
        break;
      }
    }
    if (has_shadowed_cookie) {
      GetContentClient()->browser()->LogWebFeatureForCurrentPage(
          rfh, blink::mojom::WebFeature::
                   kHttpOnlyCookieShadowedByNonHttpOnlyPartitioned);
    }
  }

  if (cookie_has_not_been_refreshed_in_201_to_300_days) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh,
        blink::mojom::WebFeature::kCookieHasNotBeenRefreshedIn201To300Days);
  }

  if (cookie_has_not_been_refreshed_in_301_to_350_days) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh,
        blink::mojom::WebFeature::kCookieHasNotBeenRefreshedIn301To350Days);
  }

  if (cookie_has_not_been_refreshed_in_351_to_400_days) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh,
        blink::mojom::WebFeature::kCookieHasNotBeenRefreshedIn351To400Days);
  }

  if (cookie_has_domain_non_ascii) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh, blink::mojom::WebFeature::kCookieDomainNonASCII);
  }

  if (cookie_set_has_empty_name) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh, blink::mojom::WebFeature::kSetCookieWithEmptyName);
  }

  if (cookie_set_has_empty_name_and_ambiguous_value) {
    GetContentClient()->browser()->LogWebFeatureForCurrentPage(
        rfh,
        blink::mojom::WebFeature::kSetCookieWithEmptyNameAndAmbiguousValue);
  }

  if (cookies_exempted_by_top_level_storage_access) {
    RecordCookiesExemptedByTopLevelStorage(
        rfh->GetPageUkmSourceId(),
        cookies_exempted_by_top_level_storage_access);
  }
}

}  // namespace content
