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

#include "components/content_settings/browser/ui/cookie_controls_controller.h"

#include <limits>
#include <memory>
#include <string>

#include "base/containers/lru_cache.h"
#include "base/feature_list.h"
#include "base/features.h"
#include "base/functional/bind.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/observer_list.h"
#include "components/browsing_data/content/browsing_data_helper.h"
#include "components/content_settings/browser/page_specific_content_settings.h"
#include "components/content_settings/browser/ui/cookie_controls_view.h"
#include "components/content_settings/core/browser/content_settings_utils.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "components/content_settings/core/common/cookie_blocking_3pcd_status.h"
#include "components/content_settings/core/common/cookie_controls_enforcement.h"
#include "components/content_settings/core/common/cookie_controls_state.h"
#include "components/content_settings/core/common/features.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/content_settings/core/common/third_party_site_data_access_type.h"
#include "components/prefs/pref_service.h"
#include "components/privacy_sandbox/tracking_protection_settings.h"
#include "components/site_engagement/content/site_engagement_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/reload_type.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "net/cookies/site_for_cookies.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_source_id.h"

namespace {

using ::base::UserMetricsAction;
using ::site_engagement::SiteEngagementService;

constexpr char kEntryPointAnimatedKey[] = "entry_point_animated";
constexpr char kLastExpirationKey[] = "last_expiration";
constexpr char kLastVisitedActiveException[] = "last_visited_active_exception";
constexpr char kActivationsCountKey[] = "activations_count_key";

using CacheSizeType =
    base::LRUCacheSet<content_settings::AccessDetails>::size_type;
constexpr CacheSizeType kAccessDetailsCacheSize = 1000;
constexpr CacheSizeType kMaximumCacheCapacity =
    std::numeric_limits<CacheSizeType>::max();

base::Value::Dict GetMetadata(HostContentSettingsMap* settings_map,
                              const GURL& url) {
  base::Value stored_value = settings_map->GetWebsiteSetting(
      url, url, ContentSettingsType::COOKIE_CONTROLS_METADATA);
  if (!stored_value.is_dict()) {
    return base::Value::Dict();
  }

  return std::move(stored_value.GetDict());
}

bool WasEntryPointAlreadyAnimated(const base::Value::Dict& metadata) {
  std::optional<bool> entry_point_animated =
      metadata.FindBool(kEntryPointAnimatedKey);
  return entry_point_animated.has_value() && entry_point_animated.value();
}

int GetActivationCount(const base::Value::Dict& metadata) {
  return metadata.FindInt(kActivationsCountKey).value_or(0);
}

bool HasExceptionExpiredSinceLastVisit(const base::Value::Dict& metadata) {
  auto last_expiration = base::ValueToTime(metadata.Find(kLastExpirationKey))
                             .value_or(base::Time());
  auto last_visited =
      base::ValueToTime(metadata.Find(kLastVisitedActiveException))
          .value_or(base::Time());

  return !last_expiration.is_null()  // Exception should have an expiration,
         && last_expiration < base::Time::Now()  // that has already expired,
         && !last_visited.is_null()              // from a previous visit,
         && last_visited < last_expiration;      // with no visit since.
}

void ApplyMetadataChanges(HostContentSettingsMap* settings_map,
                          const GURL& url,
                          base::Value::Dict&& dict) {
  settings_map->SetWebsiteSettingDefaultScope(
      url, url, ContentSettingsType::COOKIE_CONTROLS_METADATA,
      base::Value(std::move(dict)));
}

ThirdPartySiteDataAccessType GetSiteDataAccessType(int allowed_sites,
                                                   int blocked_sites) {
  if (blocked_sites > 0) {
    return ThirdPartySiteDataAccessType::kAnyBlockedThirdPartySiteAccesses;
  }
  if (allowed_sites > 0) {
    return ThirdPartySiteDataAccessType::kAnyAllowedThirdPartySiteAccesses;
  }
  return ThirdPartySiteDataAccessType::kNoThirdPartySiteAccesses;
}

}  // namespace

