| // 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. |
| |
| #ifndef CHROME_BROWSER_DIPS_DIPS_BOUNCE_DETECTOR_H_ |
| #define CHROME_BROWSER_DIPS_DIPS_BOUNCE_DETECTOR_H_ |
| |
| #include <memory> |
| #include <string> |
| #include <variant> |
| |
| #include "base/allocator/partition_allocator/pointers/raw_ptr.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/timer/timer.h" |
| #include "base/types/optional_ref.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_utils.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/render_frame_host.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/browser/web_contents_user_data.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "url/gurl.h" |
| |
| namespace base { |
| class Clock; |
| class TickClock; |
| } // namespace base |
| |
| // ClientBounceDetectionState is owned by the DIPSBounceDetector and stores |
| // data needed to detect stateful client-side redirects. |
| class ClientBounceDetectionState { |
| public: |
| ClientBounceDetectionState(GURL url, |
| std::string site, |
| base::TimeTicks load_time); |
| ClientBounceDetectionState(const ClientBounceDetectionState& other); |
| ~ClientBounceDetectionState(); |
| |
| // The NavigationHandle's previously committed URL at the time the navigation |
| // finishes and commits. |
| GURL previous_url; |
| std::string current_site; |
| base::TimeTicks page_load_time; |
| absl::optional<base::Time> last_activation_time; |
| absl::optional<base::Time> last_storage_time; |
| CookieAccessType cookie_access_type = CookieAccessType::kUnknown; |
| }; |
| |
| // Either the URL navigated away from (starting a new chain), or the client-side |
| // redirect connecting the navigation to the currently-committed chain. |
| using DIPSNavigationStart = absl::variant<GURL, DIPSRedirectInfoPtr>; |
| |
| // A redirect-chain-in-progress. It grows by calls to Append() and restarts by |
| // calls to EndChain(). |
| class DIPSRedirectContext { |
| public: |
| DIPSRedirectContext(DIPSRedirectChainHandler handler, |
| const GURL& initial_url); |
| ~DIPSRedirectContext(); |
| |
| // Immediately calls the `DIPSRedirectChainHandler` for the uncommitted |
| // navigation. It will take into account the length and initial URL of the |
| // current chain (without modifying it). |
| void HandleUncommitted(DIPSNavigationStart navigation_start, |
| std::vector<DIPSRedirectInfoPtr> server_redirects, |
| GURL final_url); |
| |
| // Either calls for termination of the in-progress redirect chain, with a |
| // start of a new one, or extends it, according to the value of |
| // `navigation_start`. |
| void AppendCommitted(DIPSNavigationStart navigation_start, |
| std::vector<DIPSRedirectInfoPtr> server_redirects); |
| |
| // Terminates the in-progress redirect chain, ending it with `final_url`, and |
| // parsing it to the `DIPSRedirectChainHandler` iff the chain is valid. It |
| // also starts a fresh redirect chain with `final_url` whilst clearing the |
| // state of the terminated chain. |
| // NOTE: A chain is valid if it has a non-empty `initial_url_`. |
| void EndChain(GURL final_url); |
| |
| [[nodiscard]] bool AddLateCookieAccess(GURL url, CookieOperation op); |
| |
| size_t size() const { return redirects_.size(); } |
| |
| GURL GetInitialURL() { return initial_url_; } |
| |
| void SetRedirectChainHandlerForTesting(DIPSRedirectChainHandler handler) { |
| handler_ = handler; |
| } |
| |
| private: |
| void AppendClientRedirect(DIPSRedirectInfoPtr client_redirect); |
| void AppendServerRedirects(std::vector<DIPSRedirectInfoPtr> server_redirects); |
| |
| DIPSRedirectChainHandler handler_; |
| // Represents the start of a chain and also indicates the presence of a valid |
| // chain. |
| GURL initial_url_; |
| std::vector<DIPSRedirectInfoPtr> redirects_; |
| // The index of the last redirect to have a known cookie access. When adding |
| // late cookie accesses, we only consider redirects from this offset onwards. |
| size_t update_offset_ = 0; |
| }; |
| |
| using DIPSIssueCallback = |
| base::RepeatingCallback<void(const std::set<std::string>& sites)>; |
| |
| // A simplified interface to WebContents and DIPSService that can be faked in |
| // tests. Needed to allow unit testing DIPSBounceDetector. |
| class DIPSBounceDetectorDelegate { |
| public: |
| virtual ~DIPSBounceDetectorDelegate(); |
| virtual const GURL& GetLastCommittedURL() const = 0; |
| virtual ukm::SourceId GetPageUkmSourceId() const = 0; |
| virtual void HandleRedirectChain(std::vector<DIPSRedirectInfoPtr> redirects, |
| DIPSRedirectChainInfoPtr chain) = 0; |
| virtual void ReportRedirectorsWithoutInteraction( |
| const std::set<std::string>& sites) = 0; |
| virtual void RecordEvent(DIPSRecordedEvent event, |
| const GURL& url, |
| const base::Time& time) = 0; |
| }; |
| |
| // ServerBounceDetectionState gets attached to NavigationHandle (which is a |
| // SupportsUserData subclass) to store data needed to detect stateful |
| // server-side redirects. |
| class ServerBounceDetectionState |
| : public content::NavigationHandleUserData<ServerBounceDetectionState> { |
| public: |
| ServerBounceDetectionState(); |
| ~ServerBounceDetectionState() override; |
| |
| DIPSNavigationStart navigation_start; |
| CookieAccessFilter filter; |
| |
| private: |
| explicit ServerBounceDetectionState( |
| content::NavigationHandle& navigation_handle); |
| |
| friend NavigationHandleUserData; |
| NAVIGATION_HANDLE_USER_DATA_KEY_DECL(); |
| }; |
| |
| // A simplified interface to content::NavigationHandle that can be faked in |
| // tests. Needed to allow unit testing DIPSBounceDetector. |
| class DIPSNavigationHandle { |
| public: |
| virtual ~DIPSNavigationHandle(); |
| |
| // See content::NavigationHandle for an explanation of these methods: |
| const GURL& GetURL() const { return GetRedirectChain().back(); } |
| virtual const GURL& GetPreviousPrimaryMainFrameURL() const = 0; |
| virtual bool HasCommitted() const = 0; |
| virtual const std::vector<GURL>& GetRedirectChain() const = 0; |
| // This method has one important (simplifying) change from |
| // content::NavigationHandle::HasUserGesture(): it returns true if the |
| // navigation was not renderer-initiated. |
| virtual bool HasUserGesture() const = 0; |
| |
| // Get a SourceId of type REDIRECT_ID for the index'th URL in the redirect |
| // chain. |
| ukm::SourceId GetRedirectSourceId(int index) const; |
| // Calls ServerBounceDetectionState::GetOrCreateForNavigationHandle(). We |
| // declare this instead of making DIPSNavigationHandle a subclass of |
| // SupportsUserData, because ServerBounceDetectionState inherits from |
| // NavigationHandleUserData, whose helper functions only work with actual |
| // content::NavigationHandle, not any SupportsUserData. |
| virtual ServerBounceDetectionState* GetServerState() = 0; |
| }; |
| |
| // Detects client/server-side bounces and handles them (currently by collecting |
| // metrics and storing them in the DIPSDatabase). |
| class DIPSBounceDetector { |
| public: |
| // The amount of time since a page last received user interaction before a |
| // subsequent user interaction event may be recorded to DIPS Storage for the |
| // same page. |
| static const base::TimeDelta kTimestampUpdateInterval; |
| |
| explicit DIPSBounceDetector(DIPSBounceDetectorDelegate* delegate, |
| const base::TickClock* tick_clock, |
| const base::Clock* clock); |
| ~DIPSBounceDetector(); |
| DIPSBounceDetector(const DIPSBounceDetector&) = delete; |
| DIPSBounceDetector& operator=(const DIPSBounceDetector&) = delete; |
| |
| void SetClockForTesting(base::Clock* clock) { clock_ = clock; } |
| // The following methods are based on WebContentsObserver, simplified. |
| void DidStartNavigation(DIPSNavigationHandle* navigation_handle); |
| void OnClientCookiesAccessed(const GURL& url, CookieOperation op); |
| void OnServerCookiesAccessed(DIPSNavigationHandle* navigation_handle, |
| const GURL& url, |
| CookieOperation op); |
| void DidFinishNavigation(DIPSNavigationHandle* navigation_handle); |
| // Only records a new user activation event once per |
| // |kTimestampUpdateInterval| for a given page. |
| void OnUserActivation(); |
| // Makes a call to process the current chain before its state is destroyed by |
| // the tab closure. |
| void BeforeDestruction(); |
| // Use the passed handler instead of |
| // DIPSBounceDetectorDelegate::HandleRedirect(). |
| void SetRedirectChainHandlerForTesting(DIPSRedirectChainHandler handler) { |
| redirect_context_.SetRedirectChainHandlerForTesting(handler); |
| } |
| // Makes a call to process the current chain on |
| // `client_bounce_detection_timer_`'s timeout. |
| void OnClientBounceDetectionTimeout(); |
| |
| private: |
| // Whether or not the `last_time` timestamp should be updated yet. This is |
| // used to enforce throttling of timestamp updates, reducing the number of |
| // writes to the DIPS db. |
| bool ShouldUpdateTimestamp(base::optional_ref<const base::Time> last_time, |
| base::Time now); |
| |
| // Returns the set of sites in the current (server) redirect chain. If the |
| // navigation started with a client redirect, that site is also included. |
| // Redirectors matching the initial or end site are omitted. |
| std::set<std::string> GetRedirectors( |
| const DIPSNavigationStart& navigation_start, |
| DIPSNavigationHandle* navigation_handle); |
| |
| raw_ptr<const base::TickClock> tick_clock_; |
| raw_ptr<const base::Clock> clock_; |
| raw_ptr<DIPSBounceDetectorDelegate> delegate_; |
| absl::optional<ClientBounceDetectionState> client_detection_state_; |
| DIPSRedirectContext redirect_context_; |
| base::RetainingOneShotTimer client_bounce_detection_timer_; |
| }; |
| |
| // A thin wrapper around DIPSBounceDetector to use it as a WebContentsObserver. |
| class DIPSWebContentsObserver |
| : public content::WebContentsObserver, |
| public content::WebContentsUserData<DIPSWebContentsObserver>, |
| public DIPSBounceDetectorDelegate { |
| public: |
| static void MaybeCreateForWebContents(content::WebContents* web_contents); |
| |
| ~DIPSWebContentsObserver() override; |
| |
| void SetRedirectChainHandlerForTesting(DIPSRedirectChainHandler handler) { |
| detector_.SetRedirectChainHandlerForTesting(handler); |
| } |
| |
| // Use the passed handler instead of DIPSWebContentsObserver::EmitDIPSIssue(). |
| void SetIssueReportingCallbackForTesting(DIPSIssueCallback callback) { |
| issue_callback_ = callback; |
| } |
| |
| void SetClockForTesting(base::Clock* clock) { |
| detector_.SetClockForTesting(clock); |
| DCHECK(dips_service_); |
| dips_service_->storage() |
| ->AsyncCall(&DIPSStorage::SetClockForTesting) |
| .WithArgs(clock); |
| } |
| |
| private: |
| DIPSWebContentsObserver(content::WebContents* web_contents, |
| DIPSService* dips_service); |
| // So WebContentsUserData::CreateForWebContents() can call the constructor. |
| friend class content::WebContentsUserData<DIPSWebContentsObserver>; |
| |
| void EmitDIPSIssue(const std::set<std::string>& sites); |
| |
| // DIPSBounceDetectorDelegate overrides: |
| const GURL& GetLastCommittedURL() const override; |
| ukm::SourceId GetPageUkmSourceId() const override; |
| void HandleRedirectChain(std::vector<DIPSRedirectInfoPtr> redirects, |
| DIPSRedirectChainInfoPtr chain) override; |
| void ReportRedirectorsWithoutInteraction( |
| const std::set<std::string>& sites) override; |
| void RecordEvent(DIPSRecordedEvent event, |
| const GURL& url, |
| const base::Time& time) override; |
| |
| // WebContentsObserver overrides: |
| void DidStartNavigation( |
| content::NavigationHandle* navigation_handle) override; |
| void OnCookiesAccessed(content::RenderFrameHost* render_frame_host, |
| const content::CookieAccessDetails& details) override; |
| void OnCookiesAccessed(content::NavigationHandle* navigation_handle, |
| const content::CookieAccessDetails& details) override; |
| void DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) override; |
| void FrameReceivedUserActivation( |
| content::RenderFrameHost* render_frame_host) override; |
| void WebContentsDestroyed() override; |
| |
| // raw_ptr<> is safe here because DIPSService is a KeyedService, associated |
| // with the BrowserContext/Profile which will outlive the WebContents that |
| // DIPSWebContentsObserver is observing. |
| raw_ptr<DIPSService> dips_service_; |
| DIPSBounceDetector detector_; |
| DIPSIssueCallback issue_callback_; |
| |
| base::WeakPtrFactory<DIPSWebContentsObserver> weak_factory_{this}; |
| |
| WEB_CONTENTS_USER_DATA_KEY_DECL(); |
| }; |
| |
| #endif // CHROME_BROWSER_DIPS_DIPS_BOUNCE_DETECTOR_H_ |