#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()
&& last_expiration < base::Time::Now()
&& !last_visited.is_null()
&& last_visited < last_expiration;
}
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 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();
const bool host_or_site_scoped_exception =
!info.secondary_pattern.HasDomainWildcard() ||
info.secondary_pattern ==
ContentSettingsPattern::FromURLToSchemefulSitePattern(url);
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)) {
return CookieControlsEnforcement::kEnforcedByCookieSetting;
} else {
return CookieControlsEnforcement::kNoEnforcement;
}
}
bool CookieControlsController::HasOriginSandboxedTopLevelDocument() const {
content::RenderFrameHost* rfh = GetWebContents()->GetPrimaryMainFrame();
if (!rfh || !rfh->HasPolicyContainerHost()) {
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());
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() {
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) {
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() {
const GURL& url = GetWebContents()->GetLastCommittedURL();
has_exception_expired_since_last_visit_ =
HasExceptionExpiredSinceLastVisit(GetMetadata(settings_map_, url));
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;
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();
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);
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) {
if (controls_state == CookieControlsState::kHidden ||
controls_state == CookieControlsState::kAllowed3pc) {
return false;
}
auto* web_contents = GetWebContents();
if (web_contents->GetBrowserContext()->IsOffTheRecord()) {
return false;
}
const GURL& url = web_contents->GetLastCommittedURL();
if (cookie_settings_->HasAnyFrameRequestedStorageAccess(url)) {
return false;
}
if (has_exception_expired_since_last_visit_) {
return true;
}
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 ||
HasOriginSandboxedTopLevelDocument() ||
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),
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;
}
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;
}
}