namespace content_settings {

CookieControlsController::CookieControlsController(
    scoped_refptr<CookieSettings> cookie_settings,
    scoped_refptr<CookieSettings> original_cookie_settings,
    HostContentSettingsMap* settings_map,
    privacy_sandbox::TrackingProtectionSettings* tracking_protection_settings,
    bool is_incognito_profile)
    : cookie_settings_(cookie_settings),
      original_cookie_settings_(original_cookie_settings),
      settings_map_(settings_map),
      tracking_protection_settings_(tracking_protection_settings) {
  CHECK(cookie_settings_);
  CHECK(tracking_protection_settings_);
  cookie_observation_.Observe(cookie_settings_.get());
}

CookieControlsController::Status::Status(
    CookieControlsState controls_state,
    CookieControlsEnforcement enforcement,
    CookieBlocking3pcdStatus blocking_status,
    base::Time expiration)
    : controls_state(controls_state),
      enforcement(enforcement),
      blocking_status(blocking_status),
      expiration(expiration) {}
CookieControlsController::Status::~Status() = default;

CookieControlsController::~CookieControlsController() = default;

void CookieControlsController::OnUiClosing() {
  auto* web_contents = GetWebContents();
  if (should_reload_ && web_contents && !web_contents->IsBeingDestroyed()) {
    web_contents->GetController().Reload(content::ReloadType::NORMAL, true);
  }
  should_reload_ = false;
}

void CookieControlsController::Update(content::WebContents* web_contents) {
  DCHECK(web_contents);
  if (!tab_observer_ || GetWebContents() != web_contents) {
    tab_observer_ = std::make_unique<TabObserver>(this, web_contents);
    SetStateChangedViaBypass(false);
    show_icon_as_confirmation_ = false;
  }
  if (observers_.empty()) {
    return;
  }
  auto status = GetStatus(web_contents);
  const bool icon_visible =
      ShouldUserBypassIconBeVisible(status.controls_state);
  const bool should_highlight =
      ShouldHighlightUserBypass(status.controls_state);
  for (auto& observer : observers_) {
    observer.OnStatusChanged(status.controls_state, status.enforcement,
                             status.blocking_status, status.expiration);
    observer.OnCookieControlsIconStatusChanged(
        icon_visible, status.controls_state, status.blocking_status,
        should_highlight);
  }
}

CookieControlsController::Status CookieControlsController::GetStatus(
    content::WebContents* web_contents) {
  if (!cookie_settings_->ShouldBlockThirdPartyCookies()) {
    return {CookieControlsState::kHidden,
            CookieControlsEnforcement::kNoEnforcement,
            CookieBlocking3pcdStatus::kNotIn3pcd, base::Time()};
  }

  const GURL& url = web_contents->GetLastCommittedURL();
  if (url.SchemeIs(content::kChromeUIScheme) ||
      url.SchemeIs(kExtensionScheme)
#if BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
      || url.SchemeIs(kArkwebExtensionScheme)
#endif
      ) {
    return {CookieControlsState::kHidden,
            CookieControlsEnforcement::kNoEnforcement,
            CookieBlocking3pcdStatus::kNotIn3pcd, base::Time()};
  }

  auto blocking_status = CookieBlocking3pcdStatus::kNotIn3pcd;
  if (cookie_settings_->AreThirdPartyCookiesLimited()) {
    blocking_status = CookieBlocking3pcdStatus::kLimited;
  } else if (tracking_protection_settings_->AreAllThirdPartyCookiesBlocked()) {
    blocking_status = CookieBlocking3pcdStatus::kAll;
  }

  SettingInfo info;
  bool cookies_allowed =
      cookie_settings_->IsThirdPartyAccessAllowed(url, &info);
  CookieControlsEnforcement enforcement =
      GetEnforcementForThirdPartyCookieBlocking(blocking_status, url, info,
                                                cookies_allowed);

  CookieControlsState controls_state;
  if (enforcement == CookieControlsEnforcement::kEnforcedByTpcdGrant) {
    controls_state = CookieControlsState::kHidden;
  } else {
    controls_state = cookies_allowed ? CookieControlsState::kAllowed3pc
                                     : CookieControlsState::kBlocked3pc;
  }

  return {controls_state, enforcement, blocking_status,
          info.metadata.expiration()};
}

CookieControlsEnforcement
CookieControlsController::GetEnforcementForThirdPartyCookieBlocking(
    CookieBlocking3pcdStatus status,
    const GURL url,
    const SettingInfo& info,
    bool cookies_allowed) {
  const bool is_default_setting =
      info.primary_pattern == ContentSettingsPattern::Wildcard() &&
      info.secondary_pattern == ContentSettingsPattern::Wildcard();

  // The UI can reset only host-scoped (without wildcards in the domain) or
  // site-scoped exceptions.
  const bool host_or_site_scoped_exception =
      !info.secondary_pattern.HasDomainWildcard() ||
      info.secondary_pattern ==
          ContentSettingsPattern::FromURLToSchemefulSitePattern(url);

  // Rules from regular mode can't be temporarily overridden in off the record
  // profiles.
  bool exception_exists_in_regular_profile = false;
  if (cookies_allowed && original_cookie_settings_) {
    SettingInfo original_info;
    original_cookie_settings_->IsThirdPartyAccessAllowed(url, &original_info);

    exception_exists_in_regular_profile =
        original_info.primary_pattern != ContentSettingsPattern::Wildcard() ||
        original_info.secondary_pattern != ContentSettingsPattern::Wildcard();
  }

  if (info.source == SettingSource::kTpcdGrant &&
      status == CookieBlocking3pcdStatus::kLimited) {
    return CookieControlsEnforcement::kEnforcedByTpcdGrant;
  } else if (info.source == SettingSource::kPolicy) {
    return CookieControlsEnforcement::kEnforcedByPolicy;
  } else if (info.source == SettingSource::kExtension) {
    return CookieControlsEnforcement::kEnforcedByExtension;
  } else if (exception_exists_in_regular_profile ||
             (!is_default_setting && !host_or_site_scoped_exception)) {
    // If the exception cannot be reset in-context because of the nature of the
    // setting, display as managed by setting.
    return CookieControlsEnforcement::kEnforcedByCookieSetting;
  } else {
    return CookieControlsEnforcement::kNoEnforcement;
  }
}

bool CookieControlsController::HasOriginSandboxedTopLevelDocument() const {
  content::RenderFrameHost* rfh = GetWebContents()->GetPrimaryMainFrame();
  // If the WebContents has not fully initialized the RenderFrameHost yet.
  // TODO(crbug.com/346386726): Remove the HasPolicyContainerHost() call once
  //   RenderFrameHost initialization order is fixed.
  if (!rfh || !rfh->HasPolicyContainerHost()) {
    // In that case, we fall back on assuming it is not sandboxed.
    // Since this is only for determining whether to render the User Bypass
    // icon this fallback is acceptable.
    return false;
  }

  return rfh->IsSandboxed(network::mojom::WebSandboxFlags::kOrigin);
}

void CookieControlsController::OnCookieBlockingEnabledForSite(
    bool block_third_party_cookies) {
  const GURL& url = GetWebContents()->GetLastCommittedURL();
  should_reload_ = true;
  if (block_third_party_cookies) {
    base::RecordAction(UserMetricsAction("CookieControls.Bubble.TurnOn"));
    cookie_settings_->ResetThirdPartyCookieSetting(url);
    Update(GetWebContents());
    return;
  }

  CHECK(!block_third_party_cookies);
  base::RecordAction(UserMetricsAction("CookieControls.Bubble.TurnOff"));
  cookie_settings_->SetCookieSettingForUserBypass(url);
  Update(GetWebContents());
  // Record expiration metadata for the newly created exception, and increased
  // the activation count.
  base::Value::Dict metadata = GetMetadata(settings_map_, url);
  metadata.Set(kLastExpirationKey,
               base::TimeToValue(GetStatus(GetWebContents()).expiration));
  metadata.Set(kActivationsCountKey, GetActivationCount(metadata) + 1);
  ApplyMetadataChanges(settings_map_, url, std::move(metadata));

  RecordActivationMetrics();
}

void CookieControlsController::OnEntryPointAnimated() {
  // sanity check if WebContents was instantiated (update method called before)
  // TODO(b/341972754): refactor this to be handled properly via update method
  // for all Android corner cases.
  if (GetWebContents() == nullptr) {
    return;
  }
  const GURL& url = GetWebContents()->GetLastCommittedURL();
  base::Value::Dict metadata = GetMetadata(settings_map_, url);
  metadata.Set(kEntryPointAnimatedKey, base::Value(true));
  ApplyMetadataChanges(settings_map_, url, std::move(metadata));
}

bool CookieControlsController::StateChangedViaBypass() {
  return user_changed_ub_state_;
}

void CookieControlsController::SetStateChangedViaBypass(bool changed) {
  // Avoid a toggle back and forth being marked as "changed".
  user_changed_ub_state_ = changed && !user_changed_ub_state_;
}

int CookieControlsController::GetAllowedThirdPartyCookiesSitesCount() const {
  auto* pscs = content_settings::PageSpecificContentSettings::GetForPage(
      GetWebContents()->GetPrimaryPage());
  if (!pscs) {
    return 0;
  }

  return browsing_data::GetUniqueThirdPartyCookiesHostCount(
      GetWebContents()->GetLastCommittedURL(),
      *(pscs->allowed_browsing_data_model()));
}

int CookieControlsController::GetBlockedThirdPartyCookiesSitesCount() const {
  auto* pscs = content_settings::PageSpecificContentSettings::GetForPage(
      GetWebContents()->GetPrimaryPage());
  if (!pscs) {
    return 0;
  }

  return browsing_data::GetUniqueThirdPartyCookiesHostCount(
      GetWebContents()->GetLastCommittedURL(),
      *(pscs->blocked_browsing_data_model()));
}

int CookieControlsController::GetStatefulBounceCount() const {
  auto* pscs = content_settings::PageSpecificContentSettings::GetForPage(
      GetWebContents()->GetPrimaryPage());
  if (pscs) {
    return pscs->stateful_bounce_count();
  } else {
    return 0;
  }
}

void CookieControlsController::UpdateUserBypass() {
  if (observers_.empty()) {
    return;
  }
  auto status = GetStatus(GetWebContents());
  const bool icon_visible =
      ShouldUserBypassIconBeVisible(status.controls_state);
  const bool should_highlight =
      ShouldHighlightUserBypass(status.controls_state);
  for (auto& observer : observers_) {
    observer.OnCookieControlsIconStatusChanged(
        icon_visible, status.controls_state, status.blocking_status,
        should_highlight);
  }
}

void CookieControlsController::UpdateLastVisitedSitesMap() {
  // Cache whether the expiration has expired since last visit before updating
  // the last visited metadata.
  const GURL& url = GetWebContents()->GetLastCommittedURL();
  has_exception_expired_since_last_visit_ =
      HasExceptionExpiredSinceLastVisit(GetMetadata(settings_map_, url));

  // We only care about visits with active expirations, if there is an active
  // exception, update the last visited time, otherwise clear it.
  base::Value::Dict metadata = GetMetadata(settings_map_, url);
  auto status = GetStatus(GetWebContents());
  if (status.controls_state == CookieControlsState::kAllowed3pc) {
    metadata.Set(kLastVisitedActiveException,
                 base::TimeToValue(base::Time::Now()));
  } else {
    metadata.Remove(kLastVisitedActiveException);
  }
  ApplyMetadataChanges(settings_map_, url, std::move(metadata));
}

void CookieControlsController::UpdatePageReloadStatus(
    int recent_reloads_count) {
  if (StateChangedViaBypass() && recent_reloads_count > 0) {
    waiting_for_page_load_finish_ = true;
    show_icon_as_confirmation_ = true;
  } else {
    show_icon_as_confirmation_ = false;
  }
  SetStateChangedViaBypass(false);
  recent_reloads_count_ = recent_reloads_count;

  if (recent_reloads_count_ >= features::kUserBypassUIReloadCount.Get()) {
    for (auto& observer : observers_) {
      observer.OnReloadThresholdExceeded();
    }
  }
}

void CookieControlsController::OnBubbleCloseTriggered() {
  for (auto& observer : observers_) {
    observer.OnBubbleCloseTriggered();
  }
}

void CookieControlsController::OnPageFinishedLoading() {
  if (!waiting_for_page_load_finish_) {
    return;
  }
  waiting_for_page_load_finish_ = false;

  // Ensure the bubble is closed before subsequent calls are made to update the
  // UI.
  OnBubbleCloseTriggered();
  for (auto& observer : observers_) {
    observer.OnFinishedPageReloadWithChangedSettings();
  }
}

void CookieControlsController::OnThirdPartyCookieBlockingChanged(
    bool block_third_party_cookies) {
  if (GetWebContents()) {
    UpdateUserBypass();
  }
}

void CookieControlsController::OnCookieSettingChanged() {
  if (GetWebContents()) {
    UpdateUserBypass();
  }
}

content::WebContents* CookieControlsController::GetWebContents() const {
  if (!tab_observer_) {
    return nullptr;
  }
  return tab_observer_->content::WebContentsObserver::web_contents();
}

void CookieControlsController::AddObserver(CookieControlsObserver* obs) {
  observers_.AddObserver(obs);
}

void CookieControlsController::RemoveObserver(CookieControlsObserver* obs) {
  observers_.RemoveObserver(obs);
}

double CookieControlsController::GetSiteEngagementScore() {
  auto* web_contents = GetWebContents();
  return SiteEngagementService::Get(web_contents->GetBrowserContext())
      ->GetScore(web_contents->GetVisibleURL());
}

void CookieControlsController::RecordActivationMetrics() {
  const GURL& url = GetWebContents()->GetLastCommittedURL();

  // Metrics, related to confidence signals:
  base::UmaHistogramBoolean(
      "Privacy.CookieControlsActivated.SaaRequested",
      cookie_settings_->HasAnyFrameRequestedStorageAccess(url));
  base::UmaHistogramCounts100(
      "Privacy.CookieControlsActivated.PageRefreshCount",
      recent_reloads_count_);
  base::UmaHistogramExactLinear(
      "Privacy.CookieControlsActivated.SiteEngagementScore",
      GetSiteEngagementScore(), 100);

  auto site_data_access_type =
      GetSiteDataAccessType(GetAllowedThirdPartyCookiesSitesCount(),
                            GetBlockedThirdPartyCookiesSitesCount());
  base::UmaHistogramEnumeration(
      "Privacy.CookieControlsActivated.SiteDataAccessType",
      site_data_access_type);

  // Record activation UKM.
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  auto ukm_source_id =
      GetWebContents()->GetPrimaryMainFrame()->GetPageUkmSourceId();
  ukm::builders::ThirdPartyCookies_CookieControlsActivated(ukm_source_id)
      .SetFedCmInitiated(false)
      .SetStorageAccessAPIRequested(
          cookie_settings_->HasAnyFrameRequestedStorageAccess(url))
      .SetPageRefreshCount(std::clamp(recent_reloads_count_, 0, 10))
      .SetRepeatedActivation(
          GetActivationCount(GetMetadata(settings_map_, url)) > 1)
      .SetSiteEngagementLevel(static_cast<uint64_t>(
          SiteEngagementService::Get(GetWebContents()->GetBrowserContext())
              ->GetEngagementLevel(url)))
      .SetThirdPartySiteDataAccessType(
          static_cast<uint64_t>(site_data_access_type))
      .Record(ukm::UkmRecorder::Get());
}

bool CookieControlsController::ShouldHighlightUserBypass(
    CookieControlsState controls_state) {
  // Highlighting is meant to draw attention to bypassing, so just return if
  // bypass has already happened.
  if (controls_state == CookieControlsState::kHidden ||
      controls_state == CookieControlsState::kAllowed3pc) {
    return false;
  }

  auto* web_contents = GetWebContents();
  // We don't want to show UI animation, and IPH in this case as we can't
  // persist their usage cross-session. This puts us at high risk of
  // over-triggering noisy UI and annoying users.
  if (web_contents->GetBrowserContext()->IsOffTheRecord()) {
    return false;
  }

  const GURL& url = web_contents->GetLastCommittedURL();
  if (cookie_settings_->HasAnyFrameRequestedStorageAccess(url)) {
    return false;
  }

  // If the user is returning to the site after their previous exception has
  // expired, highlight user bypass. The order of this check is important,
  // as the site may now be using SAA / FedCM instead of relying on 3PC. It
  // should also come before any check for whether the entrypoint was already
  // animated.
  if (has_exception_expired_since_last_visit_) {
    return true;
  }

  // Check if the entry point was already animated for the site.
  if (WasEntryPointAlreadyAnimated(GetMetadata(settings_map_, url))) {
    return false;
  }

  if (recent_reloads_count_ >= features::kUserBypassUIReloadCount.Get()) {
    return true;
  }

  if (SiteEngagementService::IsEngagementAtLeast(
          GetSiteEngagementScore(), blink::mojom::EngagementLevel::HIGH)) {
    return true;
  }

  return false;
}

bool CookieControlsController::ShouldUserBypassIconBeVisible(
    CookieControlsState controls_state) {
  if (controls_state == CookieControlsState::kHidden) {
    return false;
  }
  return show_icon_as_confirmation_ ||
         controls_state == CookieControlsState::kAllowed3pc ||
         // 3PC blocking prevents SameSite=None cookies from being sent when the
         // top-level document is sandboxed without `allow-origin`. For instance
         // when loaded with: `Content-Security-Policy: sandbox`. In that case,
         // we render the UI to allow the user to opt into sending SameSite=None
         // cookies again in those contexts.
         HasOriginSandboxedTopLevelDocument() ||
         // If no 3P sites have attempted to access site data, nor were any
         // stateful bounces recorded, the icon should not be displayed. Take
         // into account both allow and blocked counts, since the breakage might
         // be related to storage partitioning. Partitioned site will be allowed
         // to access partitioned storage.
         SiteDataAccessAttempted();
}

bool CookieControlsController::SiteDataAccessAttempted() {
  return GetStatefulBounceCount() || GetAllowedThirdPartyCookiesSitesCount() ||
         GetBlockedThirdPartyCookiesSitesCount();
}

CookieControlsController::TabObserver::TabObserver(
    CookieControlsController* cookie_controls,
    content::WebContents* web_contents)
    : content_settings::PageSpecificContentSettings::SiteDataObserver(
          web_contents),
      content::WebContentsObserver(web_contents),
      cookie_controls_(cookie_controls),
      // When under the ReducePPMs experiment reduce the capacity of the
      // cache and leave it practically unbounded otherwise.
      cookie_accessed_set_(base::features::IsReducePPMsEnabled()
                               ? kAccessDetailsCacheSize
                               : kMaximumCacheCapacity) {
  last_visited_url_ =
      content::WebContentsObserver::web_contents()->GetVisibleURL();
}

CookieControlsController::TabObserver::~TabObserver() = default;

void CookieControlsController::TabObserver::OnSiteDataAccessed(
    const AccessDetails& access_details) {
  if (access_details.site_data_type != SiteDataType::kCookies) {
    cookie_controls_->UpdateUserBypass();
    return;
  }

  // When User Bypass is enabled, a large number of string comparisons are
  // performed to determine what sites are 3P / 1P. Cookie accesses are
  // reported _very_ frequently as many sites are always reading or writing to
  // the same cookie, and there is no caching of these accesses anywhere before
  // here (in constrast to JS storage, which does cache accesses earlier).
  // A simple cache of cookie accesses is used here to limit the number of
  // repeated updates.
  // We can't cache all types of accesses here, because the `site_data_type` is
  // not always populated with sufficient granularity (often aliasing to
  // kUnknown). This is relevant as some daya types may impact the block 3P
  // count, while others may not.
  // TODO(crbug.com/40205603): Replace the SiteDataType with the Browsing Data
  // Model's StorageType, which would let us remove an enum, and let us cache
  // all accesses here.

  if (cookie_accessed_set_.Get(access_details) != cookie_accessed_set_.end()) {
    return;
  }

  cookie_accessed_set_.Put(AccessDetails(access_details));
  cookie_controls_->UpdateUserBypass();
}

void CookieControlsController::TabObserver::OnStatefulBounceDetected() {
  cookie_controls_->UpdateUserBypass();
}

void CookieControlsController::TabObserver::PrimaryPageChanged(
    content::Page& page) {
  const GURL& current_url =
      content::WebContentsObserver::web_contents()->GetVisibleURL();
  cookie_accessed_set_.Clear();

  if (current_url != last_visited_url_) {
    reload_count_ = 0;
    timer_.Stop();
  } else {
    if (!timer_.IsRunning()) {
      timer_.Start(FROM_HERE, features::kUserBypassUIReloadTime.Get(), this,
                   &CookieControlsController::TabObserver::ResetReloadCounter);
    }
    reload_count_++;
  }
  last_visited_url_ = current_url;
  cookie_controls_->UpdatePageReloadStatus(reload_count_);
  cookie_controls_->UpdateLastVisitedSitesMap();
}

void CookieControlsController::TabObserver::DidStopLoading() {
  cookie_controls_->OnPageFinishedLoading();
}

void CookieControlsController::TabObserver::BeforeFormRepostWarningShow() {
  cookie_controls_->OnBubbleCloseTriggered();
}

void CookieControlsController::TabObserver::ResetReloadCounter() {
  reload_count_ = 0;
}

}  // namespace content_settings