blob: 85ff6d467abb169887f802bb9a4120c3ccea54d0 [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_bounce_detector.h"
#include <cmath>
#include <cstddef>
#include <memory>
#include <vector>
#include "base/allocator/partition_allocator/pointers/raw_ptr.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/overloaded.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/default_clock.h"
#include "base/time/default_tick_clock.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/dips/cookie_access_filter.h"
#include "chrome/browser/dips/dips_features.h"
#include "chrome/browser/dips/dips_redirect_info.h"
#include "chrome/browser/dips/dips_service.h"
#include "chrome/browser/dips/dips_storage.h"
#include "chrome/browser/dips/dips_utils.h"
#include "components/content_settings/browser/page_specific_content_settings.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/cookie_access_details.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_handle_user_data.h"
#include "content/public/browser/page.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents_observer.h"
#include "net/cookies/canonical_cookie.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "third_party/blink/public/mojom/devtools/inspector_issue.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
using content::NavigationHandle;
ServerBounceDetectionState::ServerBounceDetectionState() = default;
ServerBounceDetectionState::~ServerBounceDetectionState() = default;
ServerBounceDetectionState::ServerBounceDetectionState(
NavigationHandle& navigation_handle) {}
NAVIGATION_HANDLE_USER_DATA_KEY_IMPL(ServerBounceDetectionState);
ClientBounceDetectionState::ClientBounceDetectionState(
const ClientBounceDetectionState& other) = default;
ClientBounceDetectionState::~ClientBounceDetectionState() = default;
ClientBounceDetectionState::ClientBounceDetectionState(
GURL url,
std::string site,
base::TimeTicks load_time) {
this->previous_url = std::move(url);
this->current_site = std::move(site);
this->page_load_time = load_time;
}
namespace {
inline void UmaHistogramTimeToBounce(base::TimeDelta sample) {
base::UmaHistogramTimes("Privacy.DIPS.TimeFromNavigationCommitToClientBounce",
sample);
}
} // namespace
/* static */
void DIPSWebContentsObserver::MaybeCreateForWebContents(
content::WebContents* web_contents) {
auto* dips_service = DIPSService::Get(web_contents->GetBrowserContext());
if (!dips_service) {
return;
}
DIPSWebContentsObserver::CreateForWebContents(web_contents, dips_service);
}
DIPSWebContentsObserver::DIPSWebContentsObserver(
content::WebContents* web_contents,
DIPSService* dips_service)
: content_settings::PageSpecificContentSettings::SiteDataObserver(
web_contents),
content::WebContentsObserver(web_contents),
content::WebContentsUserData<DIPSWebContentsObserver>(*web_contents),
dips_service_(dips_service),
detector_(this,
base::DefaultTickClock::GetInstance(),
base::DefaultClock::GetInstance()) {
issue_reporting_callback_ = base::BindRepeating(
&DIPSWebContentsObserver::EmitDIPSIssue, weak_factory_.GetWeakPtr());
}
DIPSWebContentsObserver::~DIPSWebContentsObserver() = default;
const base::TimeDelta DIPSBounceDetector::kTimestampUpdateInterval =
base::Minutes(1);
DIPSBounceDetector::DIPSBounceDetector(DIPSBounceDetectorDelegate* delegate,
const base::TickClock* tick_clock,
const base::Clock* clock)
: tick_clock_(tick_clock),
clock_(clock),
delegate_(delegate),
// It's safe to use Unretained because the callback is owned by the
// DIPSRedirectContext which is owned by `this`, and the delegate must
// outlive `this`.
committed_redirect_context_(
base::BindRepeating(&DIPSBounceDetectorDelegate::HandleRedirectChain,
base::Unretained(delegate)),
base::BindRepeating(
&DIPSBounceDetectorDelegate::ReportRedirectorsWithoutInteraction,
base::Unretained(delegate)),
/*initial_url=*/GURL::EmptyGURL(),
/*redirect_prefix_count=*/0u),
client_bounce_detection_timer_(
FROM_HERE,
dips::kClientBounceDetectionTimeout.Get(),
base::BindRepeating(
&DIPSBounceDetector::OnClientBounceDetectionTimeout,
base::Unretained(this)),
tick_clock) {}
DIPSBounceDetector::~DIPSBounceDetector() = default;
DIPSBounceDetectorDelegate::~DIPSBounceDetectorDelegate() = default;
DIPSNavigationHandle::~DIPSNavigationHandle() = default;
ukm::SourceId DIPSNavigationHandle::GetRedirectSourceId(int index) const {
return ukm::UkmRecorder::GetSourceIdForRedirectUrl(
base::PassKey<DIPSNavigationHandle>(), GetRedirectChain()[index]);
}
DIPSRedirectContext::DIPSRedirectContext(DIPSRedirectChainHandler handler,
DIPSIssueHandler issue_handler,
const GURL& initial_url,
size_t redirect_prefix_count)
: handler_(handler),
issue_handler_(issue_handler),
initial_url_(initial_url),
redirect_prefix_count_(redirect_prefix_count) {}
DIPSRedirectContext::~DIPSRedirectContext() = default;
void DIPSRedirectContext::AppendClientRedirect(
DIPSRedirectInfoPtr client_redirect) {
DCHECK_EQ(client_redirect->redirect_type, DIPSRedirectType::kClient);
if (client_redirect->access_type > SiteDataAccessType::kNone) {
update_offset_ = redirects_.size();
}
if (client_redirect->access_type > SiteDataAccessType::kRead) {
redirectors_.insert(GetSiteForDIPS(client_redirect->url));
}
client_redirect->chain_index = GetRedirectChainLength();
redirects_.push_back(std::move(client_redirect));
TrimRedirectsFromFront();
}
void DIPSRedirectContext::AppendServerRedirects(
std::vector<DIPSRedirectInfoPtr> server_redirects) {
for (auto& redirect : server_redirects) {
DCHECK_EQ(redirect->redirect_type, DIPSRedirectType::kServer);
if (redirect->access_type > SiteDataAccessType::kNone) {
update_offset_ = redirects_.size();
}
if (redirect->access_type > SiteDataAccessType::kRead) {
redirectors_.insert(GetSiteForDIPS(redirect->url));
}
redirect->chain_index = GetRedirectChainLength();
redirects_.push_back(std::move(redirect));
}
TrimRedirectsFromFront();
}
void DIPSRedirectContext::TrimRedirectsFromFront() {
size_t trim_count = base::ClampSub(redirects_.size(), kDIPSRedirectChainMax);
if (trim_count == 0) {
return;
}
TrimAndHandleRedirects(trim_count);
update_offset_ = base::ClampSub(update_offset_, trim_count);
}
void DIPSRedirectContext::ReportIssue(const GURL& final_url) {
// Since redirectors that are the same as the start or final page won't be
// acted on, we don't report on them.
//
// NOTE: This is not exactly right since the end of this navigation may not
// necessarily be the end of the chain, if a client redirect happens. However,
// this is better for developer experience than waiting until then, since
// notifications come faster.
redirectors_.erase(GetSiteForDIPS(initial_url_));
redirectors_.erase(GetSiteForDIPS(final_url));
issue_handler_.Run(std::move(redirectors_));
redirectors_.clear();
}
void DIPSRedirectContext::HandleUncommitted(
DIPSNavigationStart navigation_start,
std::vector<DIPSRedirectInfoPtr> server_redirects,
GURL final_url) {
absl::visit( //
base::Overloaded{
[&](DIPSRedirectInfoPtr client_redirect) {
// The uncommitted navigation began with a client redirect, so its
// chain is considered an extension of *this*
// `DIPSRedirectContext`'s in-progress chain within the temp
// `DIPSRedirectContext`, whilst leaving *this*
// `DIPSRedirectContext`'s in-progress chain unchanged.
DIPSRedirectContext temp_context(handler_, issue_handler_,
initial_url_,
GetRedirectChainLength());
temp_context.AppendClientRedirect(std::move(client_redirect));
temp_context.AppendServerRedirects(std::move(server_redirects));
temp_context.ReportIssue(final_url);
temp_context.EndChain(std::move(final_url));
},
[&](GURL previous_nav_last_committed_url) {
// The uncommitted navigation began *without* a client redirect, so
// a new redirect chain within a new `DIPSRedirectContext` and
// process it immediately (the in-progress chain in *this*
// `DIPSRedirectContext` is irrelevant).
DIPSRedirectContext temp_context(handler_, issue_handler_,
previous_nav_last_committed_url,
/*redirect_prefix_count=*/0);
temp_context.AppendServerRedirects(std::move(server_redirects));
temp_context.ReportIssue(final_url);
temp_context.EndChain(std::move(final_url));
},
},
std::move(navigation_start));
}
void DIPSRedirectContext::AppendCommitted(
DIPSNavigationStart navigation_start,
std::vector<DIPSRedirectInfoPtr> server_redirects,
const GURL& final_url) {
// If there was a client-side redirect before
// `DIPSBounceDetector::client_bounce_detection_timer_` timedout, grow the
// chain. Otherwise, end it.
absl::visit( //
base::Overloaded{
[this](DIPSRedirectInfoPtr client_redirect) {
// The committed navigation began with a client redirect, so extend
// the in-progress redirect chain.
AppendClientRedirect(std::move(client_redirect));
},
[this](GURL previous_nav_last_committed_url) {
// The committed navigation began *without* a client redirect, so
// end the in-progress redirect chain and start a new one.
EndChain(previous_nav_last_committed_url);
},
},
std::move(navigation_start));
// Server-side redirects always grow the chain.
AppendServerRedirects(std::move(server_redirects));
ReportIssue(final_url);
}
void DIPSRedirectContext::TrimAndHandleRedirects(size_t trim_count) {
DCHECK_GE(redirects_.size(), trim_count);
// Use an empty final_URL. This processes the redirect as different from the
// final URL, which allows recording in the DIPS database.
auto chain = std::make_unique<DIPSRedirectChainInfo>(
initial_url_,
/*final_url=*/GURL(), GetRedirectChainLength(),
/*is_partial_chain=*/true);
std::vector<DIPSRedirectInfoPtr> redirect_subchain;
for (size_t ind = 0; ind < trim_count; ind++) {
redirect_subchain.push_back(std::move(redirects_.at(ind)));
}
redirects_.erase(redirects_.begin(), redirects_.begin() + trim_count);
redirect_prefix_count_ += trim_count;
handler_.Run(std::move(redirect_subchain), std::move(chain));
}
void DIPSRedirectContext::EndChain(GURL final_url) {
if (!initial_url_.is_empty()) {
auto chain = std::make_unique<DIPSRedirectChainInfo>(
initial_url_, final_url, GetRedirectChainLength(),
/*is_partial_chain=*/false);
handler_.Run(std::move(redirects_), std::move(chain));
}
initial_url_ = std::move(final_url);
redirects_.clear();
update_offset_ = 0;
}
bool DIPSRedirectContext::AddLateCookieAccess(GURL url, CookieOperation op) {
while (update_offset_ < redirects_.size()) {
if (redirects_[update_offset_]->url == url) {
redirects_[update_offset_]->access_type =
redirects_[update_offset_]->access_type | ToSiteDataAccessType(op);
// This cookie access might indicate a stateful bounce and ideally we'd
// report an issue to notify the user, but the navigation already
// committed and any relevant notifications were already emitted, so it's
// too late.
return true;
}
update_offset_++;
}
return false;
}
void DIPSWebContentsObserver::EmitDIPSIssue(
const std::set<std::string>& sites) {
if (sites.empty()) {
return;
}
auto details = blink::mojom::InspectorIssueDetails::New();
auto bounce_tracking_issue_details =
blink::mojom::BounceTrackingIssueDetails::New();
bounce_tracking_issue_details->tracking_sites.reserve(sites.size());
for (const auto& site : sites) {
bounce_tracking_issue_details->tracking_sites.push_back(site);
}
details->bounce_tracking_issue_details =
std::move(bounce_tracking_issue_details);
WebContentsObserver::web_contents()
->GetPrimaryMainFrame()
->ReportInspectorIssue(blink::mojom::InspectorIssueInfo::New(
blink::mojom::InspectorIssueCode::kBounceTrackingIssue,
std::move(details)));
}
void DIPSWebContentsObserver::ReportRedirectorsWithoutInteraction(
const std::set<std::string>& sites) {
if (sites.size() == 0) {
return;
}
dips_service_->storage()
->AsyncCall(&DIPSStorage::FilterSitesWithoutInteraction)
.WithArgs(sites)
.Then(issue_reporting_callback_);
}
void DIPSWebContentsObserver::RecordEvent(DIPSRecordedEvent event,
const GURL& url,
const base::Time& time) {
switch (event) {
case DIPSRecordedEvent::kStorage: {
dips_service_->storage()
->AsyncCall(&DIPSStorage::RecordStorage)
.WithArgs(url, time, dips_service_->GetCookieMode());
return;
}
case DIPSRecordedEvent::kInteraction: {
dips_service_->storage()
->AsyncCall(&DIPSStorage::RecordInteraction)
.WithArgs(url, time, dips_service_->GetCookieMode());
return;
}
case DIPSRecordedEvent::kWebAuthnAssertion: {
// TODO(crbug.com/1446678): Record this events in a dedicated db column.
return;
}
}
}
const GURL& DIPSWebContentsObserver::GetLastCommittedURL() const {
return WebContentsObserver::web_contents()->GetLastCommittedURL();
}
ukm::SourceId DIPSWebContentsObserver::GetPageUkmSourceId() const {
return WebContentsObserver::web_contents()
->GetPrimaryMainFrame()
->GetPageUkmSourceId();
}
void DIPSWebContentsObserver::HandleRedirectChain(
std::vector<DIPSRedirectInfoPtr> redirects,
DIPSRedirectChainInfoPtr chain) {
// We need to pass in a WeakPtr to DIPSWebContentsObserver as it's not
// guaranteed to outlive the call.
dips_service_->HandleRedirectChain(
std::move(redirects), std::move(chain),
base::BindRepeating(
&DIPSWebContentsObserver::IncrementPageSpecificBounceCount,
weak_factory_.GetWeakPtr()));
}
void DIPSWebContentsObserver::IncrementPageSpecificBounceCount(
const GURL& final_url) {
// Do nothing if the current URL doesn't match the final URL of the chain.
// This means that the user has navigated away from the bounce destination, so
// we don't want to update settings for the wrong site.
if (WebContentsObserver::web_contents()->GetURL() != final_url) {
return;
}
auto* pscs = content_settings::PageSpecificContentSettings::GetForPage(
WebContentsObserver::web_contents()->GetPrimaryPage());
pscs->IncrementStatefulBounceCount();
}
// A thin wrapper around NavigationHandle to implement DIPSNavigationHandle.
class DIPSNavigationHandleImpl : public DIPSNavigationHandle {
public:
explicit DIPSNavigationHandleImpl(NavigationHandle* handle)
: handle_(handle) {}
bool HasUserGesture() const override {
return handle_->HasUserGesture() || !handle_->IsRendererInitiated();
}
ServerBounceDetectionState* GetServerState() override {
return ServerBounceDetectionState::GetOrCreateForNavigationHandle(*handle_);
}
bool HasCommitted() const override { return handle_->HasCommitted(); }
const GURL& GetPreviousPrimaryMainFrameURL() const override {
return handle_->GetPreviousPrimaryMainFrameURL();
}
const GURL GetInitiator() const override {
return (!handle_->GetInitiatorOrigin().has_value() ||
handle_->GetInitiatorOrigin().value().opaque())
? GURL("about:blank")
: handle_->GetInitiatorOrigin().value().GetURL();
}
const std::vector<GURL>& GetRedirectChain() const override {
return handle_->GetRedirectChain();
}
private:
raw_ptr<NavigationHandle> handle_;
};
void DIPSWebContentsObserver::DidStartNavigation(
NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInPrimaryMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
DIPSNavigationHandleImpl dips_handle(navigation_handle);
detector_.DidStartNavigation(&dips_handle);
}
void DIPSBounceDetector::DidStartNavigation(
DIPSNavigationHandle* navigation_handle) {
// These resources need to be collected as soon as possible, although some
// might not be used:
bool timedout = !client_bounce_detection_timer_.IsRunning();
client_bounce_detection_timer_.Stop();
auto now = tick_clock_->NowTicks();
auto* server_bounce_detection_state = navigation_handle->GetServerState();
// A user gesture indicates no client-redirect. And, we won't consider a
// client-redirect to be a bounce if we timedout on the
// `client_bounce_detection_timer_ `.
if (navigation_handle->HasUserGesture() || timedout ||
!client_detection_state_.has_value()) {
server_bounce_detection_state->navigation_start =
delegate_->GetLastCommittedURL().is_empty()
? navigation_handle->GetInitiator()
: delegate_->GetLastCommittedURL();
return;
}
base::TimeDelta client_bounce_delay =
now - client_detection_state_->page_load_time;
// The delay between the previous navigation commit and the current
// client-redirect is only tracked for stateful bounces.
if (client_detection_state_->site_data_access_type >
SiteDataAccessType::kNone) {
UmaHistogramTimeToBounce(client_bounce_delay);
}
// We cannot append this client-redirect to |committed_redirect_context_|
// immediately, because we don't know if the navigation will commit. We must
// wait until DidFinishNavigation() is triggered.
server_bounce_detection_state->navigation_start =
std::make_unique<DIPSRedirectInfo>(
/*url=*/delegate_->GetLastCommittedURL(),
/*redirect_type=*/DIPSRedirectType::kClient,
/*access_type=*/client_detection_state_->site_data_access_type,
/*source_id=*/
delegate_->GetPageUkmSourceId(),
/*time=*/clock_->Now(),
/*client_bounce_delay=*/client_bounce_delay,
/*has_sticky_activation=*/
client_detection_state_->last_activation_time.has_value(),
/*web_authn_assertion_request_succeeded=*/
client_detection_state_->last_successful_web_authn_assertion_time
.has_value());
}
void DIPSWebContentsObserver::OnSiteDataAccessed(
const content_settings::AccessDetails& access_details) {
// NOTE: The current implementation is only acting on all site data types
// collapsed under `content_settings::SiteDataType::kStorage` with the
// exception of WebLocks (not monitored by the
// `content_settings::PageSpecificContentSettings`) as it's not persistent.
if (access_details.site_data_type !=
content_settings::SiteDataType::kStorage) {
return;
}
DCHECK(access_details.render_frame_host);
if (!IsInPrimaryPage(access_details.render_frame_host) ||
access_details.blocked_by_policy) {
return;
}
detector_.OnClientSiteDataAccessed(
access_details.url, ToCookieOperation(access_details.access_type));
}
void DIPSWebContentsObserver::OnStatefulBounceDetected() {}
void DIPSBounceDetector::OnClientSiteDataAccessed(const GURL& url,
CookieOperation op) {
auto now = clock_->Now();
if (client_detection_state_ &&
GetSiteForDIPS(url) == client_detection_state_->current_site) {
client_detection_state_->site_data_access_type =
client_detection_state_->site_data_access_type |
ToSiteDataAccessType(op);
// To decrease the number of writes made to the database, after a site
// storage event (cookie write) on the page, new storage events will not
// be recorded for the next |kTimestampUpdateInterval|.
if (op == CookieOperation::kChange &&
ShouldUpdateTimestamp(client_detection_state_->last_storage_time,
now)) {
client_detection_state_->last_storage_time = now;
delegate_->RecordEvent(DIPSRecordedEvent::kStorage, url, now);
}
} else if (op == CookieOperation::kChange) {
delegate_->RecordEvent(DIPSRecordedEvent::kStorage, url, now);
}
}
bool HasCHIPS(const net::CookieList& cookie_list) {
for (const auto& cookie : cookie_list) {
if (cookie.IsPartitioned()) {
return true;
}
}
return false;
}
void DIPSWebContentsObserver::OnCookiesAccessed(
content::RenderFrameHost* render_frame_host,
const content::CookieAccessDetails& details) {
if (!IsInPrimaryPage(render_frame_host) || details.blocked_by_policy) {
return;
}
const absl::optional<GURL> fpu = GetFirstPartyURL(render_frame_host);
if (!fpu.has_value()) {
return;
}
if (!HasCHIPS(details.cookie_list) &&
!IsSameSiteForDIPS(fpu.value(), details.url)) {
return;
}
detector_.OnClientCookiesAccessed(fpu.value(), details.type);
}
void DIPSBounceDetector::OnClientCookiesAccessed(const GURL& url,
CookieOperation op) {
base::Time now = clock_->Now();
// We might be called for "late" server cookie accesses, not just client
// cookies. Before completing other checks, attempt to attribute the cookie
// access to the current redirect chain to handle that case.
//
// TODO(rtarpine): Is it possible for cookie accesses to be reported late for
// uncommitted navigations?
if (committed_redirect_context_.AddLateCookieAccess(url, op)) {
if (op == CookieOperation::kChange) {
delegate_->RecordEvent(DIPSRecordedEvent::kStorage, url, now);
}
return;
}
OnClientSiteDataAccessed(url, op);
}
void DIPSWebContentsObserver::OnCookiesAccessed(
NavigationHandle* navigation_handle,
const content::CookieAccessDetails& details) {
// Discard all notifications that are:
// - From other page types like FencedFrames, and Prerendered.
// - Blocked by policies.
if (!IsInPrimaryPage(navigation_handle) || details.blocked_by_policy) {
return;
}
// All access within the primary page iframes are attributed to the URL of the
// main frame (ie the first party URL).
if (IsInPrimaryPageIFrame(navigation_handle)) {
if (!HasCHIPS(details.cookie_list) &&
!IsSameSiteForDIPS(GetFirstPartyURL(navigation_handle), details.url)) {
return;
}
detector_.OnClientSiteDataAccessed(GetFirstPartyURL(navigation_handle),
details.type);
return;
}
DIPSNavigationHandleImpl dips_handle(navigation_handle);
detector_.OnServerCookiesAccessed(&dips_handle, details.url, details.type);
}
void DIPSBounceDetector::OnServerCookiesAccessed(
DIPSNavigationHandle* navigation_handle,
const GURL& url,
CookieOperation op) {
if (op == CookieOperation::kChange) {
delegate_->RecordEvent(DIPSRecordedEvent::kStorage, url, clock_->Now());
}
ServerBounceDetectionState* state = navigation_handle->GetServerState();
if (state) {
state->filter.AddAccess(url, op);
}
}
void DIPSWebContentsObserver::DidFinishNavigation(
NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInPrimaryMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
if (navigation_handle->HasCommitted()) {
if (last_committed_site_.has_value()) {
dips_service_->RemoveOpenSite(last_committed_site_.value());
}
last_committed_site_ = GetSiteForDIPS(navigation_handle->GetURL());
dips_service_->AddOpenSite(last_committed_site_.value());
}
DIPSNavigationHandleImpl dips_handle(navigation_handle);
detector_.DidFinishNavigation(&dips_handle);
}
void DIPSBounceDetector::DidFinishNavigation(
DIPSNavigationHandle* navigation_handle) {
base::TimeTicks now = tick_clock_->NowTicks();
// Starts the timer.
client_bounce_detection_timer_.Reset();
// Iff the primary page changed, reset the client detection state while
// storing the page load time and previous_url. A primary page change is
// verified by checking IsInPrimaryMainFrame, !IsSameDocument, and
// HasCommitted. HasCommitted is the only one not previously checked here.
if (navigation_handle->HasCommitted()) {
client_detection_state_ = ClientBounceDetectionState(
navigation_handle->GetPreviousPrimaryMainFrameURL(),
GetSiteForDIPS(navigation_handle->GetURL()), now);
}
ServerBounceDetectionState* server_state =
navigation_handle->GetServerState();
if (!server_state) {
return;
}
std::vector<DIPSRedirectInfoPtr> redirects;
std::vector<SiteDataAccessType> access_types;
server_state->filter.Filter(navigation_handle->GetRedirectChain(),
&access_types);
for (size_t i = 0; i < access_types.size() - 1; i++) {
redirects.push_back(std::make_unique<DIPSRedirectInfo>(
/*url=*/navigation_handle->GetRedirectChain()[i],
/*redirect_type=*/DIPSRedirectType::kServer,
/*access_type=*/access_types[i],
/*source_id=*/navigation_handle->GetRedirectSourceId(i),
/*time=*/clock_->Now()));
}
if (navigation_handle->HasCommitted()) {
committed_redirect_context_.AppendCommitted(
std::move(server_state->navigation_start), std::move(redirects),
navigation_handle->GetURL());
} else {
committed_redirect_context_.HandleUncommitted(
std::move(server_state->navigation_start), std::move(redirects),
navigation_handle->GetURL());
}
if (navigation_handle->HasCommitted()) {
// The last entry in navigation_handle->GetRedirectChain() is actually the
// page being committed (i.e., not a redirect). If its HTTP request or
// response accessed cookies, record this in our client detection state.
client_detection_state_->site_data_access_type = access_types.back();
}
}
// TODO(kaklilu): Follow up on how this interacts with Fenced Frames.
void DIPSWebContentsObserver::FrameReceivedUserActivation(
content::RenderFrameHost* render_frame_host) {
// Ignore iframe activations since we only care for its associated main-frame
// interactions on the top-level site.
if (!render_frame_host->IsInPrimaryMainFrame()) {
return;
}
detector_.OnUserActivation();
}
void DIPSBounceDetector::OnUserActivation() {
GURL url = delegate_->GetLastCommittedURL();
if (!url.SchemeIsHTTPOrHTTPS()) {
return;
}
base::Time now = clock_->Now();
if (client_detection_state_.has_value()) {
// To decrease the number of writes made to the database, after a user
// activation event on the page, new activation events will not be recorded
// for the next |kTimestampUpdateInterval|.
if (!ShouldUpdateTimestamp(client_detection_state_->last_activation_time,
now)) {
return;
}
client_detection_state_->last_activation_time = now;
}
delegate_->RecordEvent(DIPSRecordedEvent::kInteraction, url, now);
}
void DIPSBounceDetector::WebAuthnAssertionRequestSucceeded() {
GURL url = delegate_->GetLastCommittedURL();
if (!url.SchemeIsHTTPOrHTTPS()) {
return;
}
base::Time now = clock_->Now();
if (client_detection_state_.has_value()) {
// To decrease the number of writes made to the database, after a user web
// authn assertion happens, subsequent events will not be recorded for the
// next `kTimestampUpdateInterval`.
if (!ShouldUpdateTimestamp(
client_detection_state_->last_successful_web_authn_assertion_time,
now)) {
return;
}
client_detection_state_->last_successful_web_authn_assertion_time = now;
}
delegate_->RecordEvent(DIPSRecordedEvent::kWebAuthnAssertion, url, now);
}
void DIPSWebContentsObserver::WebAuthnAssertionRequestSucceeded(
content::RenderFrameHost* render_frame_host) {
if (!render_frame_host->IsInPrimaryMainFrame()) {
return;
}
detector_.WebAuthnAssertionRequestSucceeded();
}
bool DIPSBounceDetector::ShouldUpdateTimestamp(
base::optional_ref<const base::Time> last_time,
base::Time now) {
return (!last_time.has_value() ||
(now - last_time.value()) >= kTimestampUpdateInterval);
}
void DIPSWebContentsObserver::WebContentsDestroyed() {
if (last_committed_site_.has_value()) {
dips_service_->RemoveOpenSite(last_committed_site_.value());
}
detector_.BeforeDestruction();
}
void DIPSBounceDetector::BeforeDestruction() {
committed_redirect_context_.EndChain(delegate_->GetLastCommittedURL());
}
void DIPSBounceDetector::OnClientBounceDetectionTimeout() {
committed_redirect_context_.EndChain(delegate_->GetLastCommittedURL());
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(DIPSWebContentsObserver);
ukm::SourceId GetInitialRedirectSourceId(
content::NavigationHandle* navigation_handle) {
DIPSNavigationHandleImpl handle(navigation_handle);
return handle.GetRedirectSourceId(0);
}