blob: ca95fcee508d84bd73412bb9d9e8da1aa78b00f3 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/dips/dips_service.h"
#include <set>
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
#include "chrome/browser/content_settings/cookie_settings_factory.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/dips/dips_browser_signin_detector.h"
#include "chrome/browser/dips/dips_features.h"
#include "chrome/browser/dips/dips_redirect_info.h"
#include "chrome/browser/dips/dips_service_factory.h"
#include "chrome/browser/dips/dips_storage.h"
#include "chrome/browser/dips/dips_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/common/pref_names.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "components/signin/public/base/persistent_repeating_timer.h"
#include "components/site_engagement/content/site_engagement_service.h"
#include "components/site_engagement/core/mojom/site_engagement_details.mojom.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/browsing_data_filter_builder.h"
#include "content/public/browser/browsing_data_remover.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/network/public/mojom/clear_data_filter.mojom.h"
namespace {
// Controls whether UKM metrics are collected for DIPS.
BASE_FEATURE(kDipsUkm, "DipsUkm", base::FEATURE_ENABLED_BY_DEFAULT);
std::vector<std::string> GetEngagedSitesInBackground(
base::Time now,
scoped_refptr<HostContentSettingsMap> map) {
std::set<std::string> unique_sites;
auto details =
site_engagement::SiteEngagementService::GetAllDetailsInBackground(now,
map);
for (const site_engagement::mojom::SiteEngagementDetails& detail : details) {
if (!detail.origin.SchemeIsHTTPOrHTTPS()) {
continue;
}
if (!site_engagement::SiteEngagementService::IsEngagementAtLeast(
detail.total_score, blink::mojom::EngagementLevel::MINIMAL)) {
continue;
}
unique_sites.insert(GetSiteForDIPS(detail.origin));
}
return std::vector(unique_sites.begin(), unique_sites.end());
}
RedirectCategory ClassifyRedirect(CookieAccessType access,
bool has_interaction) {
switch (access) {
case CookieAccessType::kUnknown:
return has_interaction ? RedirectCategory::kUnknownCookies_HasEngagement
: RedirectCategory::kUnknownCookies_NoEngagement;
case CookieAccessType::kNone:
return has_interaction ? RedirectCategory::kNoCookies_HasEngagement
: RedirectCategory::kNoCookies_NoEngagement;
case CookieAccessType::kRead:
return has_interaction ? RedirectCategory::kReadCookies_HasEngagement
: RedirectCategory::kReadCookies_NoEngagement;
case CookieAccessType::kWrite:
return has_interaction ? RedirectCategory::kWriteCookies_HasEngagement
: RedirectCategory::kWriteCookies_NoEngagement;
case CookieAccessType::kReadWrite:
return has_interaction ? RedirectCategory::kReadWriteCookies_HasEngagement
: RedirectCategory::kReadWriteCookies_NoEngagement;
}
}
inline void UmaHistogramBounceCategory(RedirectCategory category,
DIPSCookieMode mode,
DIPSRedirectType type) {
const std::string histogram_name =
base::StrCat({"Privacy.DIPS.BounceCategory", GetHistogramPiece(type),
GetHistogramSuffix(mode)});
base::UmaHistogramEnumeration(histogram_name, category);
}
inline void UmaHistogramDeletionLatency(base::Time deletion_start) {
base::UmaHistogramLongTimes100("Privacy.DIPS.DeletionLatency",
base::Time::Now() - deletion_start);
}
void OnDeletionFinished(base::OnceClosure finished_callback,
base::Time deletion_start) {
UmaHistogramDeletionLatency(deletion_start);
std::move(finished_callback).Run();
}
class StateClearer : public content::BrowsingDataRemover::Observer {
public:
StateClearer(const StateClearer&) = delete;
StateClearer& operator=(const StateClearer&) = delete;
~StateClearer() override { remover_->RemoveObserver(this); }
// Clears state for the sites specified by 'filter'. Runs |callback| once
// clearing is complete.
//
// NOTE: This deletion task removing rows for `sites_to_clear` from the
// DIPSStorage backend relies on the assumption that rows flagged as DIPS
// eligible don't have user interaction time values. So even though 'remover'
// will only clear the storage timestamps, that's sufficient to delete the
// entire row.
static void DeleteState(
content::BrowsingDataRemover* remover,
std::unique_ptr<content::BrowsingDataFilterBuilder> filter,
base::OnceClosure callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// StateClearer manages its own lifetime and deletes itself when finished.
auto* state_clearer = new StateClearer(remover, std::move(callback));
remover->AddObserver(state_clearer);
remover->RemoveWithFilterAndReply(
base::Time::Min(), base::Time::Max(),
chrome_browsing_data_remover::FILTERABLE_DATA_TYPES |
content::BrowsingDataRemover::DATA_TYPE_AVOID_CLOSING_CONNECTIONS,
content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB |
content::BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB,
std::move(filter), state_clearer);
}
private:
StateClearer(content::BrowsingDataRemover* remover,
base::OnceClosure callback)
: remover_(remover), callback_(std::move(callback)) {}
void OnBrowsingDataRemoverDone(uint64_t failed_data_types) override {
std::move(callback_).Run();
delete this; // Matches the new in DeleteState()
}
raw_ptr<content::BrowsingDataRemover> remover_;
base::OnceClosure callback_;
};
} // namespace
DIPSService::DIPSService(content::BrowserContext* context)
: browser_context_(context),
cookie_settings_(CookieSettingsFactory::GetForProfile(
Profile::FromBrowserContext(context))),
repeating_timer_(CreateTimer(Profile::FromBrowserContext(context))) {
DCHECK(base::FeatureList::IsEnabled(dips::kFeature));
absl::optional<base::FilePath> path_to_use;
base::FilePath dips_path = GetDIPSFilePath(browser_context_);
if (browser_context_->IsOffTheRecord()) {
// OTR profiles should have no existing DIPS database file to be cleaned up.
// In fact, attempting to delete one at the path associated with the OTR
// profile would delete the DIPS database for the underlying regular
// profile.
wait_for_file_deletion_.Quit();
} else {
if (dips::kPersistedDatabaseEnabled.Get()) {
path_to_use = dips_path;
// Existing database files won't be deleted, so quit the
// `wait_for_file_deletion_` RunLoop.
wait_for_file_deletion_.Quit();
} else {
// If opening in-memory, delete any database files that may exist.
DIPSStorage::DeleteDatabaseFiles(dips_path,
wait_for_file_deletion_.QuitClosure());
}
}
storage_ = base::SequenceBound<DIPSStorage>(CreateTaskRunner(), path_to_use);
storage_.AsyncCall(&DIPSStorage::IsPrepopulated)
.Then(base::BindOnce(&DIPSService::InitializeStorageWithEngagedSites,
weak_factory_.GetWeakPtr()));
if (repeating_timer_) {
repeating_timer_->Start();
}
if (auto* identity_manager = IdentityManagerFactory::GetForProfile(
Profile::FromBrowserContext(context))) {
dips_browser_signin_detector_.emplace(this, identity_manager);
}
}
std::unique_ptr<signin::PersistentRepeatingTimer> DIPSService::CreateTimer(
Profile* profile) {
DCHECK(profile);
// base::Unretained(this) is safe here since the timer that is created has the
// same lifetime as this service.
return std::make_unique<signin::PersistentRepeatingTimer>(
profile->GetPrefs(), prefs::kDIPSTimerLastUpdate, dips::kTimerDelay.Get(),
base::BindRepeating(&DIPSService::OnTimerFired, base::Unretained(this)));
}
DIPSService::~DIPSService() = default;
/* static */
DIPSService* DIPSService::Get(content::BrowserContext* context) {
return DIPSServiceFactory::GetForBrowserContext(context);
}
void DIPSService::Shutdown() {
cached_should_block_3pcs_ = cookie_settings_->ShouldBlockThirdPartyCookies();
cookie_settings_.reset();
}
scoped_refptr<base::SequencedTaskRunner> DIPSService::CreateTaskRunner() {
return base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::ThreadPolicy::PREFER_BACKGROUND});
}
bool DIPSService::ShouldBlockThirdPartyCookies() const {
if (IsShuttingDown()) {
return cached_should_block_3pcs_.value();
}
return cookie_settings_->ShouldBlockThirdPartyCookies();
}
bool DIPSService::HasCookieException(const std::string& site) const {
DCHECK(!IsShuttingDown());
GURL url("https://" + site);
// Checks whether there is an exception allowing all third-parties embedded
// under |site| to use cookies.
if (cookie_settings_->IsFullCookieAccessAllowed(
GURL(), net::SiteForCookies::FromUrl(url), url::Origin::Create(url),
net::CookieSettingOverrides())) {
return true;
}
// Checks whether there is an exception allowing |site| to use cookies when
// embedded by any other site.
if (cookie_settings_->IsFullCookieAccessAllowed(
url, net::SiteForCookies(), absl::nullopt,
net::CookieSettingOverrides())) {
return true;
}
return false;
}
DIPSCookieMode DIPSService::GetCookieMode() const {
return GetDIPSCookieMode(browser_context_->IsOffTheRecord(),
ShouldBlockThirdPartyCookies());
}
void DIPSService::RemoveEvents(const base::Time& delete_begin,
const base::Time& delete_end,
network::mojom::ClearDataFilterPtr filter,
DIPSEventRemovalType type) {
// Storage init should be finished by now, so no need to delay until then.
storage_.AsyncCall(&DIPSStorage::RemoveEvents)
.WithArgs(delete_begin, delete_end, std::move(filter), type);
}
void DIPSService::InitializeStorageWithEngagedSites(bool prepopulated) {
if (prepopulated) {
wait_for_prepopulating_.Quit();
return;
}
base::Time now = base::Time::Now();
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::TaskPriority::USER_BLOCKING,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(
&GetEngagedSitesInBackground, now,
base::WrapRefCounted(
HostContentSettingsMapFactory::GetForProfile(browser_context_))),
base::BindOnce(&DIPSService::InitializeStorage,
weak_factory_.GetWeakPtr(), now));
}
void DIPSService::InitializeStorage(base::Time time,
std::vector<std::string> sites) {
storage_.AsyncCall(&DIPSStorage::Prepopulate)
.WithArgs(time, sites, wait_for_prepopulating_.QuitClosure());
}
void DIPSService::HandleRedirectChain(
std::vector<DIPSRedirectInfoPtr> redirects,
DIPSRedirectChainInfoPtr chain) {
if (redirects.empty()) {
for (auto& observer : observers_) {
observer.OnChainHandled(chain);
}
return;
}
chain->cookie_mode = GetCookieMode();
// Copy the URL out before |redirects| is moved, to avoid use-after-move.
GURL url = redirects[0]->url;
storage_.AsyncCall(&DIPSStorage::Read)
.WithArgs(url)
.Then(base::BindOnce(&DIPSService::GotState, weak_factory_.GetWeakPtr(),
std::move(redirects), std::move(chain), 0));
}
void DIPSService::GotState(std::vector<DIPSRedirectInfoPtr> redirects,
DIPSRedirectChainInfoPtr chain,
size_t index,
const DIPSState url_state) {
DCHECK_LT(index, redirects.size());
DIPSRedirectInfo* redirect = redirects[index].get();
// If there's any user interaction recorded in the DIPS DB, that's engagement.
redirect->has_interaction = url_state.user_interaction_times().has_value();
HandleRedirect(
*redirect, *chain,
base::BindRepeating(&DIPSService::RecordBounce, base::Unretained(this)));
if (index + 1 >= redirects.size()) {
// All redirects handled.
for (auto& observer : observers_) {
observer.OnChainHandled(chain);
}
return;
}
// Copy the URL out before `redirects` is moved, to avoid use-after-move.
GURL url = redirects[index + 1]->url;
storage_.AsyncCall(&DIPSStorage::Read)
.WithArgs(url)
.Then(base::BindOnce(&DIPSService::GotState, weak_factory_.GetWeakPtr(),
std::move(redirects), std::move(chain), index + 1));
}
void DIPSService::RecordBounce(const GURL& url,
base::Time time,
bool stateful) {
storage_.AsyncCall(&DIPSStorage::RecordBounce).WithArgs(url, time, stateful);
}
/*static*/
void DIPSService::HandleRedirect(const DIPSRedirectInfo& redirect,
const DIPSRedirectChainInfo& chain,
RecordBounceCallback record_bounce) {
const std::string site = GetSiteForDIPS(redirect.url);
bool initial_site_same = (site == chain.initial_site);
bool final_site_same = (site == chain.final_site);
DCHECK_LE(0, redirect.index);
DCHECK_LT(redirect.index, chain.length);
if (base::FeatureList::IsEnabled(kDipsUkm)) {
ukm::builders::DIPS_Redirect(redirect.source_id)
.SetSiteEngagementLevel(redirect.has_interaction.value() ? 1 : 0)
.SetRedirectType(static_cast<int64_t>(redirect.redirect_type))
.SetCookieAccessType(static_cast<int64_t>(redirect.access_type))
.SetRedirectAndInitialSiteSame(initial_site_same)
.SetRedirectAndFinalSiteSame(final_site_same)
.SetInitialAndFinalSitesSame(chain.initial_and_final_sites_same)
.SetRedirectChainIndex(redirect.index)
.SetRedirectChainLength(chain.length)
.SetClientBounceDelay(
BucketizeBounceDelay(redirect.client_bounce_delay))
.SetHasStickyActivation(redirect.has_sticky_activation)
.Record(ukm::UkmRecorder::Get());
}
if (initial_site_same || final_site_same) {
// Don't record UMA metrics for same-site redirects.
return;
}
// Record this bounce in the DIPS database.
if (redirect.access_type != CookieAccessType::kUnknown) {
record_bounce.Run(
redirect.url, redirect.time,
/*stateful=*/redirect.access_type > CookieAccessType::kRead);
}
RedirectCategory category =
ClassifyRedirect(redirect.access_type, redirect.has_interaction.value());
UmaHistogramBounceCategory(category, chain.cookie_mode.value(),
redirect.redirect_type);
}
void DIPSService::OnTimerFired() {
base::Time start = base::Time::Now();
// Storage init should be finished by now, so no need to delay until then.
storage_.AsyncCall(&DIPSStorage::GetSitesToClear)
.WithArgs(absl::nullopt)
.Then(base::BindOnce(&DIPSService::DeleteDIPSEligibleState,
weak_factory_.GetWeakPtr(), base::DoNothing(),
start));
}
void DIPSService::DeleteEligibleSitesImmediately(
DeletedSitesCallback callback) {
base::Time start = base::Time::Now();
// Storage init should be finished by now, so no need to delay until then.
storage_.AsyncCall(&DIPSStorage::GetSitesToClear)
.WithArgs(base::Seconds(0))
.Then(base::BindOnce(&DIPSService::DeleteDIPSEligibleState,
weak_factory_.GetWeakPtr(), std::move(callback),
start));
}
void DIPSService::DeleteDIPSEligibleState(
DeletedSitesCallback callback,
base::Time deletion_start,
std::vector<std::string> sites_to_clear) {
base::UmaHistogramCounts1000(
base::StrCat({"Privacy.DIPS.ClearedSitesCount",
GetHistogramSuffix(GetCookieMode())}),
sites_to_clear.size());
if (sites_to_clear.empty()) {
std::move(callback).Run(std::vector<std::string>());
return;
}
for (const auto& site : sites_to_clear) {
const ukm::SourceId source_id = ukm::UkmRecorder::GetSourceIdForDipsSite(
base::PassKey<DIPSService>(), site);
ukm::builders::DIPS_Deletion(source_id).SetDetected(true).Record(
ukm::UkmRecorder::Get());
}
base::OnceClosure finish_callback;
if (ShouldBlockThirdPartyCookies() && dips::kDeletionEnabled.Get()) {
if (IsShuttingDown()) {
return;
}
std::vector<std::string> excepted_sites;
std::vector<std::string> non_excepted_sites;
for (const auto& site : sites_to_clear) {
if (HasCookieException(site)) {
excepted_sites.push_back(site);
} else {
non_excepted_sites.push_back(site);
}
}
finish_callback = base::BindOnce(
std::move(callback), std::vector<std::string>(non_excepted_sites));
if (excepted_sites.empty()) {
PostDeletionTaskToUIThread(std::move(finish_callback), deletion_start,
std::move(non_excepted_sites));
} else {
// Storage init should be finished by now, so no need to delay until then.
storage_.AsyncCall(&DIPSStorage::RemoveRows)
.WithArgs(std::move(excepted_sites))
.Then(base::BindOnce(&DIPSService::PostDeletionTaskToUIThread,
weak_factory_.GetWeakPtr(),
std::move(finish_callback), deletion_start,
std::move(non_excepted_sites)));
}
} else {
finish_callback = base::BindOnce(std::move(callback),
std::vector<std::string>(sites_to_clear));
// Storage init should be finished by now, so no need to delay until then.
storage_.AsyncCall(&DIPSStorage::RemoveRows)
.WithArgs(std::move(sites_to_clear))
.Then(base::BindOnce(&OnDeletionFinished, std::move(finish_callback),
deletion_start));
}
}
void DIPSService::PostDeletionTaskToUIThread(base::OnceClosure callback,
base::Time deletion_start,
std::vector<std::string> sites) {
std::unique_ptr<content::BrowsingDataFilterBuilder> filter =
content::BrowsingDataFilterBuilder::Create(
content::BrowsingDataFilterBuilder::Mode::kDelete);
for (const auto& site : sites) {
filter->AddRegisterableDomain(site);
}
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&DIPSService::RunDeletionTaskOnUIThread,
weak_factory_.GetWeakPtr(), std::move(filter),
base::BindOnce(&OnDeletionFinished, std::move(callback),
deletion_start)));
}
void DIPSService::RunDeletionTaskOnUIThread(
std::unique_ptr<content::BrowsingDataFilterBuilder> filter,
base::OnceClosure callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
StateClearer::DeleteState(browser_context_->GetBrowsingDataRemover(),
std::move(filter), std::move(callback));
}
void DIPSService::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void DIPSService::RemoveObserver(const Observer* observer) {
observers_.RemoveObserver(observer);
}