diff options
Diffstat (limited to 'chromium/content/browser/conversions')
45 files changed, 5201 insertions, 443 deletions
diff --git a/chromium/content/browser/conversions/BUILD.gn b/chromium/content/browser/conversions/BUILD.gn new file mode 100644 index 00000000000..6d722613e99 --- /dev/null +++ b/chromium/content/browser/conversions/BUILD.gn @@ -0,0 +1,10 @@ +# Copyright 2020 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//mojo/public/tools/bindings/mojom.gni") + +mojom("mojo_bindings") { + sources = [ "conversion_internals.mojom" ] + public_deps = [ "//url/mojom:url_mojom_origin" ] +} diff --git a/chromium/content/browser/conversions/OWNERS b/chromium/content/browser/conversions/OWNERS index 7f1bf1a18d0..b716879726c 100644 --- a/chromium/content/browser/conversions/OWNERS +++ b/chromium/content/browser/conversions/OWNERS @@ -1,5 +1,8 @@ csharrison@chromium.org johnidel@chromium.org +per-file *.mojom=set noparent +per-file *.mojom=file://ipc/SECURITY_OWNERS + # TEAM: privacy-sandbox-dev@chromium.org # COMPONENT: Internals>ConversionMeasurement diff --git a/chromium/content/browser/conversions/conversion_host.cc b/chromium/content/browser/conversions/conversion_host.cc index ec917fabbb2..0a695271ca4 100644 --- a/chromium/content/browser/conversions/conversion_host.cc +++ b/chromium/content/browser/conversions/conversion_host.cc @@ -6,32 +6,180 @@ #include "base/bind.h" #include "base/bind_helpers.h" +#include "base/check.h" +#include "base/memory/ptr_util.h" #include "content/browser/conversions/conversion_manager.h" +#include "content/browser/conversions/conversion_manager_impl.h" +#include "content/browser/conversions/conversion_page_metrics.h" #include "content/browser/conversions/conversion_policy.h" -#include "content/browser/conversions/storable_conversion.h" +#include "content/browser/frame_host/frame_tree.h" +#include "content/browser/frame_host/frame_tree_node.h" +#include "content/browser/frame_host/render_frame_host_impl.h" #include "content/browser/storage_partition_impl.h" #include "content/public/browser/browser_context.h" +#include "content/public/browser/navigation_handle.h" #include "content/public/browser/render_frame_host.h" -#include "content/public/browser/web_contents.h" #include "mojo/public/cpp/bindings/message.h" #include "services/network/public/cpp/is_potentially_trustworthy.h" +#include "third_party/blink/public/mojom/devtools/console_message.mojom.h" #include "url/origin.h" namespace content { -ConversionHost::ConversionHost(WebContents* contents) - : web_contents_(contents), receiver_(contents, this) {} +namespace { -ConversionHost::~ConversionHost() = default; +// Abstraction that wraps an iterator to a map. When this goes out of the scope, +// the underlying iterator is erased from the map. This is useful for control +// flows where map cleanup needs to occur regardless of additional early exit +// logic. +template <typename Map> +class ScopedMapDeleter { + public: + ScopedMapDeleter(Map* map, const typename Map::key_type& key) + : map_(map), it_(map_->find(key)) {} + ~ScopedMapDeleter() { + if (*this) + map_->erase(it_); + } + + typename Map::iterator* get() { return &it_; } + + explicit operator bool() const { return it_ != map_->end(); } + + private: + Map* map_; + typename Map::iterator it_; +}; + +} // namespace + +// static +std::unique_ptr<ConversionHost> ConversionHost::CreateForTesting( + WebContents* web_contents, + std::unique_ptr<ConversionManager::Provider> conversion_manager_provider) { + return base::WrapUnique( + new ConversionHost(web_contents, std::move(conversion_manager_provider))); +} + +ConversionHost::ConversionHost(WebContents* web_contents) + : ConversionHost(web_contents, + std::make_unique<ConversionManagerProviderImpl>()) {} + +ConversionHost::ConversionHost( + WebContents* web_contents, + std::unique_ptr<ConversionManager::Provider> conversion_manager_provider) + : WebContentsObserver(web_contents), + conversion_manager_provider_(std::move(conversion_manager_provider)), + receiver_(web_contents, this) { + // TODO(csharrison): When https://crbug.com/1051334 is resolved, add a DCHECK + // that the kConversionMeasurement feature is enabled. +} + +ConversionHost::~ConversionHost() { + DCHECK_EQ(0u, navigation_impression_origins_.size()); +} + +void ConversionHost::DidStartNavigation(NavigationHandle* navigation_handle) { + // Navigations with an impression set should only occur in the main frame. + if (!navigation_handle->GetImpression() || + !navigation_handle->IsInMainFrame() || + !conversion_manager_provider_->GetManager(web_contents())) { + return; + } + + RenderFrameHostImpl* initiator_frame_host = + RenderFrameHostImpl::FromID(navigation_handle->GetInitiatorRoutingId()); + + // The initiator frame host may be deleted by this point. In that case, ignore + // this navigation and drop the impression associated with it. + // TODO(https://crbug.com/1056907): Record metrics on how often impressions + // are dropped because the initiator is destroyed. + if (!initiator_frame_host) + return; + + // Look up the initiator root's origin which will be used as the impression + // origin. This works because we won't update the origin for the initiator RFH + // until we receive confirmation from the renderer that it has committed. + // Since frame mutation is all serialized on the Blink main thread, we get an + // implicit ordering: a navigation with an impression attached won't be + // processed after a navigation commit in the initiator RFH, so reading the + // origin off is safe at the start of the navigation. + const url::Origin& initiator_root_frame_origin = + initiator_frame_host->frame_tree_node() + ->frame_tree() + ->root() + ->current_origin(); + navigation_impression_origins_.emplace(navigation_handle->GetNavigationId(), + initiator_root_frame_origin); +} + +void ConversionHost::DidFinishNavigation(NavigationHandle* navigation_handle) { + ConversionManager* conversion_manager = + conversion_manager_provider_->GetManager(web_contents()); + if (!conversion_manager) { + DCHECK(navigation_impression_origins_.empty()); + return; + } + + ScopedMapDeleter<NavigationImpressionOriginMap> it( + &navigation_impression_origins_, navigation_handle->GetNavigationId()); + + // If an impression is not associated with a main frame navigation, ignore it. + // If the navigation did not commit, committed to a Chrome error page, or was + // same document, ignore it. Impressions should never be attached to + // same-document navigations but can be the result of a bad renderer. + if (!navigation_handle->IsInMainFrame() || + !navigation_handle->HasCommitted() || navigation_handle->IsErrorPage() || + navigation_handle->IsSameDocument()) { + return; + } + + conversion_page_metrics_ = std::make_unique<ConversionPageMetrics>(); + + // If we were not able to access the impression origin, ignore the navigation. + if (!it) + return; + url::Origin impression_origin = std::move((*it.get())->second); + DCHECK(navigation_handle->GetImpression()); + const Impression& impression = *(navigation_handle->GetImpression()); + + // If the impression's conversion destination does not match the final top + // frame origin of this new navigation ignore it. + if (impression.conversion_destination != + navigation_handle->GetRenderFrameHost()->GetLastCommittedOrigin()) { + return; + } + + // Convert |impression| into a StorableImpression that can be forwarded to + // storage. If a reporting origin was not provided, default to the conversion + // destination for reporting. + const url::Origin& reporting_origin = !impression.reporting_origin + ? impression_origin + : *impression.reporting_origin; + + // Conversion measurement is only allowed in secure contexts. + if (!network::IsOriginPotentiallyTrustworthy(impression_origin) || + !network::IsOriginPotentiallyTrustworthy(reporting_origin) || + !network::IsOriginPotentiallyTrustworthy( + impression.conversion_destination)) { + // TODO (1049654): This should log a console error when it occurs. + return; + } + + base::Time impression_time = base::Time::Now(); + const ConversionPolicy& policy = conversion_manager->GetConversionPolicy(); + StorableImpression storable_impression( + policy.GetSanitizedImpressionData(impression.impression_data), + impression_origin, impression.conversion_destination, reporting_origin, + impression_time, + policy.GetExpiryTimeForImpression(impression.expiry, impression_time), + /*impression_id=*/base::nullopt); + + conversion_manager->HandleImpression(storable_impression); +} -// TODO(https://crbug.com/1044099): Limit the number of conversion redirects per -// page-load to a reasonable number. void ConversionHost::RegisterConversion( blink::mojom::ConversionPtr conversion) { - // If there is no conversion manager available, ignore any conversion - // registrations. - if (!GetManager()) - return; content::RenderFrameHost* render_frame_host = receiver_.GetCurrentTargetFrame(); @@ -42,6 +190,13 @@ void ConversionHost::RegisterConversion( return; } + // If there is no conversion manager available, ignore any conversion + // registrations. + ConversionManager* conversion_manager = + conversion_manager_provider_->GetManager(web_contents()); + if (!conversion_manager) + return; + // Only allow conversion registration on secure pages with a secure conversion // redirects. if (!network::IsOriginPotentiallyTrustworthy( @@ -54,12 +209,14 @@ void ConversionHost::RegisterConversion( } StorableConversion storable_conversion( - GetManager()->GetConversionPolicy().GetSanitizedConversionData( + conversion_manager->GetConversionPolicy().GetSanitizedConversionData( conversion->conversion_data), render_frame_host->GetLastCommittedOrigin(), conversion->reporting_origin); - GetManager()->HandleConversion(storable_conversion); + if (conversion_page_metrics_) + conversion_page_metrics_->OnConversion(storable_conversion); + conversion_manager->HandleConversion(storable_conversion); } void ConversionHost::SetCurrentTargetFrameForTesting( @@ -67,11 +224,4 @@ void ConversionHost::SetCurrentTargetFrameForTesting( receiver_.SetCurrentTargetFrameForTesting(render_frame_host); } -ConversionManager* ConversionHost::GetManager() { - return static_cast<StoragePartitionImpl*>( - BrowserContext::GetDefaultStoragePartition( - web_contents_->GetBrowserContext())) - ->GetConversionManager(); -} - } // namespace content diff --git a/chromium/content/browser/conversions/conversion_host.h b/chromium/content/browser/conversions/conversion_host.h index 468fa6b5b75..169c08a8042 100644 --- a/chromium/content/browser/conversions/conversion_host.h +++ b/chromium/content/browser/conversions/conversion_host.h @@ -5,21 +5,32 @@ #ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_HOST_H_ #define CONTENT_BROWSER_CONVERSIONS_CONVERSION_HOST_H_ +#include <memory> + +#include "base/containers/flat_map.h" #include "base/gtest_prod_util.h" +#include "content/browser/conversions/conversion_manager.h" +#include "content/common/content_export.h" +#include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_receiver_set.h" #include "third_party/blink/public/mojom/conversions/conversions.mojom.h" namespace content { -class ConversionManager; +class ConversionPageMetrics; class RenderFrameHost; class WebContents; // Class responsible for listening to conversion events originating from blink, // and verifying that they are valid. Owned by the WebContents. Lifetime is // bound to lifetime of the WebContents. -class CONTENT_EXPORT ConversionHost : public blink::mojom::ConversionHost { +class CONTENT_EXPORT ConversionHost : public WebContentsObserver, + public blink::mojom::ConversionHost { public: + static std::unique_ptr<ConversionHost> CreateForTesting( + WebContents* web_contents, + std::unique_ptr<ConversionManager::Provider> conversion_manager_provider); + explicit ConversionHost(WebContents* web_contents); ConversionHost(const ConversionHost& other) = delete; ConversionHost& operator=(const ConversionHost& other) = delete; @@ -32,17 +43,47 @@ class CONTENT_EXPORT ConversionHost : public blink::mojom::ConversionHost { FRIEND_TEST_ALL_PREFIXES(ConversionHostTest, ConversionWithInsecureReportingOrigin_BadMessage); FRIEND_TEST_ALL_PREFIXES(ConversionHostTest, ValidConversion_NoBadMessage); + FRIEND_TEST_ALL_PREFIXES(ConversionHostTest, PerPageConversionMetrics); + FRIEND_TEST_ALL_PREFIXES(ConversionHostTest, + NoManager_NoPerPageConversionMetrics); + + ConversionHost( + WebContents* web_contents, + std::unique_ptr<ConversionManager::Provider> conversion_manager_provider); // blink::mojom::ConversionHost: void RegisterConversion(blink::mojom::ConversionPtr conversion) override; + // WebContentsObserver: + void DidStartNavigation(NavigationHandle* navigation_handle) override; + void DidFinishNavigation(NavigationHandle* navigation_handle) override; + // Sets the target frame on |receiver_|. void SetCurrentTargetFrameForTesting(RenderFrameHost* render_frame_host); - // Gets the manager for this web contents. Can be null. - ConversionManager* GetManager(); + // Map which stores the top-frame origin an impression occurred on for all + // navigations with an associated impression, keyed by navigation ID. + // Initiator origins are stored at navigation start time to have the best + // chance of catching the initiating frame before it has a chance to go away. + // Storing the origins at navigation start also prevents cases where a frame + // initiates a navigation for itself, causing the frame to be correct but not + // representing the frame state at the time the navigation was initiated. They + // are stored until DidFinishNavigation, when they can be matched up with an + // impression. + // + // A flat_map is used as the number of ongoing impression navigations is + // expected to be very small in a given WebContents. + using NavigationImpressionOriginMap = base::flat_map<int64_t, url::Origin>; + NavigationImpressionOriginMap navigation_impression_origins_; + + // Gives access to a ConversionManager implementation to forward impressions + // and conversion registrations to. + std::unique_ptr<ConversionManager::Provider> conversion_manager_provider_; - WebContents* web_contents_; + // Logs metrics per top-level page load. Created for every top level + // navigation that commits, as long as there is a ConversionManager. + // Excludes the initial about:blank document. + std::unique_ptr<ConversionPageMetrics> conversion_page_metrics_; WebContentsFrameReceiverSet<blink::mojom::ConversionHost> receiver_; }; diff --git a/chromium/content/browser/conversions/conversion_host_unittest.cc b/chromium/content/browser/conversions/conversion_host_unittest.cc index 255587e9365..2769ef893d8 100644 --- a/chromium/content/browser/conversions/conversion_host_unittest.cc +++ b/chromium/content/browser/conversions/conversion_host_unittest.cc @@ -6,31 +6,51 @@ #include <memory> +#include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" +#include "content/browser/conversions/conversion_manager.h" +#include "content/browser/conversions/conversion_test_utils.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/public/common/content_features.h" #include "content/public/test/test_renderer_host.h" #include "content/test/fake_mojo_message_dispatch_context.h" +#include "content/test/navigation_simulator_impl.h" #include "content/test/test_render_frame_host.h" #include "content/test/test_web_contents.h" #include "mojo/public/cpp/test_support/test_utils.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/mojom/conversions/conversions.mojom.h" #include "url/gurl.h" #include "url/origin.h" namespace content { +namespace { + +const char kConversionUrl[] = "https://b.com"; + +Impression CreateValidImpression() { + Impression result; + result.conversion_destination = url::Origin::Create(GURL(kConversionUrl)); + result.reporting_origin = url::Origin::Create(GURL("https://c.com")); + result.impression_data = 1UL; + return result; +} + +} // namespace + class ConversionHostTest : public RenderViewHostTestHarness { public: - ConversionHostTest() { - feature_list_.InitAndEnableFeature(features::kConversionMeasurement); - } + ConversionHostTest() = default; void SetUp() override { RenderViewHostTestHarness::SetUp(); static_cast<WebContentsImpl*>(web_contents()) ->RemoveReceiverSetForTesting(blink::mojom::ConversionHost::Name_); - conversion_host_ = std::make_unique<ConversionHost>(web_contents()); + + conversion_host_ = ConversionHost::CreateForTesting( + web_contents(), std::make_unique<TestManagerProvider>(&test_manager_)); contents()->GetMainFrame()->InitializeRenderFrameIfNeeded(); } @@ -40,8 +60,10 @@ class ConversionHostTest : public RenderViewHostTestHarness { ConversionHost* conversion_host() { return conversion_host_.get(); } + protected: + TestConversionManager test_manager_; + private: - base::test::ScopedFeatureList feature_list_; std::unique_ptr<ConversionHost> conversion_host_; }; @@ -64,6 +86,7 @@ TEST_F(ConversionHostTest, ConversionInSubframe_BadMessage) { conversion_host()->RegisterConversion(std::move(conversion)); EXPECT_EQ("blink.mojom.ConversionHost can only be used by the main frame.", bad_message_observer.WaitForBadMessage()); + EXPECT_EQ(0u, test_manager_.num_conversions()); } TEST_F(ConversionHostTest, ConversionOnInsecurePage_BadMessage) { @@ -83,6 +106,7 @@ TEST_F(ConversionHostTest, ConversionOnInsecurePage_BadMessage) { "blink.mojom.ConversionHost can only be used in secure contexts with a " "secure conversion registration origin.", bad_message_observer.WaitForBadMessage()); + EXPECT_EQ(0u, test_manager_.num_conversions()); } TEST_F(ConversionHostTest, ConversionWithInsecureReportingOrigin_BadMessage) { @@ -101,14 +125,15 @@ TEST_F(ConversionHostTest, ConversionWithInsecureReportingOrigin_BadMessage) { "blink.mojom.ConversionHost can only be used in secure contexts with a " "secure conversion registration origin.", bad_message_observer.WaitForBadMessage()); + EXPECT_EQ(0u, test_manager_.num_conversions()); } TEST_F(ConversionHostTest, ValidConversion_NoBadMessage) { - // Create a page with an insecure origin. + // Create a page with a secure origin. contents()->NavigateAndCommit(GURL("https://www.example.com")); conversion_host()->SetCurrentTargetFrameForTesting(main_rfh()); - // Create a fake dispatch context to trigger a bad message in. + // Create a fake dispatch context to listen for bad messages. FakeMojoMessageDispatchContext fake_dispatch_context; mojo::test::BadMessageObserver bad_message_observer; @@ -121,6 +146,213 @@ TEST_F(ConversionHostTest, ValidConversion_NoBadMessage) { // triggered. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(bad_message_observer.got_bad_message()); + EXPECT_EQ(1u, test_manager_.num_conversions()); +} + +TEST_F(ConversionHostTest, PerPageConversionMetrics) { + base::HistogramTester histograms; + + contents()->NavigateAndCommit(GURL("https://www.example.com")); + + // Initial document should not log metrics. + histograms.ExpectTotalCount("Conversions.RegisteredConversionsPerPage", 0); + + conversion_host()->SetCurrentTargetFrameForTesting(main_rfh()); + blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New(); + conversion->reporting_origin = + url::Origin::Create(GURL("https://secure.com")); + + for (size_t i = 0u; i < 8u; i++) { + conversion_host()->RegisterConversion(conversion->Clone()); + EXPECT_EQ(1u, test_manager_.num_conversions()); + test_manager_.Reset(); + } + + // Same document navs should not reset the counter. + contents()->NavigateAndCommit(GURL("https://www.example.com#hash")); + histograms.ExpectTotalCount("Conversions.RegisteredConversionsPerPage", 0); + + // Re-navigating should reset the counter. + contents()->NavigateAndCommit(GURL("https://www.example-next.com")); + histograms.ExpectUniqueSample("Conversions.RegisteredConversionsPerPage", 8, + 1); +} + +TEST_F(ConversionHostTest, NoManager_NoPerPageConversionMetrics) { + // Replace the ConversionHost on the WebContents with one that is backed by a + // null ConversionManager. + static_cast<WebContentsImpl*>(web_contents()) + ->RemoveReceiverSetForTesting(blink::mojom::ConversionHost::Name_); + auto conversion_host = ConversionHost::CreateForTesting( + web_contents(), std::make_unique<TestManagerProvider>(nullptr)); + contents()->NavigateAndCommit(GURL("https://www.example.com")); + + base::HistogramTester histograms; + conversion_host->SetCurrentTargetFrameForTesting(main_rfh()); + blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New(); + conversion->reporting_origin = + url::Origin::Create(GURL("https://secure.com")); + conversion_host->RegisterConversion(std::move(conversion)); + + // Navigate again to trigger histogram code. + contents()->NavigateAndCommit(GURL("https://www.example-next.com")); + histograms.ExpectBucketCount("Conversions.RegisteredConversionsPerPage", 1, + 0); +} + +TEST_F(ConversionHostTest, NavigationWithNoImpression_Ignored) { + contents()->NavigateAndCommit(GURL("https://secure_impression.com")); + NavigationSimulatorImpl::NavigateAndCommitFromDocument(GURL(kConversionUrl), + main_rfh()); + + EXPECT_EQ(0u, test_manager_.num_impressions()); +} + +TEST_F(ConversionHostTest, ValidImpression_ForwardedToManager) { + contents()->NavigateAndCommit(GURL("https://secure_impression.com")); + auto navigation = NavigationSimulatorImpl::CreateRendererInitiated( + GURL(kConversionUrl), main_rfh()); + navigation->SetInitiatorFrame(main_rfh()); + navigation->set_impression(CreateValidImpression()); + navigation->Commit(); + + EXPECT_EQ(1u, test_manager_.num_impressions()); +} + +TEST_F(ConversionHostTest, ImpressionWithNoManagerAvilable_NoCrash) { + // Replace the ConversionHost on the WebContents with one that is backed by a + // null ConversionManager. + static_cast<WebContentsImpl*>(web_contents()) + ->RemoveReceiverSetForTesting(blink::mojom::ConversionHost::Name_); + auto conversion_host = ConversionHost::CreateForTesting( + web_contents(), std::make_unique<TestManagerProvider>(nullptr)); + + auto navigation = NavigationSimulatorImpl::CreateRendererInitiated( + GURL(kConversionUrl), main_rfh()); + navigation->SetInitiatorFrame(main_rfh()); + navigation->set_impression(CreateValidImpression()); + navigation->Commit(); +} + +TEST_F(ConversionHostTest, ImpressionInSubframe_Ignored) { + contents()->NavigateAndCommit(GURL("https://secure_impression.com")); + + // Create a subframe and use it as a target for the conversion registration + // mojo. + content::RenderFrameHostTester* rfh_tester = + content::RenderFrameHostTester::For(main_rfh()); + content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe"); + + auto navigation = NavigationSimulatorImpl::CreateRendererInitiated( + GURL(kConversionUrl), subframe); + navigation->SetInitiatorFrame(main_rfh()); + navigation->set_impression(CreateValidImpression()); + navigation->Commit(); + + EXPECT_EQ(0u, test_manager_.num_impressions()); +} + +// Test that if we cannot access the initiator frame of the navigation, we +// ignore the associated impression. +TEST_F(ConversionHostTest, ImpressionNavigationWithDeadInitiator_Ignored) { + contents()->NavigateAndCommit(GURL("https://secure_impression.com")); + + auto navigation = NavigationSimulatorImpl::CreateRendererInitiated( + GURL(kConversionUrl), main_rfh()); + navigation->set_impression(CreateValidImpression()); + navigation->Commit(); + + EXPECT_EQ(0u, test_manager_.num_impressions()); +} + +TEST_F(ConversionHostTest, ImpressionNavigationCommitsToErrorPage_Ignored) { + contents()->NavigateAndCommit(GURL("https://secure_impression.com")); + + auto navigation = NavigationSimulatorImpl::CreateRendererInitiated( + GURL(kConversionUrl), main_rfh()); + navigation->SetInitiatorFrame(main_rfh()); + navigation->set_impression(CreateValidImpression()); + navigation->Fail(net::ERR_FAILED); + navigation->CommitErrorPage(); + + EXPECT_EQ(0u, test_manager_.num_impressions()); +} + +TEST_F(ConversionHostTest, ImpressionNavigationAborts_Ignored) { + contents()->NavigateAndCommit(GURL("https://secure_impression.com")); + + auto navigation = NavigationSimulatorImpl::CreateRendererInitiated( + GURL(kConversionUrl), main_rfh()); + navigation->SetInitiatorFrame(main_rfh()); + navigation->set_impression(CreateValidImpression()); + navigation->AbortCommit(); + + EXPECT_EQ(0u, test_manager_.num_impressions()); +} + +TEST_F(ConversionHostTest, + CommittedOriginDiffersFromConversionDesintation_Ignored) { + contents()->NavigateAndCommit(GURL("https://secure_impression.com")); + + auto navigation = NavigationSimulatorImpl::CreateRendererInitiated( + GURL("https://different.com"), main_rfh()); + navigation->SetInitiatorFrame(main_rfh()); + navigation->set_impression(CreateValidImpression()); + navigation->Commit(); + + EXPECT_EQ(0u, test_manager_.num_impressions()); +} + +TEST_F(ConversionHostTest, + ImpressionNavigation_OriginTrustworthyChecksPerformed) { + const char kLocalHost[] = "http://localhost"; + + struct { + std::string impression_origin; + std::string conversion_origin; + std::string reporting_origin; + bool impression_expected; + } kTestCases[] = { + {kLocalHost /* impression_origin */, kLocalHost /* conversion_origin */, + kLocalHost /* reporting_origin */, true /* impression_expected */}, + {"http://127.0.0.1" /* impression_origin */, + "http://127.0.0.1" /* conversion_origin */, + "http://127.0.0.1" /* reporting_origin */, + true /* impression_expected */}, + {kLocalHost /* impression_origin */, kLocalHost /* conversion_origin */, + "http://insecure.com" /* reporting_origin */, + false /* impression_expected */}, + {kLocalHost /* impression_origin */, + "http://insecure.com" /* conversion_origin */, + kLocalHost /* reporting_origin */, false /* impression_expected */}, + {"http://insecure.com" /* impression_origin */, + kLocalHost /* conversion_origin */, kLocalHost /* reporting_origin */, + false /* impression_expected */}, + {"https://secure.com" /* impression_origin */, + "https://secure.com" /* conversion_origin */, + "https://secure.com" /* reporting_origin */, + true /* impression_expected */}, + }; + + for (const auto& test_case : kTestCases) { + contents()->NavigateAndCommit(GURL(test_case.impression_origin)); + auto navigation = NavigationSimulatorImpl::CreateRendererInitiated( + GURL(test_case.conversion_origin), main_rfh()); + + Impression impression; + impression.conversion_destination = + url::Origin::Create(GURL(test_case.conversion_origin)); + impression.reporting_origin = + url::Origin::Create(GURL(test_case.reporting_origin)); + navigation->set_impression(impression); + navigation->SetInitiatorFrame(main_rfh()); + navigation->Commit(); + + EXPECT_EQ(test_case.impression_expected, test_manager_.num_impressions()) + << "For test case: " << test_case.impression_origin << " | " + << test_case.conversion_origin << " | " << test_case.reporting_origin; + test_manager_.Reset(); + } } } // namespace content diff --git a/chromium/content/browser/conversions/conversion_internals.mojom b/chromium/content/browser/conversions/conversion_internals.mojom new file mode 100644 index 00000000000..8bfdeca562c --- /dev/null +++ b/chromium/content/browser/conversions/conversion_internals.mojom @@ -0,0 +1,54 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +module mojom; + +import "url/mojom/origin.mojom"; + +// Struct containing stored data that will be sent in a future conversion +// report. +struct WebUIConversionReport { + string impression_data; + string conversion_data; + url.mojom.Origin conversion_origin; + url.mojom.Origin reporting_origin; + double report_time; + int32 attribution_credit; +}; + +// Struct representing a stored impression that will be displayed by WebUI. +struct WebUIImpression { + string impression_data; + url.mojom.Origin impression_origin; + url.mojom.Origin conversion_destination; + url.mojom.Origin reporting_origin; + double impression_time; + double expiry_time; +}; + +// Mojo interface used for communication between a WebUI and the storage layer +// for conversion measurement. +interface ConversionInternalsHandler { + // Returns whether conversion measurement is enabled in the browisng context + // the WebUI is in. + IsMeasurementEnabled() => (bool enabled); + + // Returns all active impressions that are persisted in storage. This does + // not include expired impressions, or impressions that can no longer convert + // due to reaching policy limits. + GetActiveImpressions() => (array<WebUIImpression> impressions); + + // Returns all reports contained in storage, including those that are actively + // being sent. + GetPendingReports() => (array<WebUIConversionReport> reports); + + // Sends all stored reports, ignoring delay, returning when the + // operation has been completed and all reports have been cleared from + // storage. + SendPendingReports() => (); + + // Deletes all persisted data for the Conversion API, returning when the + // operation has been completed. + ClearStorage() => (); +};
\ No newline at end of file diff --git a/chromium/content/browser/conversions/conversion_internals_browsertest.cc b/chromium/content/browser/conversions/conversion_internals_browsertest.cc new file mode 100644 index 00000000000..9f88538a1de --- /dev/null +++ b/chromium/content/browser/conversions/conversion_internals_browsertest.cc @@ -0,0 +1,284 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_internals_ui.h" + +#include "base/optional.h" +#include "base/time/time.h" +#include "content/browser/conversions/conversion_manager.h" +#include "content/browser/conversions/conversion_report.h" +#include "content/browser/conversions/conversion_test_utils.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_controller.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/content_browser_test.h" +#include "content/public/test/content_browser_test_utils.h" +#include "content/shell/browser/shell.h" + +namespace content { + +namespace { + +const char kConversionInternalsUrl[] = "chrome://conversion-internals/"; + +const base::string16 kCompleteTitle = base::ASCIIToUTF16("Complete"); + +} // namespace + +class ConversionInternalsWebUiBrowserTest : public ContentBrowserTest { + public: + ConversionInternalsWebUiBrowserTest() = default; + + void ClickRefreshButton() { + EXPECT_TRUE(ExecJsInWebUI("document.getElementById('refresh').click();")); + } + + // Executing javascript in the WebUI requires using an isolated world in which + // to execute the script because WebUI has a default CSP policy denying + // "eval()", which is what EvalJs uses under the hood. + bool ExecJsInWebUI(std::string script) { + return ExecJs(shell()->web_contents()->GetMainFrame(), script, + EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */); + } + + void OverrideWebUIConversionManager(ConversionManager* manager) { + content::WebUI* web_ui = shell()->web_contents()->GetWebUI(); + + // Performs a safe downcast to the concrete ConversionInternalsUI subclass. + ConversionInternalsUI* conversion_internals_ui = + web_ui ? web_ui->GetController()->GetAs<ConversionInternalsUI>() + : nullptr; + EXPECT_TRUE(conversion_internals_ui); + conversion_internals_ui->SetConversionManagerProviderForTesting( + std::make_unique<TestManagerProvider>(manager)); + } + + // Registers a mutation observer that sets the window title to |title| when + // the report table is empty. + void SetTitleOnReportsTableEmpty(const base::string16& title) { + const std::string kObserveEmptyReportsTableScript = R"( + let table = document.getElementById("report-table-body"); + let obs = new MutationObserver(() => { + if (table.children.length === 1 && + table.children[0].children[0].innerText === "No pending reports.") { + document.title = $1; + } + }); + obs.observe(table, {'childList': true});)"; + EXPECT_TRUE( + ExecJsInWebUI(JsReplace(kObserveEmptyReportsTableScript, title))); + } +}; + +IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest, + NavigationUrl_ResolvedToWebUI) { + EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl))); + + // Execute script to ensure the page has loaded correctly, executing similarly + // to ExecJsInWebUI(). + EXPECT_EQ( + true, + EvalJs(shell()->web_contents()->GetMainFrame(), + "document.body.innerHTML.search('Conversion Internals') >= 0;", + EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */)); +} + +IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest, + WebUIShownWithManager_MeasurementConsideredEnabled) { + EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl))); + + TestConversionManager manager; + OverrideWebUIConversionManager(&manager); + + // Create a mutation observer to wait for the content to render to the dom. + // Waiting on calls to TestConversionManager is not sufficient because the + // results are returned in promises. + std::string wait_script = R"( + let status = document.getElementById("feature-status-content"); + let obs = new MutationObserver(() => { + if (status.innerText.trim() === "enabled") { + document.title = $1; + } + }); + obs.observe(status, {'childList': true, 'characterData': true});)"; + EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle))); + + TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle); + ClickRefreshButton(); + EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle()); +} + +IN_PROC_BROWSER_TEST_F( + ConversionInternalsWebUiBrowserTest, + WebUIShownWithNoActiveImpression_NoImpressionsDisplayed) { + EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl))); + + TestConversionManager manager; + OverrideWebUIConversionManager(&manager); + + std::string wait_script = R"( + let table = document.getElementById("impression-table-body"); + let obs = new MutationObserver(() => { + if (table.children.length === 1 && + table.children[0].children[0].innerText === + "No active impressions.") { + document.title = $1; + } + }); + obs.observe(table, {'childList': true});)"; + EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle))); + + TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle); + ClickRefreshButton(); + EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle()); +} + +IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest, + WebUIShownWithActiveImpression_ImpressionsDisplayed) { + EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl))); + + TestConversionManager manager; + manager.SetActiveImpressionsForWebUI( + {ImpressionBuilder(base::Time::Now()).SetData("100").Build(), + ImpressionBuilder(base::Time::Now()).Build()}); + OverrideWebUIConversionManager(&manager); + + std::string wait_script = R"( + let table = document.getElementById("impression-table-body"); + let obs = new MutationObserver(() => { + if (table.children.length === 2 && + table.children[0].children[0].innerText === "0x100") { + document.title = $1; + } + }); + obs.observe(table, {'childList': true});)"; + EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle))); + + TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle); + ClickRefreshButton(); + EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle()); +} + +IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest, + WebUIShownWithNoReports_NoReportsDisplayed) { + EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl))); + + TestConversionManager manager; + OverrideWebUIConversionManager(&manager); + + TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle); + SetTitleOnReportsTableEmpty(kCompleteTitle); + ClickRefreshButton(); + EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle()); +} + +IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest, + WebUIShownWithPendingReports_ReportsDisplayed) { + EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl))); + + TestConversionManager manager; + ConversionReport report( + ImpressionBuilder(base::Time::Now()).SetData("100").Build(), + "7" /* conversion_data */, base::Time::Now() /* report_time */, + 1 /* conversion_id */); + manager.SetReportsForWebUI({report}); + OverrideWebUIConversionManager(&manager); + + std::string wait_script = R"( + let table = document.getElementById("report-table-body"); + let obs = new MutationObserver(() => { + if (table.children.length === 1 && + table.children[0].children[1].innerText === "0x7") { + document.title = $1; + } + }); + obs.observe(table, {'childList': true});)"; + EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle))); + + TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle); + ClickRefreshButton(); + EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle()); +} + +IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest, + WebUIWithPendingReportsClearStorage_ReportsRemoved) { + EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl))); + + TestConversionManager manager; + ConversionReport report( + ImpressionBuilder(base::Time::Now()).SetData("100").Build(), + "7" /* conversion_data */, base::Time::Now() /* report_time */, + 1 /* conversion_id */); + manager.SetReportsForWebUI({report}); + OverrideWebUIConversionManager(&manager); + + std::string wait_script = R"( + let table = document.getElementById("report-table-body"); + let obs = new MutationObserver(() => { + if (table.children.length === 1 && + table.children[0].children[1].innerText === "0x7") { + document.title = $1; + } + }); + obs.observe(table, {'childList': true});)"; + EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle))); + + // Wait for the table to rendered. + TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle); + ClickRefreshButton(); + EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle()); + + // Click the clear storage button and expect that the report table is emptied. + const base::string16 kDeleteTitle = base::ASCIIToUTF16("Delete"); + TitleWatcher delete_title_watcher(shell()->web_contents(), kDeleteTitle); + SetTitleOnReportsTableEmpty(kDeleteTitle); + + // Click the button. + EXPECT_TRUE(ExecJsInWebUI("document.getElementById('clear-data').click();")); + EXPECT_EQ(kDeleteTitle, delete_title_watcher.WaitAndGetTitle()); +} + +// TODO(johnidel): Use a real ConversionManager here and verify that the reports +// are actually sent. +IN_PROC_BROWSER_TEST_F(ConversionInternalsWebUiBrowserTest, + WebUISendReports_ReportsRemoved) { + EXPECT_TRUE(NavigateToURL(shell(), GURL(kConversionInternalsUrl))); + + TestConversionManager manager; + ConversionReport report( + ImpressionBuilder(base::Time::Now()).SetData("100").Build(), + "7" /* conversion_data */, base::Time::Now() /* report_time */, + 1 /* conversion_id */); + manager.SetReportsForWebUI({report}); + OverrideWebUIConversionManager(&manager); + + std::string wait_script = R"( + let table = document.getElementById("report-table-body"); + let obs = new MutationObserver(() => { + if (table.children.length === 1 && + table.children[0].children[1].innerText === "0x7") { + document.title = $1; + } + }); + obs.observe(table, {'childList': true});)"; + EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle))); + + // Wait for the table to rendered. + TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle); + ClickRefreshButton(); + EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle()); + + // Click the send reports button and expect that the report table is emptied. + const base::string16 kSentTitle = base::ASCIIToUTF16("Sent"); + TitleWatcher sent_title_watcher(shell()->web_contents(), kSentTitle); + SetTitleOnReportsTableEmpty(kSentTitle); + + EXPECT_TRUE( + ExecJsInWebUI("document.getElementById('send-reports').click();")); + EXPECT_EQ(kSentTitle, sent_title_watcher.WaitAndGetTitle()); +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_internals_handler_impl.cc b/chromium/content/browser/conversions/conversion_internals_handler_impl.cc new file mode 100644 index 00000000000..b1ec072a119 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_internals_handler_impl.cc @@ -0,0 +1,129 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_internals_handler_impl.h" + +#include <utility> +#include <vector> + +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/time/time.h" +#include "content/browser/conversions/conversion_manager_impl.h" +#include "content/browser/conversions/conversion_report.h" +#include "content/browser/conversions/storable_impression.h" +#include "content/browser/storage_partition_impl.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" + +namespace content { + +namespace { + +void ForwardImpressionsToWebUI( + ::mojom::ConversionInternalsHandler::GetActiveImpressionsCallback + web_ui_callback, + std::vector<StorableImpression> stored_impressions) { + std::vector<::mojom::WebUIImpressionPtr> web_ui_impressions; + web_ui_impressions.reserve(stored_impressions.size()); + + for (const StorableImpression& impression : stored_impressions) { + web_ui_impressions.push_back(::mojom::WebUIImpression::New( + impression.impression_data(), impression.impression_origin(), + impression.conversion_origin(), impression.reporting_origin(), + impression.impression_time().ToJsTime(), + impression.expiry_time().ToJsTime())); + } + + std::move(web_ui_callback).Run(std::move(web_ui_impressions)); +} + +void ForwardReportsToWebUI( + ::mojom::ConversionInternalsHandler::GetPendingReportsCallback + web_ui_callback, + std::vector<ConversionReport> stored_reports) { + std::vector<::mojom::WebUIConversionReportPtr> web_ui_reports; + web_ui_reports.reserve(stored_reports.size()); + + for (const ConversionReport& report : stored_reports) { + web_ui_reports.push_back(::mojom::WebUIConversionReport::New( + report.impression.impression_data(), report.conversion_data, + report.impression.conversion_origin(), + report.impression.reporting_origin(), report.report_time.ToJsTime(), + report.attribution_credit)); + } + std::move(web_ui_callback).Run(std::move(web_ui_reports)); +} + +} // namespace + +ConversionInternalsHandlerImpl::ConversionInternalsHandlerImpl( + WebUI* web_ui, + mojo::PendingReceiver<::mojom::ConversionInternalsHandler> receiver) + : web_ui_(web_ui), + manager_provider_(std::make_unique<ConversionManagerProviderImpl>()), + receiver_(this, std::move(receiver)) {} + +ConversionInternalsHandlerImpl::~ConversionInternalsHandlerImpl() = default; + +void ConversionInternalsHandlerImpl::IsMeasurementEnabled( + ::mojom::ConversionInternalsHandler::IsMeasurementEnabledCallback + callback) { + bool measurement_enabled = + manager_provider_->GetManager(web_ui_->GetWebContents()); + std::move(callback).Run(measurement_enabled); +} + +void ConversionInternalsHandlerImpl::GetActiveImpressions( + ::mojom::ConversionInternalsHandler::GetActiveImpressionsCallback + callback) { + if (ConversionManager* manager = + manager_provider_->GetManager(web_ui_->GetWebContents())) { + manager->GetActiveImpressionsForWebUI( + base::BindOnce(&ForwardImpressionsToWebUI, std::move(callback))); + } else { + std::move(callback).Run({}); + } +} + +void ConversionInternalsHandlerImpl::GetPendingReports( + ::mojom::ConversionInternalsHandler::GetPendingReportsCallback callback) { + if (ConversionManager* manager = + manager_provider_->GetManager(web_ui_->GetWebContents())) { + manager->GetReportsForWebUI( + base::BindOnce(&ForwardReportsToWebUI, std::move(callback)), + base::Time::Max()); + } else { + std::move(callback).Run({}); + } +} + +void ConversionInternalsHandlerImpl::SendPendingReports( + ::mojom::ConversionInternalsHandler::SendPendingReportsCallback callback) { + if (ConversionManager* manager = + manager_provider_->GetManager(web_ui_->GetWebContents())) { + manager->SendReportsForWebUI(std::move(callback)); + } else { + std::move(callback).Run(); + } +} + +void ConversionInternalsHandlerImpl::ClearStorage( + ::mojom::ConversionInternalsHandler::ClearStorageCallback callback) { + if (ConversionManager* manager = + manager_provider_->GetManager(web_ui_->GetWebContents())) { + manager->ClearData(base::Time::Min(), base::Time::Max(), + base::NullCallback(), std::move(callback)); + } else { + std::move(callback).Run(); + } +} + +void ConversionInternalsHandlerImpl::SetConversionManagerProviderForTesting( + std::unique_ptr<ConversionManager::Provider> manager_provider) { + manager_provider_ = std::move(manager_provider); +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_internals_handler_impl.h b/chromium/content/browser/conversions/conversion_internals_handler_impl.h new file mode 100644 index 00000000000..76f621308a8 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_internals_handler_impl.h @@ -0,0 +1,56 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_INTERNALS_HANDLER_IMPL_H_ +#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_INTERNALS_HANDLER_IMPL_H_ + +#include "content/browser/conversions/conversion_internals.mojom.h" +#include "content/browser/conversions/conversion_manager.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" + +namespace content { + +class WebUI; + +// Implements the mojo endpoint for the Conversion WebUI which proxies calls to +// the ConversionManager to get information about stored conversion data. Owned +// by ConversionInternalsUI. +class ConversionInternalsHandlerImpl + : public ::mojom::ConversionInternalsHandler { + public: + ConversionInternalsHandlerImpl( + WebUI* web_ui, + mojo::PendingReceiver<::mojom::ConversionInternalsHandler> receiver); + ~ConversionInternalsHandlerImpl() override; + + // mojom::ConversionInternalsHandler overrides: + void IsMeasurementEnabled( + ::mojom::ConversionInternalsHandler::IsMeasurementEnabledCallback + callback) override; + void GetActiveImpressions( + ::mojom::ConversionInternalsHandler::GetActiveImpressionsCallback + callback) override; + void GetPendingReports( + ::mojom::ConversionInternalsHandler::GetPendingReportsCallback callback) + override; + void SendPendingReports( + ::mojom::ConversionInternalsHandler::SendPendingReportsCallback callback) + override; + void ClearStorage(::mojom::ConversionInternalsHandler::ClearStorageCallback + callback) override; + + void SetConversionManagerProviderForTesting( + std::unique_ptr<ConversionManager::Provider> manager_provider); + + private: + WebUI* web_ui_; + std::unique_ptr<ConversionManager::Provider> manager_provider_; + + mojo::Receiver<::mojom::ConversionInternalsHandler> receiver_; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_INTERNALS_HANDLER_IMPL_H_ diff --git a/chromium/content/browser/conversions/conversion_internals_ui.cc b/chromium/content/browser/conversions/conversion_internals_ui.cc new file mode 100644 index 00000000000..20306d5f62c --- /dev/null +++ b/chromium/content/browser/conversions/conversion_internals_ui.cc @@ -0,0 +1,59 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_internals_ui.h" + +#include "content/browser/conversions/conversion_internals_handler_impl.h" +#include "content/browser/frame_host/render_frame_host_impl.h" +#include "content/grit/dev_ui_content_resources.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_data_source.h" +#include "content/public/common/bindings_policy.h" +#include "content/public/common/url_constants.h" + +namespace content { + +ConversionInternalsUI::ConversionInternalsUI(WebUI* web_ui) + : WebUIController(web_ui) { + // Initialize the UI with no bindings. Mojo bindings will be separately + // granted to frames within this WebContents. + web_ui->SetBindings(0); + WebUIDataSource* source = + WebUIDataSource::Create(kChromeUIConversionInternalsHost); + + source->AddResourcePath("conversion_internals.mojom-lite.js", + IDR_CONVERSION_INTERNALS_MOJOM_JS); + source->AddResourcePath("conversion_internals.js", + IDR_CONVERSION_INTERNALS_JS); + source->AddResourcePath("conversion_internals.css", + IDR_CONVERSION_INTERNALS_CSS); + source->SetDefaultResource(IDR_CONVERSION_INTERNALS_HTML); + WebUIDataSource::Add(web_ui->GetWebContents()->GetBrowserContext(), source); +} + +WEB_UI_CONTROLLER_TYPE_IMPL(ConversionInternalsUI) + +ConversionInternalsUI::~ConversionInternalsUI() = default; + +void ConversionInternalsUI::RenderFrameCreated(RenderFrameHost* rfh) { + // Enable the JavaScript Mojo bindings in the renderer process, so the JS + // code can call the Mojo APIs exposed by this WebUI. + static_cast<RenderFrameHostImpl*>(rfh)->EnableMojoJsBindings(); +} + +void ConversionInternalsUI::BindInterface( + mojo::PendingReceiver<::mojom::ConversionInternalsHandler> receiver) { + ui_handler_ = std::make_unique<ConversionInternalsHandlerImpl>( + web_ui(), std::move(receiver)); +} + +void ConversionInternalsUI::SetConversionManagerProviderForTesting( + std::unique_ptr<ConversionManager::Provider> manager_provider) { + ui_handler_->SetConversionManagerProviderForTesting( + std::move(manager_provider)); +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_internals_ui.h b/chromium/content/browser/conversions/conversion_internals_ui.h new file mode 100644 index 00000000000..88fbdf797c3 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_internals_ui.h @@ -0,0 +1,47 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_INTERNALS_UI_H_ +#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_INTERNALS_UI_H_ + +#include <memory> + +#include "content/browser/conversions/conversion_internals.mojom.h" +#include "content/browser/conversions/conversion_manager.h" +#include "content/common/content_export.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_ui_controller.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" + +namespace content { + +class ConversionInternalsHandlerImpl; + +// WebUI which handles serving the chrome://conversion-internals page. +class CONTENT_EXPORT ConversionInternalsUI : public WebUIController, + public WebContentsObserver { + public: + explicit ConversionInternalsUI(WebUI* web_ui); + ConversionInternalsUI(const ConversionInternalsUI& other) = delete; + ConversionInternalsUI& operator=(const ConversionInternalsUI& other) = delete; + ~ConversionInternalsUI() override; + + // WebContentsObserver: + void RenderFrameCreated(RenderFrameHost* render_frame_host) override; + + void BindInterface( + mojo::PendingReceiver<::mojom::ConversionInternalsHandler> receiver); + + void SetConversionManagerProviderForTesting( + std::unique_ptr<ConversionManager::Provider> manager_provider); + + private: + std::unique_ptr<ConversionInternalsHandlerImpl> ui_handler_; + + WEB_UI_CONTROLLER_TYPE_DECL(); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_INTERNALS_UI_H_ diff --git a/chromium/content/browser/conversions/conversion_manager.cc b/chromium/content/browser/conversions/conversion_manager.cc deleted file mode 100644 index 2e358851a61..00000000000 --- a/chromium/content/browser/conversions/conversion_manager.cc +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "content/browser/conversions/conversion_manager.h" - -#include <memory> - -#include "base/bind.h" -#include "base/task_runner_util.h" -#include "base/time/default_clock.h" -#include "content/browser/conversions/conversion_storage_sql.h" - -namespace content { - -ConversionManager::ConversionManager( - const base::FilePath& user_data_directory, - scoped_refptr<base::SequencedTaskRunner> task_runner) - : storage_task_runner_(std::move(task_runner)), - clock_(base::DefaultClock::GetInstance()), - storage_(new ConversionStorageSql(user_data_directory, this, clock_), - base::OnTaskRunnerDeleter(storage_task_runner_)), - conversion_policy_(std::make_unique<ConversionPolicy>()), - weak_factory_(this) { - // Unretained is safe because any task to delete |storage_| will be posted - // after this one. - base::PostTaskAndReplyWithResult( - storage_task_runner_.get(), FROM_HERE, - base::BindOnce(&ConversionStorage::Initialize, - base::Unretained(storage_.get())), - base::BindOnce(&ConversionManager::OnInitCompleted, - weak_factory_.GetWeakPtr())); -} - -ConversionManager::~ConversionManager() = default; - -void ConversionManager::HandleConversion(const StorableConversion& conversion) { - if (!storage_) - return; - - // TODO(https://crbug.com/1043345): Add UMA for the number of conversions we - // are logging to storage, and the number of new reports logged to storage. - // Unretained is safe because any task to delete |storage_| will be posted - // after this one. - storage_task_runner_.get()->PostTask( - FROM_HERE, - base::BindOnce( - base::IgnoreResult( - &ConversionStorage::MaybeCreateAndStoreConversionReports), - base::Unretained(storage_.get()), conversion)); -} - -const ConversionPolicy& ConversionManager::GetConversionPolicy() const { - return *conversion_policy_; -} - -void ConversionManager::ProcessNewConversionReports( - std::vector<ConversionReport>* reports) { - for (ConversionReport& report : *reports) { - report.report_time = conversion_policy_->GetReportTimeForConversion(report); - } - - conversion_policy_->AssignAttributionCredits(reports); -} - -int ConversionManager::GetMaxConversionsPerImpression() const { - return conversion_policy_->GetMaxConversionsPerImpression(); -} - -void ConversionManager::OnInitCompleted(bool success) { - if (!success) - storage_.reset(); -} - -} // namespace content diff --git a/chromium/content/browser/conversions/conversion_manager.h b/chromium/content/browser/conversions/conversion_manager.h index fe132c41e2c..ca1e1a041f8 100644 --- a/chromium/content/browser/conversions/conversion_manager.h +++ b/chromium/content/browser/conversions/conversion_manager.h @@ -5,68 +5,76 @@ #ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_MANAGER_H_ #define CONTENT_BROWSER_CONVERSIONS_CONVERSION_MANAGER_H_ -#include <memory> +#include <vector> -#include "base/files/file_path.h" -#include "base/macros.h" -#include "base/memory/scoped_refptr.h" -#include "base/memory/weak_ptr.h" -#include "base/sequenced_task_runner.h" +#include "base/callback.h" +#include "base/callback_forward.h" #include "content/browser/conversions/conversion_policy.h" -#include "content/browser/conversions/conversion_storage.h" +#include "content/browser/conversions/conversion_report.h" #include "content/browser/conversions/storable_conversion.h" - -namespace base { -class Clock; -} // namespace base +#include "content/browser/conversions/storable_impression.h" +#include "content/common/content_export.h" +#include "url/origin.h" namespace content { -class ConversionStorage; +class WebContents; -// UI thread class that manages the lifetime of the underlying conversion -// storage. Owned by the storage partition. -class ConversionManager : public ConversionStorage::Delegate { +// Interface that mediates data flow between the network, storage layer, and +// blink. +class CONTENT_EXPORT ConversionManager { public: - // |storage_task_runner| should run with base::TaskPriority::BEST_EFFORT. - ConversionManager( - const base::FilePath& user_data_directory, - scoped_refptr<base::SequencedTaskRunner> storage_task_runner); - ConversionManager(const ConversionManager& other) = delete; - ConversionManager& operator=(const ConversionManager& other) = delete; - ~ConversionManager() override; + // Provides access to a ConversionManager implementation. This layer of + // abstraction is to allow tests to mock out the ConversionManager without + // injecting a manager explicitly. + class Provider { + public: + virtual ~Provider() = default; + + // Gets the ConversionManager that should be used for handling conversions + // that occur in the given |web_contents|. Returns nullptr if conversion + // measurement is not enabled in the given |web_contents|, e.g. when the + // browser context is off the record. + virtual ConversionManager* GetManager(WebContents* web_contents) const = 0; + }; + virtual ~ConversionManager() = default; + + // Persists the given |impression| to storage. Called when a navigation + // originating from an impression tag finishes. + virtual void HandleImpression(const StorableImpression& impression) = 0; // Process a newly registered conversion. Will create and log any new // conversion reports to storage. - void HandleConversion(const StorableConversion& conversion); - - const ConversionPolicy& GetConversionPolicy() const; - - private: - // ConversionStorageDelegate - void ProcessNewConversionReports( - std::vector<ConversionReport>* reports) override; - int GetMaxConversionsPerImpression() const override; - - void OnInitCompleted(bool success); - - // Task runner used to perform operations on |storage_|. Runs with - // base::TaskPriority::BEST_EFFORT. - scoped_refptr<base::SequencedTaskRunner> storage_task_runner_; - - base::Clock* clock_; - - // ConversionStorage instance which is scoped to lifetime of - // |storage_task_runner_|. |storage_| should be accessed by calling - // base::PostTask with |storage_task_runner_|, and should not be accessed - // directly. - std::unique_ptr<ConversionStorage, base::OnTaskRunnerDeleter> storage_; - - // Policy used for controlling API configurations such as reporting and - // attribution models. Unique ptr so it can be overridden for testing. - std::unique_ptr<ConversionPolicy> conversion_policy_; - - base::WeakPtrFactory<ConversionManager> weak_factory_; + virtual void HandleConversion(const StorableConversion& conversion) = 0; + + // Get all impressions that are currently stored in this partition. Used for + // populating WebUI. + virtual void GetActiveImpressionsForWebUI( + base::OnceCallback<void(std::vector<StorableImpression>)> callback) = 0; + + // Get all pending reports that are currently stored in this partition. Used + // for populating WebUI. + virtual void GetReportsForWebUI( + base::OnceCallback<void(std::vector<ConversionReport>)> callback, + base::Time max_report_time) = 0; + + // Sends all pending reports immediately, and runs |done| once they have all + // been sent. + virtual void SendReportsForWebUI(base::OnceClosure done) = 0; + + // Returns the ConversionPolicy that is used to control API policies such + // as noise. + virtual const ConversionPolicy& GetConversionPolicy() const = 0; + + // Deletes all data in storage for URLs matching |filter|, between + // |delete_begin| and |delete_end| time. + // + // If |filter| is null, then consider all origins in storage as matching. + virtual void ClearData( + base::Time delete_begin, + base::Time delete_end, + base::RepeatingCallback<bool(const url::Origin&)> filter, + base::OnceClosure done) = 0; }; } // namespace content diff --git a/chromium/content/browser/conversions/conversion_manager_impl.cc b/chromium/content/browser/conversions/conversion_manager_impl.cc new file mode 100644 index 00000000000..4f3f9ca7e5a --- /dev/null +++ b/chromium/content/browser/conversions/conversion_manager_impl.cc @@ -0,0 +1,282 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_manager_impl.h" + +#include <utility> + +#include "base/barrier_closure.h" +#include "base/bind.h" +#include "base/command_line.h" +#include "base/memory/ptr_util.h" +#include "base/task_runner_util.h" +#include "base/time/default_clock.h" +#include "content/browser/conversions/conversion_reporter_impl.h" +#include "content/browser/conversions/conversion_storage_delegate_impl.h" +#include "content/browser/conversions/conversion_storage_sql.h" +#include "content/browser/storage_partition_impl.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_switches.h" + +namespace content { + +const constexpr base::TimeDelta kConversionManagerQueueReportsInterval = + base::TimeDelta::FromMinutes(30); + +ConversionManager* ConversionManagerProviderImpl::GetManager( + WebContents* web_contents) const { + return static_cast<StoragePartitionImpl*>( + BrowserContext::GetDefaultStoragePartition( + web_contents->GetBrowserContext())) + ->GetConversionManager(); +} + +// static +std::unique_ptr<ConversionManagerImpl> ConversionManagerImpl::CreateForTesting( + std::unique_ptr<ConversionReporter> reporter, + std::unique_ptr<ConversionPolicy> policy, + const base::Clock* clock, + const base::FilePath& user_data_directory, + scoped_refptr<base::SequencedTaskRunner> storage_task_runner) { + return base::WrapUnique<ConversionManagerImpl>(new ConversionManagerImpl( + std::move(reporter), std::move(policy), clock, user_data_directory, + std::move(storage_task_runner))); +} + +ConversionManagerImpl::ConversionManagerImpl( + StoragePartition* storage_partition, + const base::FilePath& user_data_directory, + scoped_refptr<base::SequencedTaskRunner> task_runner) + : ConversionManagerImpl( + std::make_unique<ConversionReporterImpl>( + storage_partition, + base::DefaultClock::GetInstance()), + std::make_unique<ConversionPolicy>( + base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kConversionsDebugMode)), + base::DefaultClock::GetInstance(), + user_data_directory, + std::move(task_runner)) {} + +ConversionManagerImpl::ConversionManagerImpl( + std::unique_ptr<ConversionReporter> reporter, + std::unique_ptr<ConversionPolicy> policy, + const base::Clock* clock, + const base::FilePath& user_data_directory, + scoped_refptr<base::SequencedTaskRunner> storage_task_runner) + : debug_mode_(base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kConversionsDebugMode)), + storage_task_runner_(std::move(storage_task_runner)), + clock_(clock), + reporter_(std::move(reporter)), + storage_(new ConversionStorageSql( + user_data_directory, + std::make_unique<ConversionStorageDelegateImpl>(debug_mode_), + clock_), + base::OnTaskRunnerDeleter(storage_task_runner_)), + conversion_policy_(std::move(policy)), + weak_factory_(this) { + // Unretained is safe because any task to delete |storage_| will be posted + // after this one. + base::PostTaskAndReplyWithResult( + storage_task_runner_.get(), FROM_HERE, + base::BindOnce(&ConversionStorage::Initialize, + base::Unretained(storage_.get())), + base::BindOnce(&ConversionManagerImpl::OnInitCompleted, + weak_factory_.GetWeakPtr())); +} + +ConversionManagerImpl::~ConversionManagerImpl() = default; + +void ConversionManagerImpl::HandleImpression( + const StorableImpression& impression) { + if (!storage_) + return; + + // Add the impression to storage. + storage_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&ConversionStorage::StoreImpression, + base::Unretained(storage_.get()), impression)); +} + +void ConversionManagerImpl::HandleConversion( + const StorableConversion& conversion) { + if (!storage_) + return; + + // TODO(https://crbug.com/1043345): Add UMA for the number of conversions we + // are logging to storage, and the number of new reports logged to storage. + // Unretained is safe because any task to delete |storage_| will be posted + // after this one. + storage_task_runner_.get()->PostTask( + FROM_HERE, + base::BindOnce( + base::IgnoreResult( + &ConversionStorage::MaybeCreateAndStoreConversionReports), + base::Unretained(storage_.get()), conversion)); + + // If we are running in debug mode, we should also schedule a task to + // gather and send any new reports. + if (debug_mode_) + GetAndQueueReportsForNextInterval(); +} + +void ConversionManagerImpl::GetActiveImpressionsForWebUI( + base::OnceCallback<void(std::vector<StorableImpression>)> callback) { + // Unretained is safe because any task to delete |storage_| will be posted + // after this one because |storage_| uses base::OnTaskRunnerDeleter. + base::PostTaskAndReplyWithResult( + storage_task_runner_.get(), FROM_HERE, + base::BindOnce(&ConversionStorage::GetActiveImpressions, + base::Unretained(storage_.get())), + std::move(callback)); +} + +void ConversionManagerImpl::GetReportsForWebUI( + base::OnceCallback<void(std::vector<ConversionReport>)> callback, + base::Time max_report_time) { + GetAndHandleReports(std::move(callback), max_report_time); +} + +void ConversionManagerImpl::SendReportsForWebUI(base::OnceClosure done) { + GetAndHandleReports( + base::BindOnce(&ConversionManagerImpl::HandleReportsSentFromWebUI, + weak_factory_.GetWeakPtr(), std::move(done)), + base::Time::Max()); +} + +const ConversionPolicy& ConversionManagerImpl::GetConversionPolicy() const { + return *conversion_policy_; +} + +void ConversionManagerImpl::ClearData( + base::Time delete_begin, + base::Time delete_end, + base::RepeatingCallback<bool(const url::Origin&)> filter, + base::OnceClosure done) { + storage_task_runner_->PostTaskAndReply( + FROM_HERE, + base::BindOnce(&ConversionStorage::ClearData, + base::Unretained(storage_.get()), delete_begin, delete_end, + std::move(filter)), + std::move(done)); +} + +void ConversionManagerImpl::OnInitCompleted(bool success) { + if (!success) { + storage_.reset(); + return; + } + + // Once the database is loaded, get all reports that may have expired while + // Chrome was not running and handle these specially. + GetAndHandleReports( + base::BindOnce(&ConversionManagerImpl::HandleReportsExpiredAtStartup, + weak_factory_.GetWeakPtr()), + clock_->Now() + kConversionManagerQueueReportsInterval); + + // Start a repeating timer that will fetch reports once every + // |kConversionManagerQueueReportsInterval| and add them to |reporter_|. + get_and_queue_reports_timer_.Start( + FROM_HERE, kConversionManagerQueueReportsInterval, this, + &ConversionManagerImpl::GetAndQueueReportsForNextInterval); +} + +void ConversionManagerImpl::GetAndHandleReports( + ReportsHandlerFunc handler_function, + base::Time max_report_time) { + base::PostTaskAndReplyWithResult( + storage_task_runner_.get(), FROM_HERE, + base::BindOnce(&ConversionStorage::GetConversionsToReport, + base::Unretained(storage_.get()), max_report_time), + std::move(handler_function)); +} + +void ConversionManagerImpl::GetAndQueueReportsForNextInterval() { + // Get all the reports that will be reported in the next interval and them to + // the |reporter_|. + GetAndHandleReports(base::BindOnce(&ConversionManagerImpl::QueueReports, + weak_factory_.GetWeakPtr()), + clock_->Now() + kConversionManagerQueueReportsInterval); +} + +void ConversionManagerImpl::QueueReports( + std::vector<ConversionReport> reports) { + if (!reports.empty()) { + // |reporter_| is owned by |this|, so base::Unretained() is safe as the + // reporter and callbacks will be deleted first. + reporter_->AddReportsToQueue( + std::move(reports), + base::BindRepeating(&ConversionManagerImpl::OnReportSent, + base::Unretained(this))); + } +} + +void ConversionManagerImpl::HandleReportsExpiredAtStartup( + std::vector<ConversionReport> reports) { + // Add delay to all reports that expired while the browser was not running so + // they are not temporally join-able. + base::Time current_time = clock_->Now(); + for (ConversionReport& report : reports) { + if (report.report_time > current_time) + continue; + + base::Time updated_report_time = + conversion_policy_->GetReportTimeForExpiredReportAtStartup( + current_time); + + report.extra_delay = updated_report_time - report.report_time; + report.report_time = updated_report_time; + } + QueueReports(std::move(reports)); +} + +void ConversionManagerImpl::HandleReportsSentFromWebUI( + base::OnceClosure done, + std::vector<ConversionReport> reports) { + if (reports.empty()) { + std::move(done).Run(); + return; + } + + // All reports should be sent immediately. + for (ConversionReport& report : reports) { + report.report_time = base::Time::Min(); + } + + // Wraps |done| so that is will run once all of the reports have finished + // sending. + base::RepeatingClosure all_reports_sent = + base::BarrierClosure(reports.size(), std::move(done)); + + // |reporter_| is owned by |this|, so base::Unretained() is safe as the + // reporter and callbacks will be deleted first. + reporter_->AddReportsToQueue( + std::move(reports), + base::BindRepeating(&ConversionManagerImpl::OnReportSentFromWebUI, + base::Unretained(this), std::move(all_reports_sent))); +} + +void ConversionManagerImpl::OnReportSent(int64_t conversion_id) { + storage_task_runner_->PostTask( + FROM_HERE, + base::BindOnce(base::IgnoreResult(&ConversionStorage::DeleteConversion), + base::Unretained(storage_.get()), conversion_id)); +} + +void ConversionManagerImpl::OnReportSentFromWebUI( + base::OnceClosure reports_sent_barrier, + int64_t conversion_id) { + // |reports_sent_barrier| is a OnceClosure view of a RepeatingClosure obtained + // by base::BarrierClosure(). + storage_task_runner_->PostTaskAndReply( + FROM_HERE, + base::BindOnce(base::IgnoreResult(&ConversionStorage::DeleteConversion), + base::Unretained(storage_.get()), conversion_id), + std::move(reports_sent_barrier)); +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_manager_impl.h b/chromium/content/browser/conversions/conversion_manager_impl.h new file mode 100644 index 00000000000..03bcd94688f --- /dev/null +++ b/chromium/content/browser/conversions/conversion_manager_impl.h @@ -0,0 +1,182 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_MANAGER_IMPL_H_ +#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_MANAGER_IMPL_H_ + +#include <memory> +#include <vector> + +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/memory/scoped_refptr.h" +#include "base/memory/weak_ptr.h" +#include "base/sequenced_task_runner.h" +#include "base/timer/timer.h" +#include "content/browser/conversions/conversion_manager.h" +#include "content/browser/conversions/conversion_policy.h" +#include "content/browser/conversions/conversion_storage.h" + +namespace base { +class Clock; +} // namespace base + +namespace content { + +// Frequency we pull ConversionReports from storage and queue them to be +// reported. +extern CONTENT_EXPORT const base::TimeDelta + kConversionManagerQueueReportsInterval; + +class ConversionStorage; +class StoragePartition; + +// Provides access to the manager owned by the default StoragePartition. +class ConversionManagerProviderImpl : public ConversionManager::Provider { + public: + ConversionManagerProviderImpl() = default; + ConversionManagerProviderImpl(const ConversionManagerProviderImpl& other) = + delete; + ConversionManagerProviderImpl& operator=( + const ConversionManagerProviderImpl& other) = delete; + ~ConversionManagerProviderImpl() override = default; + + // ConversionManagerProvider: + ConversionManager* GetManager(WebContents* web_contents) const override; +}; + +// UI thread class that manages the lifetime of the underlying conversion +// storage. Owned by the storage partition. +class CONTENT_EXPORT ConversionManagerImpl : public ConversionManager { + public: + // Interface which manages the ownership, queuing, and sending of pending + // conversion reports. Owned by |this|. + class ConversionReporter { + public: + virtual ~ConversionReporter() = default; + + // Adds |reports| to a shared queue of reports that need to be sent. Runs + // |report_sent_callback| for every report that is sent, with the associated + // |conversion_id| of the report. + virtual void AddReportsToQueue( + std::vector<ConversionReport> reports, + base::RepeatingCallback<void(int64_t)> report_sent_callback) = 0; + }; + + static std::unique_ptr<ConversionManagerImpl> CreateForTesting( + std::unique_ptr<ConversionReporter> reporter, + std::unique_ptr<ConversionPolicy> policy, + const base::Clock* clock, + const base::FilePath& user_data_directory, + scoped_refptr<base::SequencedTaskRunner> storage_task_runner); + + // |storage_task_runner| should run with base::TaskPriority::BEST_EFFORT. + ConversionManagerImpl( + StoragePartition* storage_partition, + const base::FilePath& user_data_directory, + scoped_refptr<base::SequencedTaskRunner> storage_task_runner); + ConversionManagerImpl(const ConversionManagerImpl& other) = delete; + ConversionManagerImpl& operator=(const ConversionManagerImpl& other) = delete; + ~ConversionManagerImpl() override; + + // ConversionManager: + void HandleImpression(const StorableImpression& impression) override; + void HandleConversion(const StorableConversion& conversion) override; + void GetActiveImpressionsForWebUI( + base::OnceCallback<void(std::vector<StorableImpression>)> callback) + override; + void GetReportsForWebUI( + base::OnceCallback<void(std::vector<ConversionReport>)> callback, + base::Time max_report_time) override; + void SendReportsForWebUI(base::OnceClosure done) override; + const ConversionPolicy& GetConversionPolicy() const override; + void ClearData(base::Time delete_begin, + base::Time delete_end, + base::RepeatingCallback<bool(const url::Origin&)> filter, + base::OnceClosure done) override; + + private: + ConversionManagerImpl( + std::unique_ptr<ConversionReporter> reporter, + std::unique_ptr<ConversionPolicy> policy, + const base::Clock* clock, + const base::FilePath& user_data_directory, + scoped_refptr<base::SequencedTaskRunner> storage_task_runner); + + void OnInitCompleted(bool success); + + // Retrieves reports from storage whose |report_time| <= |max_report_time|, + // and calls |handler_function| on them. + using ReportsHandlerFunc = + base::OnceCallback<void(std::vector<ConversionReport>)>; + void GetAndHandleReports(ReportsHandlerFunc handler_function, + base::Time max_report_time); + + // Get the next set of reports from storage that need to be sent before the + // next call from |get_and_queue_reports_timer_|. Adds the reports to + // |reporter|. + void GetAndQueueReportsForNextInterval(); + + // Queue the given |reports| on |reporter_|. + void QueueReports(std::vector<ConversionReport> reports); + + void HandleReportsExpiredAtStartup(std::vector<ConversionReport> reports); + + void HandleReportsSentFromWebUI(base::OnceClosure done, + std::vector<ConversionReport> reports); + + // Notify storage to delete the given |conversion_id| when it's associated + // report has been sent. + void OnReportSent(int64_t conversion_id); + + // Similar to OnReportSent, but invokes |reports_sent_barrier| when the + // report has been removed from storage. + void OnReportSentFromWebUI(base::OnceClosure reports_sent_barrier, + int64_t conversion_id); + + // Friend to expose the ConversionStorage and task runner, consider changing + // to just expose the storage if it moves to SequenceBound. + friend std::vector<ConversionReport> GetConversionsToReportForTesting( + ConversionManagerImpl* manager, + base::Time max_report_time); + + // Whether the API is running in debug mode, meaning that there should be + // no delays or noise added to reports. This is used by end to end tests to + // verify functionality without mocking out any implementations. + const bool debug_mode_; + + // Task runner used to perform operations on |storage_|. Runs with + // base::TaskPriority::BEST_EFFORT. + scoped_refptr<base::SequencedTaskRunner> storage_task_runner_; + + const base::Clock* clock_; + + // Timer which administers calls to GetAndQueueReportsForNextInterval(). + base::RepeatingTimer get_and_queue_reports_timer_; + + // Handle keeping track of conversion reports to send. Reports are fetched + // from |storage_| and added to |reporter_| by |get_reports_timer_|. + std::unique_ptr<ConversionReporter> reporter_; + + // ConversionStorage instance which is scoped to lifetime of + // |storage_task_runner_|. |storage_| should be accessed by calling + // base::PostTask with |storage_task_runner_|, and should not be accessed + // directly. |storage_| can be null if the storage initialization did not + // succeed. + // + // TODO(https://crbug.com/1066920): This should use base::SequenceBound to + // avoid having to call PostTask manually, as well as use base::Unretained on + // |storage_|. + std::unique_ptr<ConversionStorage, base::OnTaskRunnerDeleter> storage_; + + // Policy used for controlling API configurations such as reporting and + // attribution models. Unique ptr so it can be overridden for testing. + std::unique_ptr<ConversionPolicy> conversion_policy_; + + base::WeakPtrFactory<ConversionManagerImpl> weak_factory_; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_MANAGER_IMPL_H_ diff --git a/chromium/content/browser/conversions/conversion_manager_impl_unittest.cc b/chromium/content/browser/conversions/conversion_manager_impl_unittest.cc new file mode 100644 index 00000000000..3ca019fd0d1 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_manager_impl_unittest.cc @@ -0,0 +1,365 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_manager_impl.h" + +#include <stdint.h> + +#include <memory> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback_forward.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "base/sequenced_task_runner.h" +#include "base/test/bind_test_util.h" +#include "base/time/clock.h" +#include "base/time/time.h" +#include "content/browser/conversions/conversion_report.h" +#include "content/browser/conversions/conversion_test_utils.h" +#include "content/browser/conversions/storable_conversion.h" +#include "content/browser/conversions/storable_impression.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +namespace { + +constexpr base::TimeDelta kExpiredReportOffset = + base::TimeDelta::FromMinutes(2); + +class ConstantStartupDelayPolicy : public ConversionPolicy { + public: + ConstantStartupDelayPolicy() = default; + ~ConstantStartupDelayPolicy() override = default; + + base::Time GetReportTimeForExpiredReportAtStartup( + base::Time now) const override { + return now + kExpiredReportOffset; + } +}; + +// Mock reporter that tracks reports being queued by the ConversionManager. +class TestConversionReporter + : public ConversionManagerImpl::ConversionReporter { + public: + TestConversionReporter() = default; + ~TestConversionReporter() override = default; + + // ConversionManagerImpl::ConversionReporter + void AddReportsToQueue( + std::vector<ConversionReport> reports, + base::RepeatingCallback<void(int64_t)> report_sent_callback) override { + num_reports_ += reports.size(); + last_conversion_id_ = *reports.back().conversion_id; + last_report_time_ = reports.back().report_time; + + if (should_run_report_sent_callbacks_) { + for (const auto& report : reports) { + report_sent_callback.Run(*report.conversion_id); + } + } + + if (quit_closure_ && num_reports_ >= expected_num_reports_) + std::move(quit_closure_).Run(); + } + + void ShouldRunReportSentCallbacks(bool should_run_report_sent_callbacks) { + should_run_report_sent_callbacks_ = should_run_report_sent_callbacks; + } + + size_t num_reports() { return num_reports_; } + + int64_t last_conversion_id() { return last_conversion_id_; } + + base::Time last_report_time() { return last_report_time_; } + + void WaitForNumReports(size_t expected_num_reports) { + if (num_reports_ >= expected_num_reports) + return; + + expected_num_reports_ = expected_num_reports; + base::RunLoop wait_loop; + quit_closure_ = wait_loop.QuitClosure(); + wait_loop.Run(); + } + + private: + bool should_run_report_sent_callbacks_ = false; + size_t expected_num_reports_ = 0u; + size_t num_reports_ = 0u; + int64_t last_conversion_id_ = 0UL; + base::Time last_report_time_; + base::OnceClosure quit_closure_; +}; + +// Time after impression that a conversion can first be sent. See +// ConversionStorageDelegateImpl::GetReportTimeForConversion(). +constexpr base::TimeDelta kFirstReportingWindow = base::TimeDelta::FromDays(2); + +// Give impressions a sufficiently long expiry. +constexpr base::TimeDelta kImpressionExpiry = base::TimeDelta::FromDays(30); + +} // namespace + +class ConversionManagerImplTest : public testing::Test { + public: + ConversionManagerImplTest() + : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) { + EXPECT_TRUE(dir_.CreateUniqueTempDir()); + CreateManager(); + } + + void CreateManager() { + auto reporter = std::make_unique<TestConversionReporter>(); + test_reporter_ = reporter.get(); + conversion_manager_ = ConversionManagerImpl::CreateForTesting( + std::move(reporter), std::make_unique<ConstantStartupDelayPolicy>(), + task_environment_.GetMockClock(), dir_.GetPath(), + base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})); + } + + const base::Clock& clock() { return *task_environment_.GetMockClock(); } + + protected: + base::ScopedTempDir dir_; + BrowserTaskEnvironment task_environment_; + std::unique_ptr<ConversionManagerImpl> conversion_manager_; + TestConversionReporter* test_reporter_ = nullptr; +}; + +TEST_F(ConversionManagerImplTest, ImpressionRegistered_ReturnedToWebUI) { + auto impression = ImpressionBuilder(clock().Now()) + .SetExpiry(kImpressionExpiry) + .SetData("100") + .Build(); + conversion_manager_->HandleImpression(impression); + + base::RunLoop run_loop; + auto get_impressions_callback = base::BindLambdaForTesting( + [&](std::vector<StorableImpression> impressions) { + EXPECT_EQ(1u, impressions.size()); + EXPECT_TRUE(ImpressionsEqual(impression, impressions.back())); + run_loop.Quit(); + }); + conversion_manager_->GetActiveImpressionsForWebUI( + std::move(get_impressions_callback)); + run_loop.Run(); +} + +TEST_F(ConversionManagerImplTest, ExpiredImpression_NotReturnedToWebUI) { + conversion_manager_->HandleImpression(ImpressionBuilder(clock().Now()) + .SetExpiry(kImpressionExpiry) + .SetData("100") + .Build()); + task_environment_.FastForwardBy(2 * kImpressionExpiry); + + base::RunLoop run_loop; + auto get_impressions_callback = base::BindLambdaForTesting( + [&](std::vector<StorableImpression> impressions) { + EXPECT_TRUE(impressions.empty()); + run_loop.Quit(); + }); + conversion_manager_->GetActiveImpressionsForWebUI( + std::move(get_impressions_callback)); + run_loop.Run(); +} + +TEST_F(ConversionManagerImplTest, ImpressionConverted_ReportReturnedToWebUI) { + auto impression = ImpressionBuilder(clock().Now()) + .SetExpiry(kImpressionExpiry) + .SetData("100") + .Build(); + conversion_manager_->HandleImpression(impression); + + auto conversion = DefaultConversion(); + conversion_manager_->HandleConversion(conversion); + + ConversionReport expected_report(impression, conversion.conversion_data(), + clock().Now() + kFirstReportingWindow, + base::nullopt /* conversion_id */); + expected_report.attribution_credit = 100; + + base::RunLoop run_loop; + auto reports_callback = + base::BindLambdaForTesting([&](std::vector<ConversionReport> reports) { + EXPECT_EQ(1u, reports.size()); + EXPECT_TRUE(ReportsEqual({expected_report}, reports)); + run_loop.Quit(); + }); + conversion_manager_->GetReportsForWebUI(std::move(reports_callback), + base::Time::Max()); + run_loop.Run(); +} + +TEST_F(ConversionManagerImplTest, ImpressionConverted_ReportQueued) { + conversion_manager_->HandleImpression( + ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build()); + conversion_manager_->HandleConversion(DefaultConversion()); + + // Reports are queued in intervals ahead of when they should be + // sent. Make sure the report is not queued earlier than this. + task_environment_.FastForwardBy(kFirstReportingWindow - + kConversionManagerQueueReportsInterval - + base::TimeDelta::FromMinutes(1)); + EXPECT_EQ(0u, test_reporter_->num_reports()); + + task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(1)); + EXPECT_EQ(1u, test_reporter_->num_reports()); +} + +TEST_F(ConversionManagerImplTest, QueuedReportNotSent_QueuedAgain) { + conversion_manager_->HandleImpression( + ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build()); + conversion_manager_->HandleConversion(DefaultConversion()); + task_environment_.FastForwardBy(kFirstReportingWindow - + kConversionManagerQueueReportsInterval); + EXPECT_EQ(1u, test_reporter_->num_reports()); + + // If the report is not sent, it should be added to the queue again. + task_environment_.FastForwardBy(kConversionManagerQueueReportsInterval); + EXPECT_EQ(2u, test_reporter_->num_reports()); +} + +TEST_F(ConversionManagerImplTest, QueuedReportSent_NotQueuedAgain) { + test_reporter_->ShouldRunReportSentCallbacks(true); + conversion_manager_->HandleImpression( + ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build()); + conversion_manager_->HandleConversion(DefaultConversion()); + task_environment_.FastForwardBy(kFirstReportingWindow - + kConversionManagerQueueReportsInterval); + EXPECT_EQ(1u, test_reporter_->num_reports()); + + // The report should not be added to the queue again. + task_environment_.FastForwardBy(kConversionManagerQueueReportsInterval); + EXPECT_EQ(1u, test_reporter_->num_reports()); +} + +// Add a conversion to storage and reset the manager to mimic a report being +// available at startup. +TEST_F(ConversionManagerImplTest, ExpiredReportsAtStartup_Queued) { + // Create a report that will be reported at t= 2 days. + conversion_manager_->HandleImpression( + ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build()); + conversion_manager_->HandleConversion(DefaultConversion()); + + // Create another conversion that will be reported at t= + // (kFirstReportingWindow + 2 * kConversionManagerQueueReportsInterval). + task_environment_.FastForwardBy(2 * kConversionManagerQueueReportsInterval); + conversion_manager_->HandleImpression( + ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build()); + conversion_manager_->HandleConversion(DefaultConversion()); + + EXPECT_EQ(0u, test_reporter_->num_reports()); + + // Reset the manager to simulate shutdown. + conversion_manager_.reset(); + + // Fast forward past the expected report time of the first conversion, t = + // (kFirstReportingWindow+ 1 minute). + task_environment_.FastForwardBy(kFirstReportingWindow - + (2 * kConversionManagerQueueReportsInterval) + + base::TimeDelta::FromMinutes(1)); + + // Create the manager and check that the first report is queued immediately. + CreateManager(); + test_reporter_->ShouldRunReportSentCallbacks(true); + test_reporter_->WaitForNumReports(1); + EXPECT_EQ(1u, test_reporter_->num_reports()); + + // The second report is still queued at the correct time. + task_environment_.FastForwardBy(kConversionManagerQueueReportsInterval); + EXPECT_EQ(2u, test_reporter_->num_reports()); +} + +// This functionality is tested more thoroughly in the ConversionStorageSql +// unit tests. Here, just test to make sure the basic control flow is working. +TEST_F(ConversionManagerImplTest, ClearData) { + for (bool match_url : {true, false}) { + base::Time start = clock().Now(); + conversion_manager_->HandleImpression( + ImpressionBuilder(start).SetExpiry(kImpressionExpiry).Build()); + conversion_manager_->HandleConversion(DefaultConversion()); + + base::RunLoop run_loop; + conversion_manager_->ClearData( + start, start + base::TimeDelta::FromMinutes(1), + base::BindLambdaForTesting( + [match_url](const url::Origin& _) { return match_url; }), + run_loop.QuitClosure()); + run_loop.Run(); + + task_environment_.FastForwardBy(kFirstReportingWindow - + kConversionManagerQueueReportsInterval); + size_t expected_reports = match_url ? 0u : 1u; + EXPECT_EQ(expected_reports, test_reporter_->num_reports()); + } +} + +TEST_F(ConversionManagerImplTest, ConversionsSentFromUI_ReportedImmediately) { + conversion_manager_->HandleImpression( + ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build()); + conversion_manager_->HandleImpression( + ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build()); + conversion_manager_->HandleConversion(DefaultConversion()); + EXPECT_EQ(0u, test_reporter_->num_reports()); + + conversion_manager_->SendReportsForWebUI(base::DoNothing()); + task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(0)); + EXPECT_EQ(2u, test_reporter_->num_reports()); +} + +TEST_F(ConversionManagerImplTest, ExpiredReportsAtStartup_Delayed) { + // Create a report that will be reported at t= 2 days. + base::Time start_time = clock().Now(); + conversion_manager_->HandleImpression( + ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build()); + conversion_manager_->HandleConversion(DefaultConversion()); + EXPECT_EQ(0u, test_reporter_->num_reports()); + + // Reset the manager to simulate shutdown. + conversion_manager_.reset(); + + // Fast forward past the expected report time of the first conversion, t = + // (kFirstReportingWindow+ 1 minute). + task_environment_.FastForwardBy(kFirstReportingWindow + + base::TimeDelta::FromMinutes(1)); + + CreateManager(); + test_reporter_->WaitForNumReports(1); + + // Ensure that the expired report is delayed based on the time the browser + // started. + EXPECT_EQ(start_time + kFirstReportingWindow + + base::TimeDelta::FromMinutes(1) + kExpiredReportOffset, + test_reporter_->last_report_time()); +} + +TEST_F(ConversionManagerImplTest, NonExpiredReportsQueuedAtStartup_NotDelayed) { + // Create a report that will be reported at t= 2 days. + base::Time start_time = clock().Now(); + conversion_manager_->HandleImpression( + ImpressionBuilder(clock().Now()).SetExpiry(kImpressionExpiry).Build()); + conversion_manager_->HandleConversion(DefaultConversion()); + EXPECT_EQ(0u, test_reporter_->num_reports()); + + // Reset the manager to simulate shutdown. + conversion_manager_.reset(); + + // Fast forward just before the expected report time. + task_environment_.FastForwardBy(kFirstReportingWindow - + base::TimeDelta::FromMinutes(1)); + + // Ensure that this report does not receive additional delay. + CreateManager(); + test_reporter_->WaitForNumReports(1); + EXPECT_EQ(1u, test_reporter_->num_reports()); + EXPECT_EQ(start_time + kFirstReportingWindow, + test_reporter_->last_report_time()); +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_network_sender_impl.cc b/chromium/content/browser/conversions/conversion_network_sender_impl.cc new file mode 100644 index 00000000000..d2259fbf15b --- /dev/null +++ b/chromium/content/browser/conversions/conversion_network_sender_impl.cc @@ -0,0 +1,177 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_network_sender_impl.h" + +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/check.h" +#include "base/metrics/histogram_functions.h" +#include "base/strings/strcat.h" +#include "base/strings/string_number_conversions.h" +#include "base/time/time.h" +#include "content/public/browser/storage_partition.h" +#include "net/base/load_flags.h" +#include "net/base/net_errors.h" +#include "net/http/http_request_headers.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_status_code.h" +#include "net/traffic_annotation/network_traffic_annotation.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/simple_url_loader.h" +#include "url/gurl.h" +#include "url/origin.h" +#include "url/url_canon.h" + +namespace content { + +namespace { + +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class Status { + kOk = 0, + // Corresponds to a non-zero NET_ERROR. + kInternalError = 1, + // Corresponds to a non-200 HTTP response code from the reporting endpoint. + kExternalError = 2, + kMaxValue = kExternalError +}; + +// Called when a network request is started for |report|, for logging metrics. +void LogMetricsOnReportSend(ConversionReport* report) { + DCHECK(report); + + // Reports sent from the WebUI should not log metrics. + if (report->report_time == base::Time::Min()) + return; + + // Use a large time range to capture users that might not open the browser for + // a long time while a conversion report is pending. Revisit this range if it + // is non-ideal for real world data. + // Add |extra_delay| to the reported time which will include the amount of + // time since the report was originally scheduled, for reports at startup + // whose |report_time| changes due to additional startup delay. + base::Time now = base::Time::Now(); + base::TimeDelta delay = (now - report->report_time) + report->extra_delay; + base::UmaHistogramCustomTimes("Conversions.ExtraReportDelay", delay, + base::TimeDelta::FromSeconds(1), + base::TimeDelta::FromDays(7), /*buckets=*/100); + + // TODO(csharrison): We should thread the conversion time alongside the + // ConversionReport to log the effective time since conversion. +} + +GURL GetReportUrl(const content::ConversionReport& report) { + url::Replacements<char> replacements; + const char kEndpointPath[] = "/.well-known/register-conversion"; + replacements.SetPath(kEndpointPath, url::Component(0, strlen(kEndpointPath))); + std::string query = base::StrCat( + {"impression-data=", report.impression.impression_data(), + "&conversion-data=", report.conversion_data, + "&credit=", base::NumberToString(report.attribution_credit)}); + replacements.SetQuery(query.c_str(), url::Component(0, query.length())); + return report.impression.reporting_origin().GetURL().ReplaceComponents( + replacements); +} + +} // namespace + +ConversionNetworkSenderImpl::ConversionNetworkSenderImpl( + StoragePartition* storage_partition) + : storage_partition_(storage_partition) {} + +ConversionNetworkSenderImpl::~ConversionNetworkSenderImpl() = default; + +void ConversionNetworkSenderImpl::SendReport(ConversionReport* report, + ReportSentCallback sent_callback) { + // The browser process URLLoaderFactory is not created by default, so don't + // create it until it is directly needed. + if (!url_loader_factory_) { + url_loader_factory_ = + storage_partition_->GetURLLoaderFactoryForBrowserProcess(); + } + + auto resource_request = std::make_unique<network::ResourceRequest>(); + resource_request->url = GetReportUrl(*report); + resource_request->referrer = report->impression.conversion_origin().GetURL(); + resource_request->method = net::HttpRequestHeaders::kPostMethod; + resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit; + resource_request->load_flags = + net::LOAD_DISABLE_CACHE | net::LOAD_BYPASS_CACHE; + + // TODO(https://crbug.com/1058018): Update the "policy" field in the traffic + // annotation when a setting to disable the API is properly + // surfaced/implemented. + net::NetworkTrafficAnnotationTag traffic_annotation = + net::DefineNetworkTrafficAnnotation("conversion_measurement_report", R"( + semantics { + sender: "Event-level Conversion Measurement API" + description: + "The Conversion Measurement API allows sites to measure " + "conversions (e.g. purchases) and attribute them to clicked ads, " + "without using cross-site persistent identifiers like third party " + "cookies." + trigger: + "When a registered conversion has become eligible for reporting." + data: + "A high-entropy identifier declared by the site in which the user " + "clicked on an impression. A noisy low entropy data value declared " + "on the conversion site. A browser generated value that denotes " + "if this was the last impression clicked prior to conversion." + destination:OTHER + } + policy { + cookies_allowed: NO + setting: + "This feature cannot be disabled by settings." + policy_exception_justification: "Not implemented." + })"); + + auto simple_url_loader = network::SimpleURLLoader::Create( + std::move(resource_request), traffic_annotation); + network::SimpleURLLoader* simple_url_loader_ptr = simple_url_loader.get(); + + auto it = loaders_in_progress_.insert(loaders_in_progress_.begin(), + std::move(simple_url_loader)); + simple_url_loader_ptr->SetTimeoutDuration(base::TimeDelta::FromSeconds(30)); + + // Unretained is safe because the URLLoader is owned by |this| and will be + // deleted before |this|. + simple_url_loader_ptr->DownloadHeadersOnly( + url_loader_factory_.get(), + base::BindOnce(&ConversionNetworkSenderImpl::OnReportSent, + base::Unretained(this), std::move(it), + std::move(sent_callback))); + LogMetricsOnReportSend(report); +} + +void ConversionNetworkSenderImpl::SetURLLoaderFactoryForTesting( + scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) { + url_loader_factory_ = url_loader_factory; +} + +void ConversionNetworkSenderImpl::OnReportSent( + UrlLoaderList::iterator it, + ReportSentCallback sent_callback, + scoped_refptr<net::HttpResponseHeaders> headers) { + network::SimpleURLLoader* loader = it->get(); + + // Consider a non-200 HTTP code as a non-internal error. + bool internal_ok = loader->NetError() == net::OK || + loader->NetError() == net::ERR_HTTP_RESPONSE_CODE_FAILURE; + bool external_ok = headers && headers->response_code() == net::HTTP_OK; + Status status = + internal_ok && external_ok + ? Status::kOk + : !internal_ok ? Status::kInternalError : Status::kExternalError; + base::UmaHistogramEnumeration("Conversions.ReportStatus", status); + + loaders_in_progress_.erase(it); + std::move(sent_callback).Run(); +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_network_sender_impl.h b/chromium/content/browser/conversions/conversion_network_sender_impl.h new file mode 100644 index 00000000000..0dd23717171 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_network_sender_impl.h @@ -0,0 +1,71 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_NETWORK_SENDER_IMPL_H_ +#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_NETWORK_SENDER_IMPL_H_ + +#include <stdint.h> +#include <list> +#include <memory> + +#include "base/callback.h" +#include "content/browser/conversions/conversion_report.h" +#include "content/browser/conversions/conversion_reporter_impl.h" +#include "content/common/content_export.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" + +namespace network { +class SimpleURLLoader; +} // namespace network + +namespace content { + +class StoragePartition; + +// Implemented a NetworkSender capable of issuing POST requests for complete +// conversions. Maintains a set of all ongoing UrlLoaders used for posting +// conversion reports. Created and owned by ConversionReporterImpl. +class CONTENT_EXPORT ConversionNetworkSenderImpl + : public ConversionReporterImpl::NetworkSender { + public: + explicit ConversionNetworkSenderImpl(StoragePartition* storage_partition); + ConversionNetworkSenderImpl(const ConversionNetworkSenderImpl&) = delete; + ConversionNetworkSenderImpl& operator=(const ConversionNetworkSenderImpl&) = + delete; + ~ConversionNetworkSenderImpl() override; + + // Generates a resource request for |report| and creates a new UrlLoader to + // send it. A report is only attempted to be sent once, with a timeout of 30 + // seconds. |report| is destroyed after this call finishes. + // |sent_callback| is run after the request finishes, whether or not it + // succeeded, + void SendReport(ConversionReport* report, + ReportSentCallback sent_callback) override; + + // Tests inject a TestURLLoaderFactory so they can mock the network response. + void SetURLLoaderFactoryForTesting( + scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory); + + private: + // This is a std::list so that iterators remain valid during modifications. + using UrlLoaderList = std::list<std::unique_ptr<network::SimpleURLLoader>>; + + // Called when headers are available for a sent report. + void OnReportSent(UrlLoaderList::iterator it, + ReportSentCallback sent_callback, + scoped_refptr<net::HttpResponseHeaders> headers); + + // Reports that are actively being sent. + UrlLoaderList loaders_in_progress_; + + // Must outlive |this|. + StoragePartition* storage_partition_; + + // Lazily accessed URLLoaderFactory used for network requests. + scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_NETWORK_SENDER_IMPL_H_ diff --git a/chromium/content/browser/conversions/conversion_network_sender_impl_unittest.cc b/chromium/content/browser/conversions/conversion_network_sender_impl_unittest.cc new file mode 100644 index 00000000000..87e0bfc87c1 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_network_sender_impl_unittest.cc @@ -0,0 +1,252 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_network_sender_impl.h" + +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/strings/strcat.h" +#include "base/strings/string_number_conversions.h" +#include "base/task/post_task.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "base/test/simple_test_clock.h" +#include "content/browser/conversions/conversion_test_utils.h" +#include "content/browser/storage_partition_impl.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/common/content_features.h" +#include "content/public/test/browser_task_environment.h" +#include "content/public/test/test_browser_context.h" +#include "net/base/load_flags.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" +#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +namespace { + +// Gets a report url which matches reports created by GetReport(). +std::string GetReportUrl(std::string impression_data) { + return base::StrCat( + {"https://report.test/.well-known/register-conversion?impression-data=", + impression_data, "&conversion-data=", impression_data, "&credit=0"}); +} + +// Create a simple report where impression data/conversion data/conversion id +// are all the same. +ConversionReport GetReport(int64_t conversion_id) { + return ConversionReport( + ImpressionBuilder(base::Time()) + .SetData(base::NumberToString(conversion_id)) + .Build(), + /*conversion_data=*/base::NumberToString(conversion_id), + /*report_time=*/base::Time(), + /*conversion_id=*/conversion_id); +} + +} // namespace + +class ConversionNetworkSenderTest : public testing::Test { + public: + ConversionNetworkSenderTest() + : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME), + network_sender_(std::make_unique<ConversionNetworkSenderImpl>( + /*sotrage_partition=*/nullptr)), + shared_url_loader_factory_( + base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( + &test_url_loader_factory_)) { + network_sender_->SetURLLoaderFactoryForTesting(shared_url_loader_factory_); + } + + ConversionReporterImpl::NetworkSender::ReportSentCallback GetSentCallback() { + return base::BindOnce(&ConversionNetworkSenderTest::OnReportSent, + base::Unretained(this)); + } + + protected: + size_t num_reports_sent_ = 0u; + + // |task_enviorment_| must be initialized first. + content::BrowserTaskEnvironment task_environment_; + + // Unique ptr so it can be reset during testing. + std::unique_ptr<ConversionNetworkSenderImpl> network_sender_; + network::TestURLLoaderFactory test_url_loader_factory_; + + private: + void OnReportSent() { num_reports_sent_++; } + + scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_; +}; + +TEST_F(ConversionNetworkSenderTest, + ConversionReportReceived_NetworkRequestMade) { + auto report = GetReport(/*conversion_id=*/1); + network_sender_->SendReport(&report, std::move(base::DoNothing())); + EXPECT_EQ(1, test_url_loader_factory_.NumPending()); + EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( + GetReportUrl("1"), "")); +} + +TEST_F(ConversionNetworkSenderTest, ReportSent_CallbackFired) { + auto report = GetReport(/*conversion_id=*/1); + network_sender_->SendReport(&report, GetSentCallback()); + EXPECT_EQ(1, test_url_loader_factory_.NumPending()); + EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( + GetReportUrl("1"), "")); + EXPECT_EQ(1u, num_reports_sent_); +} + +TEST_F(ConversionNetworkSenderTest, SenderDeletedDuringRequest_NoCrash) { + auto report = GetReport(/*conversion_id=*/1); + network_sender_->SendReport(&report, GetSentCallback()); + EXPECT_EQ(1, test_url_loader_factory_.NumPending()); + network_sender_.reset(); + EXPECT_FALSE(test_url_loader_factory_.SimulateResponseForPendingRequest( + GetReportUrl("1"), "")); + EXPECT_EQ(0u, num_reports_sent_); +} + +TEST_F(ConversionNetworkSenderTest, ReportRequestHangs_TimesOut) { + auto report = GetReport(/*conversion_id=*/1); + network_sender_->SendReport(&report, GetSentCallback()); + EXPECT_EQ(1, test_url_loader_factory_.NumPending()); + + // The request should time out after 30 seconds. + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(30)); + + EXPECT_EQ(0, test_url_loader_factory_.NumPending()); + + // Also verify that the sent callback runs if the request times out. + EXPECT_EQ(1u, num_reports_sent_); +} + +TEST_F(ConversionNetworkSenderTest, ReportSent_QueryParamsSetCorrectly) { + auto impression = + ImpressionBuilder(base::Time()) + .SetData("impression") + .SetReportingOrigin(url::Origin::Create(GURL("https://a.com"))) + .Build(); + ConversionReport report(impression, + /*conversion_data=*/"conversion", + /*report_time=*/base::Time(), + /*conversion_id=*/1); + report.attribution_credit = 50; + network_sender_->SendReport(&report, base::DoNothing()); + + std::string expected_report_url( + "https://a.com/.well-known/" + "register-conversion?impression-data=impression&conversion-data=" + "conversion&credit=50"); + EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( + expected_report_url, "")); +} + +TEST_F(ConversionNetworkSenderTest, ReportSent_RequestAttributesSet) { + auto impression = + ImpressionBuilder(base::Time()) + .SetData("1") + .SetReportingOrigin(url::Origin::Create(GURL("https://a.com"))) + .SetConversionOrigin(url::Origin::Create(GURL("https://b.com"))) + .Build(); + ConversionReport report(impression, + /*conversion_data=*/"1", + /*report_time=*/base::Time(), + /*conversion_id=*/1); + network_sender_->SendReport(&report, base::DoNothing()); + + const network::ResourceRequest* pending_request; + std::string expected_report_url( + "https://a.com/.well-known/" + "register-conversion?impression-data=1&conversion-data=1&credit=0"); + EXPECT_TRUE(test_url_loader_factory_.IsPending(expected_report_url, + &pending_request)); + + // Ensure that the request is sent with no credentials. + EXPECT_EQ(network::mojom::CredentialsMode::kOmit, + pending_request->credentials_mode); + EXPECT_EQ("POST", pending_request->method); + EXPECT_EQ(GURL("https://b.com"), pending_request->referrer); +} + +TEST_F(ConversionNetworkSenderTest, ReportResultsInHttpError_SentCallbackRuns) { + auto report = GetReport(/*conversion_id=*/1); + network_sender_->SendReport(&report, GetSentCallback()); + EXPECT_EQ(0u, num_reports_sent_); + + // We should run the sent callback even if there is an http error. + EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( + GetReportUrl("1"), "", net::HttpStatusCode::HTTP_BAD_REQUEST)); + EXPECT_EQ(1u, num_reports_sent_); +} + +TEST_F(ConversionNetworkSenderTest, ManyReports_AllSentSuccessfully) { + for (int i = 0; i < 10; i++) { + auto report = GetReport(/*conversion_id=*/i); + network_sender_->SendReport(&report, GetSentCallback()); + } + EXPECT_EQ(10, test_url_loader_factory_.NumPending()); + + // Send reports out of order to guarantee that callback conversion_ids are + // properly handled. + for (int i = 9; i >= 0; i--) { + std::string report_id = base::NumberToString(i); + + EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( + GetReportUrl(report_id), "")); + } + EXPECT_EQ(10u, num_reports_sent_); + EXPECT_EQ(0, test_url_loader_factory_.NumPending()); +} + +TEST_F(ConversionNetworkSenderTest, LoadFlags) { + auto report = GetReport(/*conversion_id=*/1); + network_sender_->SendReport(&report, GetSentCallback()); + int load_flags = + test_url_loader_factory_.GetPendingRequest(0)->request.load_flags; + EXPECT_TRUE(load_flags & net::LOAD_BYPASS_CACHE); + EXPECT_TRUE(load_flags & net::LOAD_DISABLE_CACHE); +} + +TEST_F(ConversionNetworkSenderTest, ErrorHistogram) { + // All OK. + { + base::HistogramTester histograms; + auto report = GetReport(/*conversion_id=*/1); + network_sender_->SendReport(&report, GetSentCallback()); + EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( + GetReportUrl("1"), "")); + // kOk = 0. + histograms.ExpectUniqueSample("Conversions.ReportStatus", 0, 1); + } + // Internal error. + { + base::HistogramTester histograms; + auto report = GetReport(/*conversion_id=*/2); + network_sender_->SendReport(&report, GetSentCallback()); + network::URLLoaderCompletionStatus completion_status(net::ERR_FAILED); + EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( + GURL(GetReportUrl("2")), completion_status, + network::mojom::URLResponseHead::New(), std::string())); + // kInternalError = 1. + histograms.ExpectUniqueSample("Conversions.ReportStatus", 1, 1); + } + { + base::HistogramTester histograms; + auto report = GetReport(/*conversion_id=*/3); + network_sender_->SendReport(&report, GetSentCallback()); + EXPECT_TRUE(test_url_loader_factory_.SimulateResponseForPendingRequest( + GetReportUrl("3"), std::string(), net::HTTP_UNAUTHORIZED)); + // kExternalError = 2. + histograms.ExpectUniqueSample("Conversions.ReportStatus", 2, 1); + } +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_page_metrics.cc b/chromium/content/browser/conversions/conversion_page_metrics.cc new file mode 100644 index 00000000000..616ec324d67 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_page_metrics.cc @@ -0,0 +1,24 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_page_metrics.h" + +#include "base/metrics/histogram_functions.h" + +namespace content { + +ConversionPageMetrics::ConversionPageMetrics() = default; + +ConversionPageMetrics::~ConversionPageMetrics() { + // TODO(https://crbug.com/1044099): Consider limiting registrations per page + // based on this metric. + base::UmaHistogramExactLinear("Conversions.RegisteredConversionsPerPage", + num_conversions_on_current_page_, 100); +} + +void ConversionPageMetrics::OnConversion(const StorableConversion& conversion) { + num_conversions_on_current_page_++; +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_page_metrics.h b/chromium/content/browser/conversions/conversion_page_metrics.h new file mode 100644 index 00000000000..bd974b1098e --- /dev/null +++ b/chromium/content/browser/conversions/conversion_page_metrics.h @@ -0,0 +1,33 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_PAGE_METRICS_H_ +#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_PAGE_METRICS_H_ + +namespace content { + +class StorableConversion; + +// Keeps track of per-page-load metrics for conversion measurement. Lifetime is +// scoped to a single page load. +class ConversionPageMetrics { + public: + ConversionPageMetrics(); + ~ConversionPageMetrics(); + + ConversionPageMetrics(const ConversionPageMetrics& other) = delete; + ConversionPageMetrics& operator=(const ConversionPageMetrics& other) = delete; + + // Called when a conversion is registered. + void OnConversion(const StorableConversion& conversion); + + private: + // Keeps track of how many conversion registrations there have been on the + // current page. + int num_conversions_on_current_page_ = 0; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_PAGE_METRICS_H_ diff --git a/chromium/content/browser/conversions/conversion_policy.cc b/chromium/content/browser/conversions/conversion_policy.cc index d323ccfe3b7..4df9ed74183 100644 --- a/chromium/content/browser/conversions/conversion_policy.cc +++ b/chromium/content/browser/conversions/conversion_policy.cc @@ -5,7 +5,6 @@ #include "content/browser/conversions/conversion_policy.h" #include "base/format_macros.h" -#include "base/logging.h" #include "base/memory/ptr_util.h" #include "base/rand_util.h" #include "base/strings/stringprintf.h" @@ -41,97 +40,24 @@ std::unique_ptr<ConversionPolicy> ConversionPolicy::CreateForTesting( new ConversionPolicy(std::move(noise_provider))); } -ConversionPolicy::ConversionPolicy() - : noise_provider_(std::make_unique<NoiseProvider>()) {} +ConversionPolicy::ConversionPolicy(bool debug_mode) + : debug_mode_(debug_mode), + noise_provider_(debug_mode ? nullptr + : std::make_unique<NoiseProvider>()) {} ConversionPolicy::ConversionPolicy( std::unique_ptr<ConversionPolicy::NoiseProvider> noise_provider) - : noise_provider_(std::move(noise_provider)) {} + : debug_mode_(false), noise_provider_(std::move(noise_provider)) {} ConversionPolicy::~ConversionPolicy() = default; -base::Time ConversionPolicy::GetReportTimeForConversion( - const ConversionReport& report) const { - // After the initial impression, a schedule of reporting windows and deadlines - // associated with that impression begins. The time between impression time - // and impression expiry is split into multiple reporting windows. At the end - // of each window, the browser will send all scheduled reports for that - // impression. - // - // Each reporting window has a deadline and only conversions registered before - // that deadline are sent in that window. Each deadline is one hour prior to - // the window report time. The deadlines relative to impression time are <2 - // days minus 1 hour, 7 days minus 1 hour, impression expiry>. The impression - // expiry window is only used for conversions that occur after the 7 day - // deadline. For example, a conversion which happens one hour after an - // impression with an expiry of two hours, is still reported in the 2 day - // window. - constexpr base::TimeDelta kWindowDeadlineOffset = - base::TimeDelta::FromHours(1); - base::TimeDelta expiry_deadline = - report.impression.expiry_time() - report.impression.impression_time(); - const base::TimeDelta kReportingWindowDeadlines[] = { - base::TimeDelta::FromDays(2) - kWindowDeadlineOffset, - base::TimeDelta::FromDays(7) - kWindowDeadlineOffset, expiry_deadline}; - - base::TimeDelta deadline_to_use; - - // Given a conversion report that was created at |report.report_time|, find - // the first applicable reporting window this conversion should be reported - // at. - for (base::TimeDelta report_window_deadline : kReportingWindowDeadlines) { - // If this window is valid for the conversion, use it. |report.report_time| - // is roughly ~now, as the conversion time is used as the default value for - // newly created reports that have not had a report time set. - if (report.impression.impression_time() + report_window_deadline >= - report.report_time) { - deadline_to_use = report_window_deadline; - break; - } - } - - // Valid conversion reports should always have a valid reporting deadline. - DCHECK(!deadline_to_use.is_zero()); - - // If the expiry deadline falls after the first window, but before another - // window, use it instead. For example, if expiry is at 3 days, we can send - // reports at the 2 day deadline and the expiry deadline instead of at the 7 - // day deadline. - if (expiry_deadline > kReportingWindowDeadlines[0] && - expiry_deadline < deadline_to_use) { - deadline_to_use = expiry_deadline; - } - - return report.impression.impression_time() + deadline_to_use + - kWindowDeadlineOffset; -} - -int ConversionPolicy::GetMaxConversionsPerImpression() const { - return 3; -} - -void ConversionPolicy::AssignAttributionCredits( - std::vector<ConversionReport>* reports) const { - DCHECK(!reports->empty()); - ConversionReport* last_report = &(reports->at(0)); - - // Give the latest impression an attribution of 100 and all the rest 0. - for (auto& report : *reports) { - report.attribution_credit = 0; - if (report.impression.impression_time() > - last_report->impression.impression_time()) - last_report = &report; - } - - last_report->attribution_credit = 100; -} - std::string ConversionPolicy::GetSanitizedConversionData( uint64_t conversion_data) const { // Add noise to the conversion when the value is first sanitized from a // conversion registration event. This noised data will be used for all // associated impressions that convert. - conversion_data = noise_provider_->GetNoisedConversionData(conversion_data); + if (noise_provider_) + conversion_data = noise_provider_->GetNoisedConversionData(conversion_data); // Allow at most 3 bits of entropy in conversion data. base::StringPrintf() is // used over base::HexEncode() because HexEncode() returns a hex string with @@ -141,4 +67,38 @@ std::string ConversionPolicy::GetSanitizedConversionData( conversion_data % kMaxAllowedConversionValues); } +std::string ConversionPolicy::GetSanitizedImpressionData( + uint64_t impression_data) const { + // Impression data is allowed the full 64 bits. + return base::StringPrintf("%" PRIx64, impression_data); +} + +base::Time ConversionPolicy::GetExpiryTimeForImpression( + const base::Optional<base::TimeDelta>& declared_expiry, + base::Time impression_time) const { + static constexpr base::TimeDelta kDefaultImpressionExpiry = + base::TimeDelta::FromDays(30); + + // Default to the maximum expiry time. + base::TimeDelta expiry = declared_expiry.value_or(kDefaultImpressionExpiry); + + // If the impression specified its own expiry, clamp it to the maximum. + return impression_time + std::min(expiry, kDefaultImpressionExpiry); +} + +base::Time ConversionPolicy::GetReportTimeForExpiredReportAtStartup( + base::Time now) const { + // Do not use any delay in debug mode. + if (debug_mode_) + return now; + + // Add uniform random noise in the range of [0, 5 minutes] to the report time. + // TODO(https://crbug.com/1075600): This delay is very conservative. Consider + // increasing this delay once we can be sure reports are still sent at + // reasonable times, and not delayed for many browser sessions due to short + // session up-times. + return now + + base::TimeDelta::FromMilliseconds(base::RandInt(0, 5 * 60 * 1000)); +} + } // namespace content diff --git a/chromium/content/browser/conversions/conversion_policy.h b/chromium/content/browser/conversions/conversion_policy.h index 1a14eac950e..79f86a739ec 100644 --- a/chromium/content/browser/conversions/conversion_policy.h +++ b/chromium/content/browser/conversions/conversion_policy.h @@ -10,8 +10,8 @@ #include <string> #include <vector> +#include "base/optional.h" #include "base/time/time.h" -#include "content/browser/conversions/conversion_report.h" #include "content/common/content_export.h" namespace content { @@ -36,35 +36,41 @@ class CONTENT_EXPORT ConversionPolicy { static std::unique_ptr<ConversionPolicy> CreateForTesting( std::unique_ptr<NoiseProvider> noise_provider); - ConversionPolicy(); + // |debug_mode| indicates whether the API is currently running in a mode where + // it should not use noise. + explicit ConversionPolicy(bool debug_mode = false); ConversionPolicy(const ConversionPolicy& other) = delete; ConversionPolicy& operator=(const ConversionPolicy& other) = delete; virtual ~ConversionPolicy(); - // Get the time a conversion report should be sent, by batching reports into - // set reporting windows based on their impression time. This strictly delays - // the time a report will be sent. - virtual base::Time GetReportTimeForConversion( - const ConversionReport& report) const; - - // Maximum number of times the an impression is allowed to convert. - virtual int GetMaxConversionsPerImpression() const; - - // Given a set of conversion reports for a single conversion registrations, - // assigns attribution credits to each one which will be sent at report time. - // By default, this performs "last click" attribution which assigns the report - // for the most recent impression a credit of 100, and the rest a credit of 0. - virtual void AssignAttributionCredits( - std::vector<ConversionReport>* reports) const; - // Gets the sanitized conversion data for a conversion. This strips entropy // from the provided to data to at most 3 bits of information. virtual std::string GetSanitizedConversionData( uint64_t conversion_data) const; + // Gets the sanitized impression data for an impression. Returns the decoded + // number as a hexadecimal string. + virtual std::string GetSanitizedImpressionData( + uint64_t impression_data) const; + + // Returns the expiry time for an impression that is clamped to a maximum + // value of 30 days from |impression_time|. + virtual base::Time GetExpiryTimeForImpression( + const base::Optional<base::TimeDelta>& declared_expiry, + base::Time impression_time) const; + + // Delays reports that should have been sent while the browser was not open by + // given them a noisy report time to help disassociate them from other + // reports. + virtual base::Time GetReportTimeForExpiredReportAtStartup( + base::Time now) const; + private: // For testing only. - ConversionPolicy(std::unique_ptr<NoiseProvider> noise_provider); + explicit ConversionPolicy(std::unique_ptr<NoiseProvider> noise_provider); + + // Whether the API is running in debug mode. No noise or delay should be used. + const bool debug_mode_; std::unique_ptr<NoiseProvider> noise_provider_; }; diff --git a/chromium/content/browser/conversions/conversion_policy_unittest.cc b/chromium/content/browser/conversions/conversion_policy_unittest.cc index b2e5941da25..15ff3505fb7 100644 --- a/chromium/content/browser/conversions/conversion_policy_unittest.cc +++ b/chromium/content/browser/conversions/conversion_policy_unittest.cc @@ -16,17 +16,6 @@ namespace content { namespace { -constexpr base::TimeDelta kDefaultExpiry = base::TimeDelta::FromDays(30); - -ConversionReport GetReport(base::Time impression_time, - base::Time conversion_time, - base::TimeDelta expiry = kDefaultExpiry) { - return ConversionReport( - ImpressionBuilder(impression_time).SetExpiry(expiry).Build(), - /*conversion_data=*/"123", conversion_time, - /*conversion_id=*/base::nullopt); -} - // Fake ConversionNoiseProvider that return un-noised conversion data. class EmptyNoiseProvider : public ConversionPolicy::NoiseProvider { public: @@ -59,6 +48,15 @@ TEST_F(ConversionPolicyTest, HighEntropyConversionData_StrippedToLowerBits) { ->GetSanitizedConversionData(conversion_data)); } +TEST_F(ConversionPolicyTest, SanitizeHighEntropyImpressionData_Unchanged) { + uint64_t impression_data = 256LU; + + // The policy should not alter the impression data, and return the hexadecimal + // representation. + EXPECT_EQ("100", + ConversionPolicy().GetSanitizedImpressionData(impression_data)); +} + TEST_F(ConversionPolicyTest, ThreeBitConversionData_Unchanged) { std::unique_ptr<ConversionPolicy> policy = ConversionPolicy::CreateForTesting( std::make_unique<EmptyNoiseProvider>()); @@ -75,100 +73,37 @@ TEST_F(ConversionPolicyTest, SantizizeConversionData_OutputHasNoise) { ->GetSanitizedConversionData(4UL)); } -TEST_F(ConversionPolicyTest, ImmediateConversion_FirstWindowUsed) { - base::Time impression_time = base::Time::Now(); - auto report = GetReport(impression_time, /*conversion_time=*/impression_time); - EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2), - ConversionPolicy().GetReportTimeForConversion(report)); -} - -TEST_F(ConversionPolicyTest, ConversionImmediatelyBeforeWindow_NextWindowUsed) { - base::Time impression_time = base::Time::Now(); - base::Time conversion_time = impression_time + base::TimeDelta::FromDays(2) - - base::TimeDelta::FromMinutes(1); - auto report = GetReport(impression_time, conversion_time); - EXPECT_EQ(impression_time + base::TimeDelta::FromDays(7), - ConversionPolicy().GetReportTimeForConversion(report)); -} - -TEST_F(ConversionPolicyTest, ConversionBeforeWindowDelay_WindowUsed) { - base::Time impression_time = base::Time::Now(); - - // The deadline for a window is 1 hour before the window. Use a time just - // before the deadline. - base::Time conversion_time = impression_time + base::TimeDelta::FromDays(2) - - base::TimeDelta::FromMinutes(61); - auto report = GetReport(impression_time, conversion_time); - EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2), - ConversionPolicy().GetReportTimeForConversion(report)); +// This test will fail flakily if noise is used. +TEST_F(ConversionPolicyTest, DebugMode_ConversionDataNotNoised) { + uint64_t conversion_data = 0UL; + for (int i = 0; i < 100; i++) { + EXPECT_EQ(base::NumberToString(conversion_data), + ConversionPolicy(true /* debug_mode */) + .GetSanitizedConversionData(conversion_data)); + } } -TEST_F(ConversionPolicyTest, - ImpressionExpiryBeforeTwoDayWindow_TwoDayWindowUsed) { +TEST_F(ConversionPolicyTest, NoExpiryForImpression_DefaultUsed) { base::Time impression_time = base::Time::Now(); - base::Time conversion_time = impression_time + base::TimeDelta::FromHours(1); - - // Set the impression to expire before the two day window. - auto report = GetReport(impression_time, conversion_time, - /*expiry=*/base::TimeDelta::FromHours(2)); - EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2), - ConversionPolicy().GetReportTimeForConversion(report)); + EXPECT_EQ(impression_time + base::TimeDelta::FromDays(30), + ConversionPolicy().GetExpiryTimeForImpression( + /*declared_expiry=*/base::nullopt, impression_time)); } -TEST_F(ConversionPolicyTest, - ImpressionExpiryBeforeSevenDayWindow_ExpiryWindowUsed) { +TEST_F(ConversionPolicyTest, LargeImpressionExpirySpecified_ClampedTo30Days) { + constexpr base::TimeDelta declared_expiry = base::TimeDelta::FromDays(60); base::Time impression_time = base::Time::Now(); - base::Time conversion_time = impression_time + base::TimeDelta::FromDays(3); - - // Set the impression to expire before the two day window. - auto report = GetReport(impression_time, conversion_time, - /*expiry=*/base::TimeDelta::FromDays(4)); - - // The expiry window is reported one hour after expiry time. - EXPECT_EQ(impression_time + base::TimeDelta::FromDays(4) + - base::TimeDelta::FromHours(1), - ConversionPolicy().GetReportTimeForConversion(report)); + EXPECT_EQ(impression_time + base::TimeDelta::FromDays(30), + ConversionPolicy().GetExpiryTimeForImpression(declared_expiry, + impression_time)); } -TEST_F(ConversionPolicyTest, - ImpressionExpiryAfterSevenDayWindow_ExpiryWindowUsed) { +TEST_F(ConversionPolicyTest, ImpressionExpirySpecified_ExpiryOverrideDefault) { + constexpr base::TimeDelta declared_expiry = base::TimeDelta::FromDays(10); base::Time impression_time = base::Time::Now(); - base::Time conversion_time = impression_time + base::TimeDelta::FromDays(7); - - // Set the impression to expire before the two day window. - auto report = GetReport(impression_time, conversion_time, - /*expiry=*/base::TimeDelta::FromDays(9)); - - // The expiry window is reported one hour after expiry time. - EXPECT_EQ(impression_time + base::TimeDelta::FromDays(9) + - base::TimeDelta::FromHours(1), - ConversionPolicy().GetReportTimeForConversion(report)); -} - -TEST_F(ConversionPolicyTest, - SingleReportForConversion_AttributionCreditAssigned) { - base::Time now = base::Time::Now(); - std::vector<ConversionReport> reports = { - GetReport(/*impression_time=*/now, /*conversion_time=*/now)}; - ConversionPolicy().AssignAttributionCredits(&reports); - EXPECT_EQ(1u, reports.size()); - EXPECT_EQ(100, reports[0].attribution_credit); -} - -TEST_F(ConversionPolicyTest, TwoReportsForConversion_LastReceivesCredit) { - base::Time now = base::Time::Now(); - std::vector<ConversionReport> reports = { - GetReport(/*impression_time=*/now, /*conversion_time=*/now), - GetReport(/*impression_time=*/now + base::TimeDelta::FromHours(100), - /*conversion_time=*/now)}; - ConversionPolicy().AssignAttributionCredits(&reports); - EXPECT_EQ(2u, reports.size()); - EXPECT_EQ(0, reports[0].attribution_credit); - EXPECT_EQ(100, reports[1].attribution_credit); - - // Ensure the reports were not rearranged. - EXPECT_EQ(now + base::TimeDelta::FromHours(100), - reports[1].impression.impression_time()); + EXPECT_EQ(impression_time + base::TimeDelta::FromDays(10), + ConversionPolicy().GetExpiryTimeForImpression(declared_expiry, + impression_time)); } } // namespace content diff --git a/chromium/content/browser/conversions/conversion_registration_browsertest.cc b/chromium/content/browser/conversions/conversion_registration_browsertest.cc index 55d40c22149..8b6fce9261d 100644 --- a/chromium/content/browser/conversions/conversion_registration_browsertest.cc +++ b/chromium/content/browser/conversions/conversion_registration_browsertest.cc @@ -10,6 +10,7 @@ #include "content/browser/conversions/conversion_host.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/public/common/content_features.h" +#include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" #include "content/public/test/content_browser_test.h" #include "content/public/test/content_browser_test_utils.h" @@ -135,6 +136,25 @@ IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest, } IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest, + FeaturePolicyDisabled_ConversionNotRegistered) { + EXPECT_TRUE(NavigateToURL( + shell(), embedded_test_server()->GetURL( + "/page_with_conversion_measurement_disabled.html"))); + std::unique_ptr<TestConversionHost> host = + TestConversionHost::ReplaceAndGetConversionHost(web_contents()); + + GURL redirect_url = embedded_test_server()->GetURL( + "/server-redirect?" + kWellKnownUrl + "?conversion-data=200"); + ResourceLoadObserver load_observer(shell()); + EXPECT_TRUE(ExecJs(web_contents(), + JsReplace("createTrackingPixel($1);", redirect_url))); + load_observer.WaitForResourceCompletion(redirect_url); + + EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank"))); + EXPECT_EQ(0u, host->num_conversions()); +} + +IN_PROC_BROWSER_TEST_F(ConversionRegistrationBrowserTest, ConversionRegistrationNotRedirect_NotReceived) { EXPECT_TRUE(NavigateToURL( shell(), diff --git a/chromium/content/browser/conversions/conversion_report.cc b/chromium/content/browser/conversions/conversion_report.cc index 8b285ad5791..6d8bbbaf2dc 100644 --- a/chromium/content/browser/conversions/conversion_report.cc +++ b/chromium/content/browser/conversions/conversion_report.cc @@ -28,6 +28,7 @@ std::ostream& operator<<(std::ostream& out, const ConversionReport& report) { << ", reporting_origin: " << report.impression.reporting_origin() << ", conversion_data: " << report.conversion_data << ", report_time: " << report.report_time + << ", extra_delay: " << report.extra_delay << ", attribution_credit: " << report.attribution_credit; return out; } diff --git a/chromium/content/browser/conversions/conversion_report.h b/chromium/content/browser/conversions/conversion_report.h index 64d2e62f0ef..38aa9b4a5a1 100644 --- a/chromium/content/browser/conversions/conversion_report.h +++ b/chromium/content/browser/conversions/conversion_report.h @@ -38,6 +38,10 @@ struct CONTENT_EXPORT ConversionReport { // The time this conversion report should be sent. base::Time report_time; + // Tracks ephemeral increases to |report_time| for this conversion report, for + // the purposes of logging metrics. + base::TimeDelta extra_delay; + // The attribution credit assigned to this conversion report. This is derived // from the set of all impressions that matched a singular conversion event. // This should be in the range 0-100. A set of ConversionReports for one diff --git a/chromium/content/browser/conversions/conversion_reporter_impl.cc b/chromium/content/browser/conversions/conversion_reporter_impl.cc new file mode 100644 index 00000000000..3011e2efc51 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_reporter_impl.cc @@ -0,0 +1,108 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_reporter_impl.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/rand_util.h" +#include "base/time/clock.h" +#include "content/browser/conversions/conversion_manager.h" +#include "content/browser/conversions/conversion_network_sender_impl.h" + +namespace content { + +ConversionReporterImpl::ConversionReporterImpl( + StoragePartition* storage_partition, + const base::Clock* clock) + : clock_(clock), + network_sender_( + std::make_unique<ConversionNetworkSenderImpl>(storage_partition)) {} + +ConversionReporterImpl::~ConversionReporterImpl() = default; + +void ConversionReporterImpl::AddReportsToQueue( + std::vector<ConversionReport> reports, + base::RepeatingCallback<void(int64_t)> report_sent_callback) { + DCHECK(!reports.empty()); + + std::vector<std::unique_ptr<ConversionReport>> swappable_reports; + for (ConversionReport& report : reports) { + swappable_reports.push_back( + std::make_unique<ConversionReport>(std::move(report))); + } + + // Shuffle new reports to provide plausible deniability on the ordering of + // reports that share the same |report_time|. This is important because + // multiple conversions for the same impression share the same report time if + // they are within the same reporting window, and we do not want to allow + // ordering on their conversion metadata bits. + base::RandomShuffle(swappable_reports.begin(), swappable_reports.end()); + + for (std::unique_ptr<ConversionReport>& report : swappable_reports) { + // If the given report is already being processed, ignore it. + bool inserted = conversion_report_callbacks_ + .emplace(*(report->conversion_id), report_sent_callback) + .second; + if (inserted) + report_queue_.push(std::move(report)); + } + MaybeScheduleNextReport(); +} + +void ConversionReporterImpl::SetNetworkSenderForTesting( + std::unique_ptr<NetworkSender> network_sender) { + network_sender_ = std::move(network_sender); +} + +void ConversionReporterImpl::SendNextReport() { + // Send the next report and remove it from the queue. Bind the conversion id + // to the sent callback so we know which conversion report has finished + // sending. + network_sender_->SendReport( + report_queue_.top().get(), + base::BindOnce(&ConversionReporterImpl::OnReportSent, + base::Unretained(this), + *report_queue_.top()->conversion_id)); + report_queue_.pop(); + MaybeScheduleNextReport(); +} + +void ConversionReporterImpl::MaybeScheduleNextReport() { + if (report_queue_.empty()) + return; + + send_report_timer_.Stop(); + base::Time current_time = clock_->Now(); + base::Time report_time = report_queue_.top()->report_time; + + // Start a timer to wait until the next report is ready to be sent. This + // purposefully yields the thread for every report that gets scheduled. + // Unretained is safe because the task should never actually be posted if the + // timer itself is destroyed + send_report_timer_.Start( + FROM_HERE, + (report_time < current_time) ? base::TimeDelta() + : report_time - current_time, + base::BindOnce(&ConversionReporterImpl::SendNextReport, + base::Unretained(this))); +} + +void ConversionReporterImpl::OnReportSent(int64_t conversion_id) { + auto it = conversion_report_callbacks_.find(conversion_id); + DCHECK(it != conversion_report_callbacks_.end()); + std::move(it->second).Run(conversion_id); + conversion_report_callbacks_.erase(it); +} + +bool ConversionReporterImpl::ReportComparator::operator()( + const std::unique_ptr<ConversionReport>& a, + const std::unique_ptr<ConversionReport>& b) const { + // Returns whether a should appear before b in ordering. Because + // std::priority_queue is max priority queue, we used greater then to make a + // min priority queue. + return a->report_time > b->report_time; +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_reporter_impl.h b/chromium/content/browser/conversions/conversion_reporter_impl.h new file mode 100644 index 00000000000..855a2b3a4f4 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_reporter_impl.h @@ -0,0 +1,111 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_REPORTER_IMPL_H_ +#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_REPORTER_IMPL_H_ + +#include <stdint.h> +#include <memory> +#include <queue> +#include <vector> + +#include "base/callback.h" +#include "base/containers/flat_map.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "content/browser/conversions/conversion_manager_impl.h" +#include "content/browser/conversions/conversion_report.h" +#include "content/common/content_export.h" +#include "services/network/public/cpp/shared_url_loader_factory.h" + +namespace base { +class Clock; +} // namespace base + +namespace content { + +class StoragePartition; + +// This class is responsible for managing the dispatch of conversion reports to +// a ConversionReporterImpl::NetworkSender. It maintains a queue of reports and +// a timer to ensure all reports are sent at the correct time, since the time in +// which a conversion report is sent is potentially sensitive information. +// Created and owned by ConversionManager. +class CONTENT_EXPORT ConversionReporterImpl + : public ConversionManagerImpl::ConversionReporter { + public: + // This class is responsible for sending conversion reports to their + // configured endpoints over the network. + class NetworkSender { + public: + virtual ~NetworkSender() = default; + + // Callback used to notify caller that the requested report has been sent. + using ReportSentCallback = base::OnceCallback<void()>; + + // Generates and sends a conversion report matching |report|. This should + // generate a secure POST quest with no-credentials. Does not persist the + // raw pointer. + virtual void SendReport(ConversionReport* report, + ReportSentCallback sent_callback) = 0; + }; + + ConversionReporterImpl(StoragePartition* storage_partition, + const base::Clock* clock); + ConversionReporterImpl(const ConversionReporterImpl&) = delete; + ConversionReporterImpl& operator=(const ConversionReporterImpl&) = delete; + ~ConversionReporterImpl() override; + + // ConversionManagerImpl::ConversionReporter: + void AddReportsToQueue( + std::vector<ConversionReport> reports, + base::RepeatingCallback<void(int64_t)> report_sent_callback) override; + + void SetNetworkSenderForTesting( + std::unique_ptr<NetworkSender> network_sender); + + private: + void MaybeScheduleNextReport(); + void SendNextReport(); + + // Called when a conversion report sent via NetworkSender::SendReport() has + // completed loading. + void OnReportSent(int64_t conversion_id); + + // Comparator used to order ConversionReports by their report time, with the + // smallest time at the top of |report_queue_|. + struct ReportComparator { + bool operator()(const std::unique_ptr<ConversionReport>& a, + const std::unique_ptr<ConversionReport>& b) const; + }; + + // Priority queue which holds reports that are yet to be sent. Reports are + // removed from the queue when they are delivered to the NetworkSender. + std::priority_queue<std::unique_ptr<ConversionReport>, + std::vector<std::unique_ptr<ConversionReport>>, + ReportComparator> + report_queue_; + + // Map of all conversion ids that are currently in |report_queue| or are being + // sent by |network_sender_|, and their associated report sent callbacks. The + // number of concurrent conversion reports being sent at any time is expected + // to be small, so a flat_map is used. + base::flat_map<int64_t, base::OnceCallback<void(int64_t)>> + conversion_report_callbacks_; + + const base::Clock* clock_; + + // Timer which signals the next report in |report_queue_| should be sent. + base::OneShotTimer send_report_timer_; + + // Responsible for issuing requests to network for report that need to be + // sent. Calls OnReportSent() when a report has finished sending. + // + // Should never be nullptr. + std::unique_ptr<NetworkSender> network_sender_; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_REPORTER_IMPL_H_ diff --git a/chromium/content/browser/conversions/conversion_reporter_impl_unittest.cc b/chromium/content/browser/conversions/conversion_reporter_impl_unittest.cc new file mode 100644 index 00000000000..a2b030ed8f6 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_reporter_impl_unittest.cc @@ -0,0 +1,203 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_reporter_impl.h" + +#include <stdint.h> + +#include "base/bind.h" +#include "base/sequenced_task_runner.h" +#include "base/strings/strcat.h" +#include "base/task/post_task.h" +#include "base/test/bind_test_util.h" +#include "base/test/simple_test_clock.h" +#include "content/browser/conversions/conversion_manager.h" +#include "content/browser/conversions/conversion_test_utils.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/test/browser_task_environment.h" +#include "content/public/test/test_browser_context.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +namespace { + +// Create a report which should be sent at |report_time|. Impression +// data/conversion data/conversion id are all the same for simplicity. +ConversionReport GetReport(base::Time report_time, int64_t conversion_id) { + // Construct impressions with a null impression time as it is not used for + // reporting. + return ConversionReport(ImpressionBuilder(base::Time()).Build(), + /*conversion_data=*/"", report_time, + /*conversion_id=*/conversion_id); +} + +// NetworkSender that keep track of the last sent report id. +class MockNetworkSender : public ConversionReporterImpl::NetworkSender { + public: + MockNetworkSender() = default; + + void SendReport(ConversionReport* conversion_report, + ReportSentCallback sent_callback) override { + last_sent_report_id_ = *conversion_report->conversion_id; + num_reports_sent_++; + std::move(sent_callback).Run(); + } + + int64_t last_sent_report_id() { return last_sent_report_id_; } + + size_t num_reports_sent() { return num_reports_sent_; } + + private: + size_t num_reports_sent_ = 0u; + int64_t last_sent_report_id_ = -1; +}; + +} // namespace + +class ConversionReporterImplTest : public testing::Test { + public: + ConversionReporterImplTest() + : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME), + browser_context_(std::make_unique<TestBrowserContext>()), + reporter_(std::make_unique<ConversionReporterImpl>( + BrowserContext::GetDefaultStoragePartition(browser_context_.get()), + task_environment_.GetMockClock())) { + auto network_sender = std::make_unique<MockNetworkSender>(); + sender_ = network_sender.get(); + reporter_->SetNetworkSenderForTesting(std::move(network_sender)); + } + + const base::Clock& clock() { return *task_environment_.GetMockClock(); } + + protected: + // |task_enviorment_| must be initialized first. + content::BrowserTaskEnvironment task_environment_; + std::unique_ptr<TestBrowserContext> browser_context_; + + std::unique_ptr<ConversionReporterImpl> reporter_; + MockNetworkSender* sender_; +}; + +TEST_F(ConversionReporterImplTest, + ReportAddedWithImmediateReportTime_ReportSent) { + reporter_->AddReportsToQueue({GetReport(clock().Now(), /*conversion_id=*/1)}, + base::BindRepeating([](int64_t conversion_id) { + EXPECT_EQ(1L, conversion_id); + })); + + // Fast forward by 0, as we yield the thread when a report is scheduled to be + // sent. + task_environment_.FastForwardBy(base::TimeDelta()); + EXPECT_EQ(1, sender_->last_sent_report_id()); +} + +TEST_F(ConversionReporterImplTest, + ReportWithReportTimeBeforeCurrentTime_ReportSent) { + reporter_->AddReportsToQueue( + {GetReport(clock().Now() - base::TimeDelta::FromHours(10), + /*conversion_id=*/1)}, + base::BindRepeating( + [](int64_t conversion_id) { EXPECT_EQ(1L, conversion_id); })); + + // Fast forward by 0, as we yield the thread when a report is scheduled to be + // sent. + task_environment_.FastForwardBy(base::TimeDelta()); + EXPECT_EQ(1, sender_->last_sent_report_id()); +} + +TEST_F(ConversionReporterImplTest, + ReportWithDelayedReportTime_NotSentUntilDelay) { + const base::TimeDelta delay = base::TimeDelta::FromMinutes(30); + + reporter_->AddReportsToQueue( + {GetReport(clock().Now() + delay, /*conversion_id=*/1)}, + base::DoNothing()); + task_environment_.FastForwardBy(base::TimeDelta()); + EXPECT_EQ(0u, sender_->num_reports_sent()); + + task_environment_.FastForwardBy(delay - base::TimeDelta::FromSeconds(1)); + EXPECT_EQ(0u, sender_->num_reports_sent()); + + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(1)); + EXPECT_EQ(1u, sender_->num_reports_sent()); +} + +TEST_F(ConversionReporterImplTest, DuplicateReportScheduled_Ignored) { + reporter_->AddReportsToQueue( + {GetReport(clock().Now() + base::TimeDelta::FromMinutes(1), + /*conversion_id=*/1)}, + base::DoNothing()); + + // A duplicate report should not be scheduled. + reporter_->AddReportsToQueue( + {GetReport(clock().Now() + base::TimeDelta::FromMinutes(1), + /*conversion_id=*/1)}, + base::DoNothing()); + task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(1)); + EXPECT_EQ(1u, sender_->num_reports_sent()); +} + +TEST_F(ConversionReporterImplTest, + NewReportWithPreviouslySeenConversionId_Scheduled) { + reporter_->AddReportsToQueue({GetReport(clock().Now(), /*conversion_id=*/1)}, + base::DoNothing()); + task_environment_.FastForwardBy(base::TimeDelta()); + EXPECT_EQ(1u, sender_->num_reports_sent()); + + // We should schedule the new report because the previous report has been + // sent. + reporter_->AddReportsToQueue({GetReport(clock().Now(), /*conversion_id=*/1)}, + base::DoNothing()); + task_environment_.FastForwardBy(base::TimeDelta()); + EXPECT_EQ(2u, sender_->num_reports_sent()); +} + +TEST_F(ConversionReporterImplTest, ManyReportsAddedAtOnce_SentInOrder) { + std::vector<ConversionReport> reports; + int64_t last_report_id = 0UL; + for (int i = 1; i < 10; i++) { + reports.push_back(GetReport(clock().Now() + base::TimeDelta::FromMinutes(i), + /*conversion_id=*/i)); + } + reporter_->AddReportsToQueue( + reports, base::BindLambdaForTesting([&](int64_t conversion_id) { + last_report_id = conversion_id; + })); + task_environment_.FastForwardBy(base::TimeDelta()); + EXPECT_EQ(0u, sender_->num_reports_sent()); + + for (int i = 1; i < 10; i++) { + task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(1)); + + EXPECT_EQ(static_cast<size_t>(i), sender_->num_reports_sent()); + EXPECT_EQ(static_cast<int64_t>(i), sender_->last_sent_report_id()); + EXPECT_EQ(static_cast<int64_t>(i), last_report_id); + } +} + +TEST_F(ConversionReporterImplTest, ManyReportsAddedSeparately_SentInOrder) { + int64_t last_report_id = 0; + auto report_sent_callback = base::BindLambdaForTesting( + [&](int64_t conversion_id) { last_report_id = conversion_id; }); + for (int i = 1; i < 10; i++) { + reporter_->AddReportsToQueue( + {GetReport(clock().Now() + base::TimeDelta::FromMinutes(i), + /*conversion_id=*/i)}, + report_sent_callback); + } + task_environment_.FastForwardBy(base::TimeDelta()); + EXPECT_EQ(0u, sender_->num_reports_sent()); + + for (int i = 1; i < 10; i++) { + task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(1)); + + EXPECT_EQ(static_cast<size_t>(i), sender_->num_reports_sent()); + EXPECT_EQ(static_cast<int64_t>(i), sender_->last_sent_report_id()); + EXPECT_EQ(static_cast<int64_t>(i), last_report_id); + } +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_storage.h b/chromium/content/browser/conversions/conversion_storage.h index cfdae6e57ce..1c8feed1ccf 100644 --- a/chromium/content/browser/conversions/conversion_storage.h +++ b/chromium/content/browser/conversions/conversion_storage.h @@ -8,10 +8,12 @@ #include <stdint.h> #include <vector> +#include "base/callback.h" #include "base/time/time.h" #include "content/browser/conversions/conversion_report.h" #include "content/browser/conversions/storable_conversion.h" #include "content/browser/conversions/storable_impression.h" +#include "url/origin.h" namespace content { @@ -41,6 +43,21 @@ class ConversionStorage { // Impressions will be checked against this limit after they schedule a new // report. virtual int GetMaxConversionsPerImpression() const = 0; + + // These limits are designed solely to avoid excessive disk / memory usage. + // In particular, they do not correspond with any privacy parameters. + // TODO(crbug.com/1082754): Consider replacing this functionality (and the + // data deletion logic) with the quota system. + // + // Returns the maximum number of impressions that can be in storage at any + // time for an impression top-level origin. + virtual int GetMaxImpressionsPerOrigin() const = 0; + // Returns the maximum number of conversions that can be in storage at any + // time for a conversion top-level origin. Note that since reporting + // origins are the actual entities that invoke conversion registration, we + // could consider changing this limit to be keyed by a <conversion origin, + // reporting origin> tuple. + virtual int GetMaxConversionsPerOrigin() const = 0; }; virtual ~ConversionStorage() = default; @@ -69,6 +86,13 @@ class ConversionStorage { virtual std::vector<ConversionReport> GetConversionsToReport( base::Time max_report_time) = 0; + // Returns all active impressions in storage. Active impressions are all + // impressions that can still convert. Impressions that: are past expiry, + // reached the conversion limit, or was marked inactive due to having + // converted and then superceded by a matching impression should not be + // returned. + virtual std::vector<StorableImpression> GetActiveImpressions() = 0; + // Deletes all impressions that have expired and have no pending conversion // reports. Returns the number of impressions that were deleted. virtual int DeleteExpiredImpressions() = 0; @@ -77,9 +101,18 @@ class ConversionStorage { // whether the deletion was successful. virtual bool DeleteConversion(int64_t conversion_id) = 0; - // TODO(johnidel): Add an API to ConversionStorage that removes site data, and - // hook it into the data remover. This should be added before the API is - // enabled. + // Deletes all data in storage for URLs matching |filter|, between + // |delete_begin| and |delete_end| time. More specifically, this: + // 1. Deletes all impressions within the time range. If any conversion is + // attributed to this impression it is also deleted. + // 2. Deletes all conversions within the time range. All impressions + // attributed to the conversion are also deleted. + // + // Note: if |filter| is null, it means that all Origins should match. + virtual void ClearData( + base::Time delete_begin, + base::Time delete_end, + base::RepeatingCallback<bool(const url::Origin& origin)> filter) = 0; }; } // namespace content diff --git a/chromium/content/browser/conversions/conversion_storage_delegate_impl.cc b/chromium/content/browser/conversions/conversion_storage_delegate_impl.cc new file mode 100644 index 00000000000..6899b7ef7f8 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_storage_delegate_impl.cc @@ -0,0 +1,110 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_storage_delegate_impl.h" + +namespace content { + +ConversionStorageDelegateImpl::ConversionStorageDelegateImpl(bool debug_mode) + : debug_mode_(debug_mode) { + DETACH_FROM_SEQUENCE(sequence_checker_); +} + +void ConversionStorageDelegateImpl::ProcessNewConversionReports( + std::vector<ConversionReport>* reports) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!reports->empty()); + ConversionReport* last_report = &(reports->at(0)); + + // Assign attribution credits to each report that will be sent at report time. + // This performs "last click" attribution which assigns the report + // for the most recent impression a credit of 100, and the rest a credit of 0. + for (ConversionReport& report : *reports) { + report.report_time = GetReportTimeForConversion(report); + + report.attribution_credit = 0; + if (report.impression.impression_time() > + last_report->impression.impression_time()) + last_report = &report; + } + + last_report->attribution_credit = 100; +} + +int ConversionStorageDelegateImpl::GetMaxConversionsPerImpression() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + return 3; +} + +int ConversionStorageDelegateImpl::GetMaxImpressionsPerOrigin() const { + return 1024; +} + +int ConversionStorageDelegateImpl::GetMaxConversionsPerOrigin() const { + return 1024; +} + +base::Time ConversionStorageDelegateImpl::GetReportTimeForConversion( + const ConversionReport& report) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + // |report.report_time| is roughly ~now, for newly created conversion + // reports. If in debug mode, the report should be sent immediately. + if (debug_mode_) + return report.report_time; + + // After the initial impression, a schedule of reporting windows and deadlines + // associated with that impression begins. The time between impression time + // and impression expiry is split into multiple reporting windows. At the end + // of each window, the browser will send all scheduled reports for that + // impression. + // + // Each reporting window has a deadline and only conversions registered before + // that deadline are sent in that window. Each deadline is one hour prior to + // the window report time. The deadlines relative to impression time are <2 + // days minus 1 hour, 7 days minus 1 hour, impression expiry>. The impression + // expiry window is only used for conversions that occur after the 7 day + // deadline. For example, a conversion which happens one hour after an + // impression with an expiry of two hours, is still reported in the 2 day + // window. + constexpr base::TimeDelta kWindowDeadlineOffset = + base::TimeDelta::FromHours(1); + base::TimeDelta expiry_deadline = + report.impression.expiry_time() - report.impression.impression_time(); + const base::TimeDelta kReportingWindowDeadlines[] = { + base::TimeDelta::FromDays(2) - kWindowDeadlineOffset, + base::TimeDelta::FromDays(7) - kWindowDeadlineOffset, expiry_deadline}; + + base::TimeDelta deadline_to_use; + + // Given a conversion report that was created at |report.report_time|, find + // the first applicable reporting window this conversion should be reported + // at. + for (base::TimeDelta report_window_deadline : kReportingWindowDeadlines) { + // If this window is valid for the conversion, use it. |report.report_time| + // is roughly ~now, as the conversion time is used as the default value for + // newly created reports that have not had a report time set. + if (report.impression.impression_time() + report_window_deadline >= + report.report_time) { + deadline_to_use = report_window_deadline; + break; + } + } + + // Valid conversion reports should always have a valid reporting deadline. + DCHECK(!deadline_to_use.is_zero()); + + // If the expiry deadline falls after the first window, but before another + // window, use it instead. For example, if expiry is at 3 days, we can send + // reports at the 2 day deadline and the expiry deadline instead of at the 7 + // day deadline. + if (expiry_deadline > kReportingWindowDeadlines[0] && + expiry_deadline < deadline_to_use) { + deadline_to_use = expiry_deadline; + } + + return report.impression.impression_time() + deadline_to_use + + kWindowDeadlineOffset; +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_storage_delegate_impl.h b/chromium/content/browser/conversions/conversion_storage_delegate_impl.h new file mode 100644 index 00000000000..c0d103a7905 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_storage_delegate_impl.h @@ -0,0 +1,53 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_DELEGATE_IMPL_H_ +#define CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_DELEGATE_IMPL_H_ + +#include "base/sequence_checker.h" +#include "base/time/time.h" +#include "content/browser/conversions/conversion_report.h" +#include "content/browser/conversions/conversion_storage.h" +#include "content/common/content_export.h" + +namespace content { + +// Implementation of the storage delegate. This class handles assigning +// attribution credits and report times to newly created conversion reports. It +// also controls constants for ConversionStorage. This is owned by +// ConversionStorageSql, and should only be accessed on the conversions storage +// task runner. +class CONTENT_EXPORT ConversionStorageDelegateImpl + : public ConversionStorage::Delegate { + public: + explicit ConversionStorageDelegateImpl(bool debug_mode = false); + ConversionStorageDelegateImpl(const ConversionStorageDelegateImpl& other) = + delete; + ConversionStorageDelegateImpl& operator=( + const ConversionStorageDelegateImpl& other) = delete; + ~ConversionStorageDelegateImpl() override = default; + + // ConversionStorageDelegate: + void ProcessNewConversionReports( + std::vector<ConversionReport>* reports) override; + int GetMaxConversionsPerImpression() const override; + int GetMaxImpressionsPerOrigin() const override; + int GetMaxConversionsPerOrigin() const override; + + private: + // Get the time a conversion report should be sent, by batching reports into + // set reporting windows based on their impression time. This strictly delays + // the time a report will be sent. + base::Time GetReportTimeForConversion(const ConversionReport& report) const; + + // Whether the API is running in debug mode, meaning that there should be + // no delays or noise added to reports. + bool debug_mode_ = false; + + SEQUENCE_CHECKER(sequence_checker_); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_DELEGATE_IMPL_H_ diff --git a/chromium/content/browser/conversions/conversion_storage_delegate_impl_unittest.cc b/chromium/content/browser/conversions/conversion_storage_delegate_impl_unittest.cc new file mode 100644 index 00000000000..bffa131e510 --- /dev/null +++ b/chromium/content/browser/conversions/conversion_storage_delegate_impl_unittest.cc @@ -0,0 +1,148 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/conversions/conversion_storage_delegate_impl.h" + +#include <vector> + +#include "base/optional.h" +#include "base/time/time.h" +#include "content/browser/conversions/conversion_report.h" +#include "content/browser/conversions/conversion_test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +namespace { + +constexpr base::TimeDelta kDefaultExpiry = base::TimeDelta::FromDays(30); + +ConversionReport GetReport(base::Time impression_time, + base::Time conversion_time, + base::TimeDelta expiry = kDefaultExpiry) { + return ConversionReport( + ImpressionBuilder(impression_time).SetExpiry(expiry).Build(), + /*conversion_data=*/"123", conversion_time, + /*conversion_id=*/base::nullopt); +} + +} // namespace + +class ConversionStorageDelegateImplTest : public testing::Test { + public: + ConversionStorageDelegateImplTest() = default; +}; + +TEST_F(ConversionStorageDelegateImplTest, ImmediateConversion_FirstWindowUsed) { + base::Time impression_time = base::Time::Now(); + std::vector<ConversionReport> reports = { + GetReport(impression_time, /*conversion_time=*/impression_time)}; + ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports); + EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2), + reports[0].report_time); +} + +TEST_F(ConversionStorageDelegateImplTest, + ConversionImmediatelyBeforeWindow_NextWindowUsed) { + base::Time impression_time = base::Time::Now(); + base::Time conversion_time = impression_time + base::TimeDelta::FromDays(2) - + base::TimeDelta::FromMinutes(1); + std::vector<ConversionReport> reports = { + GetReport(impression_time, conversion_time)}; + ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports); + EXPECT_EQ(impression_time + base::TimeDelta::FromDays(7), + reports[0].report_time); +} + +TEST_F(ConversionStorageDelegateImplTest, + ConversionBeforeWindowDelay_WindowUsed) { + base::Time impression_time = base::Time::Now(); + + // The deadline for a window is 1 hour before the window. Use a time just + // before the deadline. + base::Time conversion_time = impression_time + base::TimeDelta::FromDays(2) - + base::TimeDelta::FromMinutes(61); + std::vector<ConversionReport> reports = { + GetReport(impression_time, conversion_time)}; + ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports); + EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2), + reports[0].report_time); +} + +TEST_F(ConversionStorageDelegateImplTest, + ImpressionExpiryBeforeTwoDayWindow_TwoDayWindowUsed) { + base::Time impression_time = base::Time::Now(); + base::Time conversion_time = impression_time + base::TimeDelta::FromHours(1); + + // Set the impression to expire before the two day window. + std::vector<ConversionReport> reports = { + GetReport(impression_time, conversion_time, + /*expiry=*/base::TimeDelta::FromHours(2))}; + ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports); + EXPECT_EQ(impression_time + base::TimeDelta::FromDays(2), + reports[0].report_time); +} + +TEST_F(ConversionStorageDelegateImplTest, + ImpressionExpiryBeforeSevenDayWindow_ExpiryWindowUsed) { + base::Time impression_time = base::Time::Now(); + base::Time conversion_time = impression_time + base::TimeDelta::FromDays(3); + + // Set the impression to expire before the two day window. + std::vector<ConversionReport> reports = { + GetReport(impression_time, conversion_time, + /*expiry=*/base::TimeDelta::FromDays(4))}; + ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports); + + // The expiry window is reported one hour after expiry time. + EXPECT_EQ(impression_time + base::TimeDelta::FromDays(4) + + base::TimeDelta::FromHours(1), + reports[0].report_time); +} + +TEST_F(ConversionStorageDelegateImplTest, + ImpressionExpiryAfterSevenDayWindow_ExpiryWindowUsed) { + base::Time impression_time = base::Time::Now(); + base::Time conversion_time = impression_time + base::TimeDelta::FromDays(7); + + // Set the impression to expire before the two day window. + std::vector<ConversionReport> reports = { + GetReport(impression_time, conversion_time, + /*expiry=*/base::TimeDelta::FromDays(9))}; + ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports); + + // The expiry window is reported one hour after expiry time. + EXPECT_EQ(impression_time + base::TimeDelta::FromDays(9) + + base::TimeDelta::FromHours(1), + reports[0].report_time); +} + +TEST_F(ConversionStorageDelegateImplTest, + SingleReportForConversion_AttributionCreditAssigned) { + base::Time now = base::Time::Now(); + std::vector<ConversionReport> reports = { + GetReport(/*impression_time=*/now, /*conversion_time=*/now)}; + ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports); + EXPECT_EQ(1u, reports.size()); + EXPECT_EQ(100, reports[0].attribution_credit); +} + +TEST_F(ConversionStorageDelegateImplTest, + TwoReportsForConversion_LastReceivesCredit) { + base::Time now = base::Time::Now(); + std::vector<ConversionReport> reports = { + GetReport(/*impression_time=*/now, /*conversion_time=*/now), + GetReport(/*impression_time=*/now + base::TimeDelta::FromHours(100), + /*conversion_time=*/now)}; + ConversionStorageDelegateImpl().ProcessNewConversionReports(&reports); + EXPECT_EQ(2u, reports.size()); + EXPECT_EQ(0, reports[0].attribution_credit); + EXPECT_EQ(100, reports[1].attribution_credit); + + // Ensure the reports were not rearranged. + EXPECT_EQ(now + base::TimeDelta::FromHours(100), + reports[1].impression.impression_time()); +} + +} // namespace content diff --git a/chromium/content/browser/conversions/conversion_storage_sql.cc b/chromium/content/browser/conversions/conversion_storage_sql.cc index 9f8abdbb270..a921f883991 100644 --- a/chromium/content/browser/conversions/conversion_storage_sql.cc +++ b/chromium/content/browser/conversions/conversion_storage_sql.cc @@ -8,8 +8,10 @@ #include <utility> #include "base/bind.h" +#include "base/containers/flat_set.h" #include "base/logging.h" #include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" #include "base/optional.h" #include "base/time/default_clock.h" #include "base/time/time.h" @@ -28,16 +30,10 @@ const base::FilePath::CharType kDatabaseName[] = FILE_PATH_LITERAL("Conversions"); std::string SerializeOrigin(const url::Origin& origin) { - // Conversion API is only designed to be used for secure contexts (targets and - // reporting endpoints). We should have filtered out bad origins at a higher - // layer. - // - // Because we only allow https origins to use the API, we could potentially - // omit the scheme from storage to save 8 bytes per origin. However this would - // require maintaining our own serialization logic and also complicates - // extending storage to other scheme in the future. + // Conversion API is only designed to be used for secure + // contexts (targets and reporting endpoints). We should have filtered out bad + // origins at a higher layer. DCHECK(!origin.opaque()); - DCHECK_EQ(url::kHttpsScheme, origin.scheme()); return origin.Serialize(); } @@ -58,12 +54,13 @@ base::Time DeserializeTime(int64_t microseconds) { ConversionStorageSql::ConversionStorageSql( const base::FilePath& path_to_database_dir, - Delegate* delegate, - base::Clock* clock) + std::unique_ptr<Delegate> delegate, + const base::Clock* clock) : path_to_database_(path_to_database_dir.Append(kDatabaseName)), clock_(clock), - delegate_(delegate), + delegate_(std::move(delegate)), weak_factory_(this) { + DCHECK(delegate_); DETACH_FROM_SEQUENCE(sequence_checker_); } @@ -91,6 +88,19 @@ void ConversionStorageSql::StoreImpression( const StorableImpression& impression) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + // Cleanup any impression that may be expired by this point. This is done when + // an impression is added to prevent additional logic for cleaning the table + // while providing a guarantee that the size of the table is proportional to + // the number of active impression. + DeleteExpiredImpressions(); + + // TODO(csharrison): Thread this failure to the caller and report a console + // error. + const std::string serialized_impression_origin = + SerializeOrigin(impression.impression_origin()); + if (!HasCapacityForStoringImpression(serialized_impression_origin)) + return; + // Wrap the deactivation and insertion in the same transaction. If the // deactivation fails, we do not want to store the new impression as we may // return the wrong set of impressions for a conversion. @@ -98,6 +108,11 @@ void ConversionStorageSql::StoreImpression( if (!transaction.Begin()) return; + const std::string serialized_conversion_origin = + SerializeOrigin(impression.conversion_origin()); + const std::string serialized_reporting_origin = + SerializeOrigin(impression.reporting_origin()); + // In the case where we get a new impression for a given <reporting_origin, // conversion_origin> we should mark all active, converted impressions with // the matching <reporting_origin, conversion_origin> as not active. @@ -107,10 +122,8 @@ void ConversionStorageSql::StoreImpression( "active = 1 AND num_conversions > 0"; sql::Statement deactivate_statement(db_.GetCachedStatement( SQL_FROM_HERE, kDeactivateMatchingConvertedImpressionsSql)); - deactivate_statement.BindString( - 0, SerializeOrigin(impression.conversion_origin())); - deactivate_statement.BindString( - 1, SerializeOrigin(impression.reporting_origin())); + deactivate_statement.BindString(0, serialized_conversion_origin); + deactivate_statement.BindString(1, serialized_reporting_origin); deactivate_statement.Run(); const char kInsertImpressionSql[] = @@ -121,9 +134,9 @@ void ConversionStorageSql::StoreImpression( sql::Statement statement( db_.GetCachedStatement(SQL_FROM_HERE, kInsertImpressionSql)); statement.BindString(0, impression.impression_data()); - statement.BindString(1, SerializeOrigin(impression.impression_origin())); - statement.BindString(2, SerializeOrigin(impression.conversion_origin())); - statement.BindString(3, SerializeOrigin(impression.reporting_origin())); + statement.BindString(1, serialized_impression_origin); + statement.BindString(2, serialized_conversion_origin); + statement.BindString(3, serialized_reporting_origin); statement.BindInt64(4, SerializeTime(impression.impression_time())); statement.BindInt64(5, SerializeTime(impression.expiry_time())); statement.Run(); @@ -136,7 +149,15 @@ int ConversionStorageSql::MaybeCreateAndStoreConversionReports( DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); const url::Origin& conversion_origin = conversion.conversion_origin(); + const std::string serialized_conversion_origin = + SerializeOrigin(conversion_origin); + if (!HasCapacityForStoringConversion(serialized_conversion_origin)) + return 0; + const url::Origin& reporting_origin = conversion.reporting_origin(); + DCHECK(!conversion_origin.opaque()); + DCHECK(!reporting_origin.opaque()); + base::Time current_time = clock_->Now(); int64_t serialized_current_time = SerializeTime(current_time); @@ -151,7 +172,7 @@ int ConversionStorageSql::MaybeCreateAndStoreConversionReports( sql::Statement statement( db_.GetCachedStatement(SQL_FROM_HERE, kGetMatchingImpressionsSql)); - statement.BindString(0, SerializeOrigin(conversion_origin)); + statement.BindString(0, serialized_conversion_origin); statement.BindString(1, SerializeOrigin(reporting_origin)); statement.BindInt64(2, serialized_current_time); @@ -162,6 +183,11 @@ int ConversionStorageSql::MaybeCreateAndStoreConversionReports( std::string impression_data = statement.ColumnString(1); url::Origin impression_origin = DeserializeOrigin(statement.ColumnString(2)); + + // Skip the report if the impression origin is opaque. This should only + // happen if there is some sort of database corruption. + if (impression_origin.opaque()) + continue; base::Time impression_time = DeserializeTime(statement.ColumnInt64(3)); base::Time expiry_time = DeserializeTime(statement.ColumnInt64(4)); @@ -274,6 +300,15 @@ std::vector<ConversionReport> ConversionStorageSql::GetConversionsToReport( base::Time expiry_time = DeserializeTime(statement.ColumnInt64(9)); int64_t impression_id = statement.ColumnInt64(10); + // Ensure origins are valid before continuing. This could happen if there is + // database corruption. + // TODO(csharrison): This should be an extremely rare occurrence but it + // would entail that some records will remain in the DB as vestigial if a + // conversion is never sent. We should delete these entries from the DB. + if (impression_origin.opaque() || conversion_origin.opaque() || + reporting_origin.opaque()) + continue; + // Create the impression and ConversionReport objects from the retrieved // columns. StorableImpression impression(impression_data, impression_origin, @@ -292,6 +327,37 @@ std::vector<ConversionReport> ConversionStorageSql::GetConversionsToReport( return conversions; } +std::vector<StorableImpression> ConversionStorageSql::GetActiveImpressions() { + const char kGetImpressionsSql[] = + "SELECT impression_data, impression_origin, conversion_origin, " + "reporting_origin, impression_time, expiry_time, impression_id " + "FROM impressions WHERE active = 1 AND expiry_time > ?"; + sql::Statement statement( + db_.GetCachedStatement(SQL_FROM_HERE, kGetImpressionsSql)); + statement.BindInt64(0, SerializeTime(clock_->Now())); + + std::vector<StorableImpression> impressions; + while (statement.Step()) { + std::string impression_data = statement.ColumnString(0); + url::Origin impression_origin = + DeserializeOrigin(statement.ColumnString(1)); + url::Origin conversion_origin = + DeserializeOrigin(statement.ColumnString(2)); + url::Origin reporting_origin = DeserializeOrigin(statement.ColumnString(3)); + base::Time impression_time = DeserializeTime(statement.ColumnInt64(4)); + base::Time expiry_time = DeserializeTime(statement.ColumnInt64(5)); + int64_t impression_id = statement.ColumnInt64(6); + + StorableImpression impression(impression_data, impression_origin, + conversion_origin, reporting_origin, + impression_time, expiry_time, impression_id); + impressions.push_back(std::move(impression)); + } + if (!statement.Succeeded()) + return {}; + return impressions; +} + int ConversionStorageSql::DeleteExpiredImpressions() { // Delete all impressions that have no associated conversions and are past // their expiry time. Optimized by |kImpressionExpiryIndexSql|. @@ -331,10 +397,205 @@ bool ConversionStorageSql::DeleteConversion(int64_t conversion_id) { if (!statement.Run()) return false; - DCHECK_EQ(1, db_.GetLastChangeCount()); return db_.GetLastChangeCount() > 0; } +void ConversionStorageSql::ClearData( + base::Time delete_begin, + base::Time delete_end, + base::RepeatingCallback<bool(const url::Origin&)> filter) { + SCOPED_UMA_HISTOGRAM_TIMER("Conversions.ClearDataTime"); + if (filter.is_null()) { + ClearAllDataInRange(delete_begin, delete_end); + return; + } + + // TODO(csharrison, johnidel): This query can be split up and optimized by + // adding indexes on the impression_time and conversion_time columns. + // See this comment for more information: + // crrev.com/c/2150071/4/content/browser/conversions/conversion_storage_sql.cc#342 + const char kScanCandidateData[] = + "SELECT C.conversion_id, I.impression_id," + "I.impression_origin, I.conversion_origin, I.reporting_origin " + "FROM impressions I LEFT JOIN conversions C ON " + "C.impression_id = I.impression_id WHERE" + "(I.impression_time BETWEEN ?1 AND ?2) OR" + "(C.conversion_time BETWEEN ?1 AND ?2)"; + sql::Statement statement( + db_.GetCachedStatement(SQL_FROM_HERE, kScanCandidateData)); + statement.BindInt64(0, SerializeTime(delete_begin)); + statement.BindInt64(1, SerializeTime(delete_end)); + + std::vector<int64_t> impression_ids_to_delete; + std::vector<int64_t> conversion_ids_to_delete; + while (statement.Step()) { + int64_t conversion_id = statement.ColumnInt64(0); + int64_t impression_id = statement.ColumnInt64(1); + if (filter.Run(DeserializeOrigin(statement.ColumnString(2))) || + filter.Run(DeserializeOrigin(statement.ColumnString(3))) || + filter.Run(DeserializeOrigin(statement.ColumnString(4)))) { + impression_ids_to_delete.push_back(impression_id); + if (conversion_id != 0) + conversion_ids_to_delete.push_back(conversion_id); + } + } + + // Since multiple conversions can be associated with a single impression, + // |impression_ids_to_delete| may contain duplicates. Remove duplicates by + // converting the vector into a flat_set. Internally, this sorts the vector + // and then removes duplicates. + const base::flat_set<int64_t> unique_impression_ids_to_delete( + impression_ids_to_delete); + + // TODO(csharrison, johnidel): Should we consider poisoning the DB if some of + // the delete operations fail? + if (!statement.Succeeded()) + return; + + // Delete the data in a transaction to avoid cases where the impression part + // of a conversion is deleted without deleting the associated conversion, or + // vice versa. + sql::Transaction transaction(&db_); + if (!transaction.Begin()) + return; + + for (int64_t impression_id : unique_impression_ids_to_delete) { + const char kDeleteImpressionSql[] = + "DELETE FROM impressions WHERE impression_id = ?"; + sql::Statement impression_statement( + db_.GetCachedStatement(SQL_FROM_HERE, kDeleteImpressionSql)); + impression_statement.BindInt64(0, impression_id); + if (!impression_statement.Run()) + return; + } + + for (int64_t conversion_id : conversion_ids_to_delete) { + const char kDeleteConversionSql[] = + "DELETE FROM conversions WHERE conversion_id = ?"; + sql::Statement conversion_statement( + db_.GetCachedStatement(SQL_FROM_HERE, kDeleteConversionSql)); + conversion_statement.BindInt64(0, conversion_id); + if (!conversion_statement.Run()) + return; + } + + // Careful! At this point we can still have some vestigial entries in the DB. + // For example, if an impression has two conversions, and one conversion is + // deleted, the above logic will delete the impression as well, leaving the + // second conversion in limbo (it was not in the deletion time range). + // Delete all unattributed conversions here to ensure everything is cleaned + // up. + for (int64_t impression_id : unique_impression_ids_to_delete) { + const char kDeleteVestigialConversionSql[] = + "DELETE FROM conversions WHERE impression_id = ?"; + sql::Statement delete_vestigial_statement( + db_.GetCachedStatement(SQL_FROM_HERE, kDeleteVestigialConversionSql)); + delete_vestigial_statement.BindInt64(0, impression_id); + if (!delete_vestigial_statement.Run()) + return; + } + transaction.Commit(); +} + +void ConversionStorageSql::ClearAllDataInRange(base::Time delete_begin, + base::Time delete_end) { + // Browsing data remover will call this with null |delete_begin|, but also + // perform the ClearAllDataAllTime optimization if |delete_begin| is + // base::Time::Min(). + if ((delete_begin.is_null() || delete_begin.is_min()) && + delete_end.is_max()) { + ClearAllDataAllTime(); + return; + } + + sql::Transaction transaction(&db_); + if (!transaction.Begin()) + return; + + // Delete all impressions and conversion reports in the given time range. + // Note: This should follow the same basic logic in ClearData, with the + // assumption that all origins match the filter. This means we can omit a + // SELECT statement, and all of the in-memory id management. + // + // Optimizing these queries are also tough, see this comment for an idea: + // http://crrev.com/c/2150071/12/content/browser/conversions/conversion_storage_sql.cc#468 + const char kDeleteImpressionRangeSql[] = + "DELETE FROM impressions WHERE (impression_time BETWEEN ?1 AND ?2) OR " + "impression_id in (SELECT impression_id FROM conversions " + "WHERE conversion_time BETWEEN ?1 AND ?2)"; + sql::Statement delete_impressions_statement( + db_.GetCachedStatement(SQL_FROM_HERE, kDeleteImpressionRangeSql)); + delete_impressions_statement.BindInt64(0, SerializeTime(delete_begin)); + delete_impressions_statement.BindInt64(1, SerializeTime(delete_end)); + if (!delete_impressions_statement.Run()) + return; + + const char kDeleteConversionRangeSql[] = + "DELETE FROM conversions WHERE (conversion_time BETWEEN ? AND ?) " + "OR impression_id NOT IN (SELECT impression_id FROM impressions)"; + sql::Statement delete_conversions_statement( + db_.GetCachedStatement(SQL_FROM_HERE, kDeleteConversionRangeSql)); + delete_conversions_statement.BindInt64(0, SerializeTime(delete_begin)); + delete_conversions_statement.BindInt64(1, SerializeTime(delete_end)); + if (!delete_conversions_statement.Run()) + return; + transaction.Commit(); +} + +void ConversionStorageSql::ClearAllDataAllTime() { + sql::Transaction transaction(&db_); + if (!transaction.Begin()) + return; + const char kDeleteAllConversionsSql[] = "DELETE FROM conversions"; + const char kDeleteAllImpressionsSql[] = "DELETE FROM impressions"; + sql::Statement delete_all_conversions_statement( + db_.GetCachedStatement(SQL_FROM_HERE, kDeleteAllConversionsSql)); + sql::Statement delete_all_impressions_statement( + db_.GetCachedStatement(SQL_FROM_HERE, kDeleteAllImpressionsSql)); + if (!delete_all_conversions_statement.Run()) + return; + if (!delete_all_impressions_statement.Run()) + return; + transaction.Commit(); +} + +bool ConversionStorageSql::HasCapacityForStoringImpression( + const std::string& serialized_origin) { + // Optimized by impression_origin_idx. + const char kCountImpressionsSql[] = + "SELECT COUNT(impression_origin) FROM impressions WHERE " + "impression_origin = ?"; + sql::Statement statement( + db_.GetCachedStatement(SQL_FROM_HERE, kCountImpressionsSql)); + statement.BindString(0, serialized_origin); + if (!statement.Step()) + return false; + int64_t count = statement.ColumnInt64(0); + return count < delegate_->GetMaxImpressionsPerOrigin(); +} + +bool ConversionStorageSql::HasCapacityForStoringConversion( + const std::string& serialized_origin) { + // This query should be reasonably optimized via conversion_origin_idx. The + // conversion origin is the second column in a multi-column index where the + // first column is just a boolean. Therefore the second column in the index + // should be very well-sorted. + // + // Note: to take advantage of this, we need to hint to the query planner that + // |active| is a boolean, so include it in the conditional. + const char kCountConversionsSql[] = + "SELECT COUNT(conversion_id) FROM conversions C JOIN impressions I ON" + " I.impression_id = C.impression_id" + " WHERE I.conversion_origin = ? AND (active BETWEEN 0 AND 1)"; + sql::Statement statement( + db_.GetCachedStatement(SQL_FROM_HERE, kCountConversionsSql)); + statement.BindString(0, serialized_origin); + if (!statement.Step()) + return false; + int64_t count = statement.ColumnInt64(0); + return count < delegate_->GetMaxConversionsPerOrigin(); +} + bool ConversionStorageSql::InitializeSchema() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); // TODO(johnidel, csharrison): Many impressions will share a target origin and @@ -392,6 +653,13 @@ bool ConversionStorageSql::InitializeSchema() { if (!db_.Execute(kImpressionExpiryIndexSql)) return false; + // Optimizes counting impressions by impression origin. + const char kImpressionOriginIndexSql[] = + "CREATE INDEX IF NOT EXISTS impression_origin_idx " + "ON impressions(impression_origin)"; + if (!db_.Execute(kImpressionOriginIndexSql)) + return false; + // All columns in this table are const. |impression_id| is the primary key of // a row in the [impressions] table, [impressions.impression_id]. // |conversion_time| is the time at which the conversion was registered, and diff --git a/chromium/content/browser/conversions/conversion_storage_sql.h b/chromium/content/browser/conversions/conversion_storage_sql.h index b6d3cbc11d1..686df2f1658 100644 --- a/chromium/content/browser/conversions/conversion_storage_sql.h +++ b/chromium/content/browser/conversions/conversion_storage_sql.h @@ -5,6 +5,9 @@ #ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_SQL_H_ #define CONTENT_BROWSER_CONVERSIONS_CONVERSION_STORAGE_SQL_H_ +#include <memory> +#include <vector> + #include "base/files/file_path.h" #include "base/memory/weak_ptr.h" #include "base/sequence_checker.h" @@ -26,8 +29,8 @@ namespace content { class CONTENT_EXPORT ConversionStorageSql : public ConversionStorage { public: ConversionStorageSql(const base::FilePath& path_to_database_dir, - Delegate* delegate, - base::Clock* clock); + std::unique_ptr<Delegate> delegate, + const base::Clock* clock); ConversionStorageSql(const ConversionStorageSql& other) = delete; ConversionStorageSql& operator=(const ConversionStorageSql& other) = delete; ~ConversionStorageSql() override; @@ -40,8 +43,20 @@ class CONTENT_EXPORT ConversionStorageSql : public ConversionStorage { const StorableConversion& conversion) override; std::vector<ConversionReport> GetConversionsToReport( base::Time expiry_time) override; + std::vector<StorableImpression> GetActiveImpressions() override; int DeleteExpiredImpressions() override; bool DeleteConversion(int64_t conversion_id) override; + void ClearData( + base::Time delete_begin, + base::Time delete_end, + base::RepeatingCallback<bool(const url::Origin&)> filter) override; + + // Variants of ClearData that assume all Origins match the filter. + void ClearAllDataInRange(base::Time delete_begin, base::Time delete_end); + void ClearAllDataAllTime(); + + bool HasCapacityForStoringImpression(const std::string& serialized_origin); + bool HasCapacityForStoringConversion(const std::string& serialized_origin); bool InitializeSchema(); @@ -51,10 +66,9 @@ class CONTENT_EXPORT ConversionStorageSql : public ConversionStorage { sql::Database db_; // Must outlive |this|. - base::Clock* const clock_; + const base::Clock* clock_; - // Must outlive |this|. - Delegate* const delegate_; + std::unique_ptr<Delegate> delegate_; SEQUENCE_CHECKER(sequence_checker_); base::WeakPtrFactory<ConversionStorageSql> weak_factory_; diff --git a/chromium/content/browser/conversions/conversion_storage_sql_unittest.cc b/chromium/content/browser/conversions/conversion_storage_sql_unittest.cc index 83179fa46be..ca408b729c3 100644 --- a/chromium/content/browser/conversions/conversion_storage_sql_unittest.cc +++ b/chromium/content/browser/conversions/conversion_storage_sql_unittest.cc @@ -4,11 +4,14 @@ #include "content/browser/conversions/conversion_storage_sql.h" +#include <functional> #include <memory> +#include "base/bind.h" #include "base/files/scoped_temp_dir.h" #include "base/run_loop.h" #include "base/test/simple_test_clock.h" +#include "base/time/time.h" #include "content/browser/conversions/conversion_report.h" #include "content/browser/conversions/conversion_test_utils.h" #include "content/browser/conversions/storable_conversion.h" @@ -28,8 +31,10 @@ class ConversionStorageSqlTest : public testing::Test { void OpenDatabase() { storage_.reset(); - storage_ = std::make_unique<ConversionStorageSql>(temp_directory_.GetPath(), - &delegate_, &clock_); + auto delegate = std::make_unique<ConfigurableStorageDelegate>(); + delegate_ = delegate.get(); + storage_ = std::make_unique<ConversionStorageSql>( + temp_directory_.GetPath(), std::move(delegate), &clock_); EXPECT_TRUE(storage_->Initialize()); } @@ -48,11 +53,13 @@ class ConversionStorageSqlTest : public testing::Test { ConversionStorage* storage() { return storage_.get(); } + ConfigurableStorageDelegate* delegate() { return delegate_; } + private: base::ScopedTempDir temp_directory_; std::unique_ptr<ConversionStorage> storage_; + ConfigurableStorageDelegate* delegate_ = nullptr; base::SimpleTestClock clock_; - EmptyStorageDelegate delegate_; }; TEST_F(ConversionStorageSqlTest, @@ -66,8 +73,9 @@ TEST_F(ConversionStorageSqlTest, EXPECT_EQ(2u, sql::test::CountSQLTables(&raw_db)); // [conversion_origin_idx], [impression_expiry_idx], - // [conversion_report_time_idx], [conversion_impression_id_idx]. - EXPECT_EQ(4u, sql::test::CountSQLIndices(&raw_db)); + // [impression_origin_idx], [conversion_report_time_idx], + // [conversion_impression_id_idx]. + EXPECT_EQ(5u, sql::test::CountSQLIndices(&raw_db)); } TEST_F(ConversionStorageSqlTest, DatabaseReopened_DataPersisted) { @@ -100,4 +108,155 @@ TEST_F(ConversionStorageSqlTest, CorruptDatabase_RecoveredOnOpen) { EXPECT_TRUE(expecter.SawExpectedErrors()); } +// Create an impression with two conversions (C1 and C2). Craft a query that +// will target C2, which will in turn delete the impression. We should ensure +// that C1 is properly deleted (conversions should not be stored unattributed). +TEST_F(ConversionStorageSqlTest, ClearDataWithVestigialConversion) { + OpenDatabase(); + + base::Time start = clock()->Now(); + auto impression = + ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build(); + storage()->StoreImpression(impression); + + clock()->Advance(base::TimeDelta::FromDays(1)); + EXPECT_EQ( + 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + + clock()->Advance(base::TimeDelta::FromDays(1)); + EXPECT_EQ( + 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + + // Use a time range that only intersects the last conversion. + storage()->ClearData(clock()->Now(), clock()->Now(), + base::BindRepeating(std::equal_to<url::Origin>(), + impression.impression_origin())); + EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty()); + + CloseDatabase(); + + // Verify that everything is deleted. + sql::Database raw_db; + EXPECT_TRUE(raw_db.Open(db_path())); + + size_t conversion_rows; + size_t impression_rows; + sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows); + sql::test::CountTableRows(&raw_db, "impressions", &impression_rows); + + EXPECT_EQ(0u, conversion_rows); + EXPECT_EQ(0u, impression_rows); +} + +// Same as the above test, but with a null filter. +TEST_F(ConversionStorageSqlTest, ClearAllDataWithVestigialConversion) { + OpenDatabase(); + + base::Time start = clock()->Now(); + auto impression = + ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build(); + storage()->StoreImpression(impression); + + clock()->Advance(base::TimeDelta::FromDays(1)); + EXPECT_EQ( + 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + + clock()->Advance(base::TimeDelta::FromDays(1)); + EXPECT_EQ( + 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + + // Use a time range that only intersects the last conversion. + auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>(); + storage()->ClearData(clock()->Now(), clock()->Now(), null_filter); + EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty()); + + CloseDatabase(); + + // Verify that everything is deleted. + sql::Database raw_db; + EXPECT_TRUE(raw_db.Open(db_path())); + + size_t conversion_rows; + size_t impression_rows; + sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows); + sql::test::CountTableRows(&raw_db, "impressions", &impression_rows); + + EXPECT_EQ(0u, conversion_rows); + EXPECT_EQ(0u, impression_rows); +} + +// The max time range with a null filter should delete everything. +TEST_F(ConversionStorageSqlTest, DeleteEverything) { + OpenDatabase(); + + base::Time start = clock()->Now(); + for (int i = 0; i < 10; i++) { + auto impression = ImpressionBuilder(start) + .SetExpiry(base::TimeDelta::FromDays(30)) + .Build(); + storage()->StoreImpression(impression); + clock()->Advance(base::TimeDelta::FromDays(1)); + } + + EXPECT_EQ( + 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + clock()->Advance(base::TimeDelta::FromDays(1)); + EXPECT_EQ( + 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + + auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>(); + storage()->ClearData(base::Time::Min(), base::Time::Max(), null_filter); + EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty()); + + CloseDatabase(); + + // Verify that everything is deleted. + sql::Database raw_db; + EXPECT_TRUE(raw_db.Open(db_path())); + + size_t conversion_rows; + size_t impression_rows; + sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows); + sql::test::CountTableRows(&raw_db, "impressions", &impression_rows); + + EXPECT_EQ(0u, conversion_rows); + EXPECT_EQ(0u, impression_rows); +} + +TEST_F(ConversionStorageSqlTest, MaxImpressionsPerOrigin) { + OpenDatabase(); + delegate()->set_max_impressions_per_origin(2); + storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build()); + storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build()); + storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build()); + EXPECT_EQ( + 2, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + + CloseDatabase(); + sql::Database raw_db; + EXPECT_TRUE(raw_db.Open(db_path())); + size_t impression_rows; + sql::test::CountTableRows(&raw_db, "impressions", &impression_rows); + EXPECT_EQ(2u, impression_rows); +} + +TEST_F(ConversionStorageSqlTest, MaxConversionsPerOrigin) { + OpenDatabase(); + delegate()->set_max_conversions_per_origin(2); + storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build()); + EXPECT_EQ( + 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + EXPECT_EQ( + 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + EXPECT_EQ( + 0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + + CloseDatabase(); + sql::Database raw_db; + EXPECT_TRUE(raw_db.Open(db_path())); + size_t conversion_rows; + sql::test::CountTableRows(&raw_db, "conversions", &conversion_rows); + EXPECT_EQ(2u, conversion_rows); +} + } // namespace content diff --git a/chromium/content/browser/conversions/conversion_storage_unittest.cc b/chromium/content/browser/conversions/conversion_storage_unittest.cc index c67a69ef253..b8089b96a06 100644 --- a/chromium/content/browser/conversions/conversion_storage_unittest.cc +++ b/chromium/content/browser/conversions/conversion_storage_unittest.cc @@ -4,13 +4,17 @@ #include "content/browser/conversions/conversion_storage.h" +#include <functional> #include <list> #include <memory> #include <tuple> +#include <utility> #include <vector> +#include "base/callback.h" #include "base/files/scoped_temp_dir.h" #include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" #include "base/test/simple_test_clock.h" #include "content/browser/conversions/conversion_report.h" #include "content/browser/conversions/conversion_storage_sql.h" @@ -33,45 +37,12 @@ const int kReportTime = 5; using AttributionCredits = std::list<int>; -} // namespace - -// Mock delegate which provides default behavior and delays reports by a fixed -// time from impression. -class MockStorageDelegate : public ConversionStorage::Delegate { - public: - MockStorageDelegate() = default; - ~MockStorageDelegate() override = default; - - // ConversionStorage::Delegate - void ProcessNewConversionReports( - std::vector<ConversionReport>* reports) override { - for (auto& report : *reports) { - report.report_time = report.impression.impression_time() + - base::TimeDelta::FromMilliseconds(kReportTime); - - // If attribution credits were provided, associate them with reports - // in order. - if (!attribution_credits_.empty()) { - report.attribution_credit = attribution_credits_.front(); - attribution_credits_.pop_front(); - } - } - } - - int GetMaxConversionsPerImpression() const override { - return kMaxConversions; - } - - void AddCredits(AttributionCredits credits) { - // Add all credits to our list in order. - attribution_credits_.splice(attribution_credits_.end(), credits); - } +base::RepeatingCallback<bool(const url::Origin&)> GetMatcher( + const url::Origin& to_delete) { + return base::BindRepeating(std::equal_to<url::Origin>(), to_delete); +} - private: - // List of attribution credits the mock delegate should associate with - // reports. - AttributionCredits attribution_credits_; -}; +} // namespace // Unit test suite for the ConversionStorage interface. All ConversionStorage // implementations (including fakes) should be able to re-use this test suite. @@ -79,8 +50,12 @@ class ConversionStorageTest : public testing::Test { public: ConversionStorageTest() { EXPECT_TRUE(dir_.CreateUniqueTempDir()); - storage_ = std::make_unique<ConversionStorageSql>(dir_.GetPath(), - &delegate_, &clock_); + auto delegate = std::make_unique<ConfigurableStorageDelegate>(); + delegate->set_report_time_ms(kReportTime); + delegate->set_max_conversions_per_impression(kMaxConversions); + delegate_ = delegate.get(); + storage_ = std::make_unique<ConversionStorageSql>( + dir_.GetPath(), std::move(delegate), &clock_); EXPECT_TRUE(storage_->Initialize()); } @@ -104,20 +79,43 @@ class ConversionStorageTest : public testing::Test { } void AddAttributionCredits(AttributionCredits credits) { - delegate_.AddCredits(credits); + delegate_->AddCredits(credits); } base::SimpleTestClock* clock() { return &clock_; } ConversionStorage* storage() { return storage_.get(); } + ConfigurableStorageDelegate* delegate() { return delegate_; } + private: - MockStorageDelegate delegate_; + ConfigurableStorageDelegate* delegate_; base::SimpleTestClock clock_; base::ScopedTempDir dir_; std::unique_ptr<ConversionStorage> storage_; }; +TEST_F(ConversionStorageTest, ImpressionStoredAndRetrieved_ValuesIdentical) { + auto impression = ImpressionBuilder(clock()->Now()).Build(); + storage()->StoreImpression(impression); + std::vector<StorableImpression> stored_impressions = + storage()->GetActiveImpressions(); + EXPECT_EQ(1u, stored_impressions.size()); + + // Verify that each field was stored as expected. + EXPECT_EQ(impression.impression_data(), + stored_impressions[0].impression_data()); + EXPECT_EQ(impression.impression_origin(), + stored_impressions[0].impression_origin()); + EXPECT_EQ(impression.conversion_origin(), + stored_impressions[0].conversion_origin()); + EXPECT_EQ(impression.reporting_origin(), + stored_impressions[0].reporting_origin()); + EXPECT_EQ(impression.impression_time(), + stored_impressions[0].impression_time()); + EXPECT_EQ(impression.expiry_time(), stored_impressions[0].expiry_time()); +} + TEST_F(ConversionStorageTest, GetWithNoMatchingImpressions_NoImpressionsReturned) { EXPECT_EQ( @@ -547,4 +545,279 @@ TEST_F(ConversionStorageTest, EXPECT_TRUE(ReportsEqual(expected_reports, actual_reports)); } +TEST_F(ConversionStorageTest, MaxImpressionsPerOrigin) { + delegate()->set_max_impressions_per_origin(2); + storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build()); + storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build()); + storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build()); + EXPECT_EQ( + 2, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); +} + +TEST_F(ConversionStorageTest, MaxConversionsPerOrigin) { + delegate()->set_max_conversions_per_origin(2); + storage()->StoreImpression(ImpressionBuilder(clock()->Now()).Build()); + EXPECT_EQ( + 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + EXPECT_EQ( + 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + EXPECT_EQ( + 0, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); +} + +TEST_F(ConversionStorageTest, ClearDataWithNoMatch_NoDelete) { + base::Time now = clock()->Now(); + auto impression = ImpressionBuilder(now).Build(); + storage()->StoreImpression(impression); + storage()->ClearData( + now, now, GetMatcher(url::Origin::Create(GURL("https://no-match.com")))); + EXPECT_EQ( + 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); +} + +TEST_F(ConversionStorageTest, ClearDataOutsideRange_NoDelete) { + base::Time now = clock()->Now(); + auto impression = ImpressionBuilder(now).Build(); + storage()->StoreImpression(impression); + + storage()->ClearData(now + base::TimeDelta::FromMinutes(10), + now + base::TimeDelta::FromMinutes(20), + GetMatcher(impression.impression_origin())); + EXPECT_EQ( + 1, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); +} + +TEST_F(ConversionStorageTest, ClearDataImpression) { + base::Time now = clock()->Now(); + { + auto impression = ImpressionBuilder(now).Build(); + storage()->StoreImpression(impression); + storage()->ClearData(now, now + base::TimeDelta::FromMinutes(20), + GetMatcher(impression.impression_origin())); + EXPECT_EQ(0, storage()->MaybeCreateAndStoreConversionReports( + DefaultConversion())); + } + { + auto impression = ImpressionBuilder(now).Build(); + storage()->StoreImpression(impression); + storage()->ClearData(now, now + base::TimeDelta::FromMinutes(20), + GetMatcher(impression.reporting_origin())); + EXPECT_EQ(0, storage()->MaybeCreateAndStoreConversionReports( + DefaultConversion())); + } + { + auto impression = ImpressionBuilder(now).Build(); + storage()->StoreImpression(impression); + storage()->ClearData(now, now + base::TimeDelta::FromMinutes(20), + GetMatcher(impression.conversion_origin())); + EXPECT_EQ(0, storage()->MaybeCreateAndStoreConversionReports( + DefaultConversion())); + } +} + +TEST_F(ConversionStorageTest, ClearDataImpressionConversion) { + base::Time now = clock()->Now(); + auto impression = ImpressionBuilder(now).Build(); + auto conversion = DefaultConversion(); + + storage()->StoreImpression(impression); + EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion)); + + storage()->ClearData(now - base::TimeDelta::FromMinutes(20), + now + base::TimeDelta::FromMinutes(20), + GetMatcher(impression.impression_origin())); + + EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty()); +} + +// The null filter should match all origins. +TEST_F(ConversionStorageTest, ClearDataNullFilter) { + base::Time now = clock()->Now(); + + for (int i = 0; i < 10; i++) { + auto origin = + url::Origin::Create(GURL(base::StringPrintf("https://%d.com/", i))); + storage()->StoreImpression(ImpressionBuilder(now) + .SetExpiry(base::TimeDelta::FromDays(30)) + .SetImpressionOrigin(origin) + .SetReportingOrigin(origin) + .SetConversionOrigin(origin) + .Build()); + clock()->Advance(base::TimeDelta::FromDays(1)); + } + + // Convert half of them now, half after another day. + for (int i = 0; i < 5; i++) { + auto origin = + url::Origin::Create(GURL(base::StringPrintf("https://%d.com/", i))); + StorableConversion conversion("1", origin, origin); + EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion)); + } + clock()->Advance(base::TimeDelta::FromDays(1)); + for (int i = 5; i < 10; i++) { + auto origin = + url::Origin::Create(GURL(base::StringPrintf("https://%d.com/", i))); + StorableConversion conversion("1", origin, origin); + EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion)); + } + + auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>(); + storage()->ClearData(clock()->Now(), clock()->Now(), null_filter); + EXPECT_EQ(5u, storage()->GetConversionsToReport(base::Time::Max()).size()); +} + +TEST_F(ConversionStorageTest, ClearDataWithImpressionOutsideRange) { + base::Time start = clock()->Now(); + auto impression = + ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build(); + auto conversion = DefaultConversion(); + + storage()->StoreImpression(impression); + + EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion)); + storage()->ClearData(clock()->Now(), clock()->Now(), + GetMatcher(impression.impression_origin())); + EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty()); +} + +// Deletions with time range between the impression and conversion should not +// delete anything, unless the time range intersects one of the events. +TEST_F(ConversionStorageTest, ClearDataRangeBetweenEvents) { + base::Time start = clock()->Now(); + auto impression = + ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build(); + auto conversion = DefaultConversion(); + + std::vector<ConversionReport> expected_reports = { + GetExpectedReport(impression, conversion, 0)}; + + storage()->StoreImpression(impression); + + clock()->Advance(base::TimeDelta::FromDays(1)); + + EXPECT_EQ(1, storage()->MaybeCreateAndStoreConversionReports(conversion)); + + storage()->ClearData(start + base::TimeDelta::FromMinutes(1), + start + base::TimeDelta::FromMinutes(10), + GetMatcher(impression.impression_origin())); + + std::vector<ConversionReport> actual_reports = + storage()->GetConversionsToReport(base::Time::Max()); + EXPECT_TRUE(ReportsEqual(expected_reports, actual_reports)); +} +// Test that only a subset of impressions / conversions are deleted with +// multiple impressions per conversion, if only a subset of impressions match. +TEST_F(ConversionStorageTest, ClearDataWithMultiTouch) { + base::Time start = clock()->Now(); + auto impression1 = + ImpressionBuilder(start).SetExpiry(base::TimeDelta::FromDays(30)).Build(); + storage()->StoreImpression(impression1); + + clock()->Advance(base::TimeDelta::FromDays(1)); + auto impression2 = ImpressionBuilder(clock()->Now()) + .SetExpiry(base::TimeDelta::FromDays(30)) + .Build(); + auto impression3 = ImpressionBuilder(clock()->Now()) + .SetExpiry(base::TimeDelta::FromDays(30)) + .Build(); + + storage()->StoreImpression(impression2); + storage()->StoreImpression(impression3); + + EXPECT_EQ( + 3, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + + // Only the first impression should overlap with this time range, but all the + // impressions should share the origin. + storage()->ClearData(start, start, + GetMatcher(impression1.impression_origin())); + EXPECT_EQ(2u, storage()->GetConversionsToReport(base::Time::Max()).size()); +} + +// Attribution occurs at conversion time, not report time, so deleted +// impressions should not adjust credit allocation. +TEST_F(ConversionStorageTest, ClearData_AttributionUnaffected) { + auto impression1 = ImpressionBuilder(clock()->Now()) + .SetData("xyz") + .SetExpiry(base::TimeDelta::FromDays(30)) + .Build(); + auto impression2 = ImpressionBuilder(clock()->Now()) + .SetData("abc") + .SetExpiry(base::TimeDelta::FromDays(30)) + .Build(); + auto conversion = DefaultConversion(); + storage()->StoreImpression(impression1); + storage()->StoreImpression(impression2); + std::vector<ConversionReport> expected_reports = { + GetExpectedReport(impression1, conversion, 0), + GetExpectedReport(impression2, conversion, 0)}; + + clock()->Advance(base::TimeDelta::FromDays(1)); + auto impression3 = ImpressionBuilder(clock()->Now()) + .SetExpiry(base::TimeDelta::FromDays(30)) + .Build(); + storage()->StoreImpression(impression3); + base::Time delete_time = clock()->Now(); + clock()->Advance(base::TimeDelta::FromDays(1)); + + AddAttributionCredits({100, 0, 0}); + EXPECT_EQ(3, storage()->MaybeCreateAndStoreConversionReports(conversion)); + + // The last impression should be deleted, but the conversion shouldn't be. + storage()->ClearData(delete_time, delete_time, + GetMatcher(impression1.impression_origin())); + std::vector<ConversionReport> actual_reports = + storage()->GetConversionsToReport(base::Time::Max()); + EXPECT_TRUE(ReportsEqual(expected_reports, actual_reports)); +} + +// The max time range with a null filter should delete everything. +TEST_F(ConversionStorageTest, DeleteAll) { + base::Time start = clock()->Now(); + for (int i = 0; i < 10; i++) { + auto impression = ImpressionBuilder(start) + .SetExpiry(base::TimeDelta::FromDays(30)) + .Build(); + storage()->StoreImpression(impression); + clock()->Advance(base::TimeDelta::FromDays(1)); + } + + EXPECT_EQ( + 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + clock()->Advance(base::TimeDelta::FromDays(1)); + EXPECT_EQ( + 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + + auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>(); + storage()->ClearData(base::Time::Min(), base::Time::Max(), null_filter); + + // Verify that everything is deleted. + EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty()); +} + +// Same as the above test, but uses base::Time() instead of base::Time::Min() +// for delete_begin, which should yield the same behavior. +TEST_F(ConversionStorageTest, DeleteAllNullDeleteBegin) { + base::Time start = clock()->Now(); + for (int i = 0; i < 10; i++) { + auto impression = ImpressionBuilder(start) + .SetExpiry(base::TimeDelta::FromDays(30)) + .Build(); + storage()->StoreImpression(impression); + clock()->Advance(base::TimeDelta::FromDays(1)); + } + + EXPECT_EQ( + 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + clock()->Advance(base::TimeDelta::FromDays(1)); + EXPECT_EQ( + 10, storage()->MaybeCreateAndStoreConversionReports(DefaultConversion())); + + auto null_filter = base::RepeatingCallback<bool(const url::Origin&)>(); + storage()->ClearData(base::Time(), base::Time::Max(), null_filter); + + // Verify that everything is deleted. + EXPECT_TRUE(storage()->GetConversionsToReport(base::Time::Max()).empty()); +} + } // namespace content diff --git a/chromium/content/browser/conversions/conversion_test_utils.cc b/chromium/content/browser/conversions/conversion_test_utils.cc index 7209814154c..10f95f365bf 100644 --- a/chromium/content/browser/conversions/conversion_test_utils.cc +++ b/chromium/content/browser/conversions/conversion_test_utils.cc @@ -4,25 +4,121 @@ #include "content/browser/conversions/conversion_test_utils.h" +#include <limits.h> + #include <tuple> +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/run_loop.h" +#include "base/task_runner_util.h" +#include "base/test/bind_test_util.h" #include "url/gurl.h" namespace content { namespace { -const char kDefaultImpressionOrigin[] = "https:/impression.test/"; -const char kDefaultConversionOrigin[] = "https:/conversion.test/"; -const char kDefaultReportOrigin[] = "https:/report.test/"; +const char kDefaultImpressionOrigin[] = "https://impression.test/"; +const char kDefaultConversionOrigin[] = "https://conversion.test/"; +const char kDefaultReportOrigin[] = "https://report.test/"; // Default expiry time for impressions for testing. const int64_t kExpiryTime = 30; } // namespace -int EmptyStorageDelegate::GetMaxConversionsPerImpression() const { - return 1; +ConfigurableStorageDelegate::ConfigurableStorageDelegate() = default; +ConfigurableStorageDelegate::~ConfigurableStorageDelegate() = default; + +void ConfigurableStorageDelegate::ProcessNewConversionReports( + std::vector<ConversionReport>* reports) { + // Note: reports are ordered by impression time, descending. + for (auto& report : *reports) { + report.report_time = report.impression.impression_time() + + base::TimeDelta::FromMilliseconds(report_time_ms_); + + // If attribution credits were provided, associate them with reports + // in order. + if (!attribution_credits_.empty()) { + report.attribution_credit = attribution_credits_.front(); + attribution_credits_.pop_front(); + } + } +} +int ConfigurableStorageDelegate::GetMaxConversionsPerImpression() const { + return max_conversions_per_impression_; +} +int ConfigurableStorageDelegate::GetMaxImpressionsPerOrigin() const { + return max_impressions_per_origin_; +} +int ConfigurableStorageDelegate::GetMaxConversionsPerOrigin() const { + return max_conversions_per_origin_; +} + +ConversionManager* TestManagerProvider::GetManager( + WebContents* web_contents) const { + return manager_; +} + +TestConversionManager::TestConversionManager() = default; + +TestConversionManager::~TestConversionManager() = default; + +void TestConversionManager::HandleImpression( + const StorableImpression& impression) { + num_impressions_++; +} + +void TestConversionManager::HandleConversion( + const StorableConversion& conversion) { + num_conversions_++; +} + +void TestConversionManager::GetActiveImpressionsForWebUI( + base::OnceCallback<void(std::vector<StorableImpression>)> callback) { + std::move(callback).Run(impressions_); +} + +void TestConversionManager::GetReportsForWebUI( + base::OnceCallback<void(std::vector<ConversionReport>)> callback, + base::Time max_report_time) { + std::move(callback).Run(reports_); +} + +void TestConversionManager::SendReportsForWebUI(base::OnceClosure done) { + reports_.clear(); + std::move(done).Run(); +} + +const ConversionPolicy& TestConversionManager::GetConversionPolicy() const { + return policy_; +} + +void TestConversionManager::ClearData( + base::Time delete_begin, + base::Time delete_end, + base::RepeatingCallback<bool(const url::Origin&)> filter, + base::OnceClosure done) { + impressions_.clear(); + reports_.clear(); + std::move(done).Run(); +} + +void TestConversionManager::SetActiveImpressionsForWebUI( + std::vector<StorableImpression> impressions) { + impressions_ = std::move(impressions); +} + +void TestConversionManager::SetReportsForWebUI( + std::vector<ConversionReport> reports) { + reports_ = std::move(reports); +} + +void TestConversionManager::Reset() { + num_impressions_ = 0u; + num_conversions_ = 0u; } // Builds an impression with default values. This is done as a builder because @@ -82,6 +178,23 @@ StorableConversion DefaultConversion() { return conversion; } +// Custom comparator for StorableImpressions that does not take impression id's +// into account. +testing::AssertionResult ImpressionsEqual(const StorableImpression& expected, + const StorableImpression& actual) { + const auto tie = [](const StorableImpression& impression) { + return std::make_tuple( + impression.impression_data(), impression.impression_origin(), + impression.conversion_origin(), impression.reporting_origin(), + impression.impression_time(), impression.expiry_time()); + }; + + if (tie(expected) != tie(actual)) { + return testing::AssertionFailure(); + } + return testing::AssertionSuccess(); +} + // Custom comparator for comparing two vectors of conversion reports. Does not // compare impression and conversion id's as they are set by the underlying // sqlite db and should not be tested. @@ -114,4 +227,23 @@ testing::AssertionResult ReportsEqual( return testing::AssertionSuccess(); } +std::vector<ConversionReport> GetConversionsToReportForTesting( + ConversionManagerImpl* manager, + base::Time max_report_time) { + base::RunLoop run_loop; + std::vector<ConversionReport> conversion_reports; + base::PostTaskAndReplyWithResult( + manager->storage_task_runner_.get(), FROM_HERE, + base::BindOnce(&ConversionStorage::GetConversionsToReport, + base::Unretained(manager->storage_.get()), + max_report_time), + base::BindOnce(base::BindLambdaForTesting( + [&](std::vector<ConversionReport> reports) { + conversion_reports = std::move(reports); + run_loop.Quit(); + }))); + run_loop.Run(); + return conversion_reports; +} + } // namespace content diff --git a/chromium/content/browser/conversions/conversion_test_utils.h b/chromium/content/browser/conversions/conversion_test_utils.h index 20b0c378cfd..80e569b5cfb 100644 --- a/chromium/content/browser/conversions/conversion_test_utils.h +++ b/chromium/content/browser/conversions/conversion_test_utils.h @@ -5,10 +5,15 @@ #ifndef CONTENT_BROWSER_CONVERSIONS_CONVERSION_TEST_UTILS_H_ #define CONTENT_BROWSER_CONVERSIONS_CONVERSION_TEST_UTILS_H_ +#include <list> #include <string> #include <vector> +#include "base/memory/scoped_refptr.h" +#include "base/sequenced_task_runner.h" #include "base/time/time.h" +#include "content/browser/conversions/conversion_manager.h" +#include "content/browser/conversions/conversion_manager_impl.h" #include "content/browser/conversions/conversion_report.h" #include "content/browser/conversions/conversion_storage.h" #include "content/browser/conversions/storable_conversion.h" @@ -18,16 +23,105 @@ namespace content { -class EmptyStorageDelegate : public ConversionStorage::Delegate { +class ConfigurableStorageDelegate : public ConversionStorage::Delegate { public: - EmptyStorageDelegate() = default; - ~EmptyStorageDelegate() override = default; + using AttributionCredits = std::list<int>; + ConfigurableStorageDelegate(); + ~ConfigurableStorageDelegate() override; // ConversionStorage::Delegate void ProcessNewConversionReports( - std::vector<ConversionReport>* reports) override {} - + std::vector<ConversionReport>* reports) override; int GetMaxConversionsPerImpression() const override; + int GetMaxImpressionsPerOrigin() const override; + int GetMaxConversionsPerOrigin() const override; + + void set_max_conversions_per_impression(int max) { + max_conversions_per_impression_ = max; + } + + void set_max_impressions_per_origin(int max) { + max_impressions_per_origin_ = max; + } + + void set_max_conversions_per_origin(int max) { + max_conversions_per_origin_ = max; + } + + void set_report_time_ms(int report_time_ms) { + report_time_ms_ = report_time_ms; + } + + void AddCredits(AttributionCredits credits) { + // Add all credits to our list in order. + attribution_credits_.splice(attribution_credits_.end(), credits); + } + + private: + int max_conversions_per_impression_ = INT_MAX; + int max_impressions_per_origin_ = INT_MAX; + int max_conversions_per_origin_ = INT_MAX; + + int report_time_ms_ = 0; + + // List of attribution credits the test delegate should associate with + // reports. + AttributionCredits attribution_credits_; +}; + +// Test manager provider which can be used to inject a fake ConversionManager. +class TestManagerProvider : public ConversionManager::Provider { + public: + explicit TestManagerProvider(ConversionManager* manager) + : manager_(manager) {} + ~TestManagerProvider() override = default; + + ConversionManager* GetManager(WebContents* web_contents) const override; + + private: + ConversionManager* manager_ = nullptr; +}; + +// Test ConversionManager which can be injected into tests to monitor calls to a +// ConversionManager instance. +class TestConversionManager : public ConversionManager { + public: + TestConversionManager(); + ~TestConversionManager() override; + + // ConversionManager: + void HandleImpression(const StorableImpression& impression) override; + void HandleConversion(const StorableConversion& conversion) override; + void GetActiveImpressionsForWebUI( + base::OnceCallback<void(std::vector<StorableImpression>)> callback) + override; + void GetReportsForWebUI( + base::OnceCallback<void(std::vector<ConversionReport>)> callback, + base::Time max_report_time) override; + void SendReportsForWebUI(base::OnceClosure done) override; + const ConversionPolicy& GetConversionPolicy() const override; + void ClearData(base::Time delete_begin, + base::Time delete_end, + base::RepeatingCallback<bool(const url::Origin&)> filter, + base::OnceClosure done) override; + + void SetActiveImpressionsForWebUI( + std::vector<StorableImpression> impressions); + void SetReportsForWebUI(std::vector<ConversionReport> reports); + + // Resets all counters on this. + void Reset(); + + size_t num_impressions() const { return num_impressions_; } + size_t num_conversions() const { return num_conversions_; } + + private: + ConversionPolicy policy_; + size_t num_impressions_ = 0; + size_t num_conversions_ = 0; + + std::vector<StorableImpression> impressions_; + std::vector<ConversionReport> reports_; }; // Helper class to construct a StorableImpression for tests using default data. @@ -35,7 +129,7 @@ class EmptyStorageDelegate : public ConversionStorage::Delegate { // builder pattern. class ImpressionBuilder { public: - ImpressionBuilder(base::Time time); + explicit ImpressionBuilder(base::Time time); ~ImpressionBuilder(); ImpressionBuilder& SetExpiry(base::TimeDelta delta); @@ -63,10 +157,17 @@ class ImpressionBuilder { // impressions created by ImpressionBuilder. StorableConversion DefaultConversion(); +testing::AssertionResult ImpressionsEqual(const StorableImpression& expected, + const StorableImpression& actual); + testing::AssertionResult ReportsEqual( const std::vector<ConversionReport>& expected, const std::vector<ConversionReport>& actual); +std::vector<ConversionReport> GetConversionsToReportForTesting( + ConversionManagerImpl* manager, + base::Time max_report_time); + } // namespace content #endif // CONTENT_BROWSER_CONVERSIONS_CONVERSION_TEST_UTILS_H_ diff --git a/chromium/content/browser/conversions/conversions_browsertest.cc b/chromium/content/browser/conversions/conversions_browsertest.cc new file mode 100644 index 00000000000..dedcf5a42ec --- /dev/null +++ b/chromium/content/browser/conversions/conversions_browsertest.cc @@ -0,0 +1,254 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <memory> + +#include "base/command_line.h" +#include "base/test/scoped_feature_list.h" +#include "content/public/common/content_features.h" +#include "content/public/common/content_switches.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/content_browser_test.h" +#include "content/public/test/content_browser_test_utils.h" +#include "content/public/test/test_navigation_observer.h" +#include "content/shell/browser/shell.h" +#include "net/dns/mock_host_resolver.h" +#include "net/test/embedded_test_server/controllable_http_response.h" +#include "net/test/embedded_test_server/default_handlers.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "net/test/embedded_test_server/request_handler_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace content { + +namespace { + +// Waits for the a given |report_url| to be received by the test server. Wraps a +// ControllableHttpResponse so that it can wait for the server request in a +// thread-safe manner. Therefore, these must be registered prior to |server| +// starting. +struct ExpectedReportWaiter { + // ControllableHTTPResponses can only wait for relative urls, so only supply + // the path + query. + ExpectedReportWaiter(const GURL& report_url, net::EmbeddedTestServer* server) + : expected_url(report_url), + response(std::make_unique<net::test_server::ControllableHttpResponse>( + server, + report_url.path() + "?" + report_url.query())) {} + + GURL expected_url; + std::unique_ptr<net::test_server::ControllableHttpResponse> response; + + // Returns the url for the HttpRequest handled by |response|. This returns a + // URL formatted with the host defined in the headers. This would not match + // |expected_url| if the host for report url was not set properly. + GURL WaitForRequestUrl() { + if (!response->http_request()) + response->WaitForRequest(); + + // The embedded test server resolves all urls to 127.0.0.1, so get the real + // request host from the request headers. + const net::test_server::HttpRequest& request = *response->http_request(); + DCHECK(request.headers.find("Host") != request.headers.end()); + const GURL& request_url = request.GetURL(); + GURL header_url = GURL("https://" + request.headers.at("Host")); + std::string host = header_url.host(); + GURL::Replacements replace_host; + replace_host.SetHostStr(host); + + // Clear the port as it is assigned by the EmbeddedTestServer at runtime. + replace_host.SetPortStr(""); + return request_url.ReplaceComponents(replace_host); + } +}; + +} // namespace + +class ConversionsBrowserTest : public ContentBrowserTest { + public: + ConversionsBrowserTest() { + feature_list_.InitAndEnableFeature(features::kConversionMeasurement); + } + + void SetUpCommandLine(base::CommandLine* command_line) override { + command_line->AppendSwitch(switches::kConversionsDebugMode); + } + + void SetUpOnMainThread() override { + host_resolver()->AddRule("*", "127.0.0.1"); + + https_server_ = std::make_unique<net::EmbeddedTestServer>( + net::EmbeddedTestServer::TYPE_HTTPS); + https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); + net::test_server::RegisterDefaultHandlers(https_server_.get()); + https_server_->ServeFilesFromSourceDirectory("content/test/data"); + SetupCrossSiteRedirector(https_server_.get()); + } + + WebContents* web_contents() { return shell()->web_contents(); } + + net::EmbeddedTestServer* https_server() { return https_server_.get(); } + + private: + base::test::ScopedFeatureList feature_list_; + std::unique_ptr<net::EmbeddedTestServer> https_server_; +}; + +IN_PROC_BROWSER_TEST_F(ConversionsBrowserTest, + ImpressionConversion_ReportSent) { + // Expected reports must be registered before the server starts. + ExpectedReportWaiter expected_report( + GURL( + "https://a.test/.well-known/" + "register-conversion?impression-data=1&conversion-data=7&credit=100"), + https_server()); + ASSERT_TRUE(https_server()->Start()); + + GURL impression_url = https_server()->GetURL( + "a.test", "/conversions/page_with_impression_creator.html"); + EXPECT_TRUE(NavigateToURL(web_contents(), impression_url)); + + // Create an anchor tag with impression attributes and click the link. By + // default the target is set to "_top". + GURL conversion_url = https_server()->GetURL( + "b.test", "/conversions/page_with_conversion_redirect.html"); + EXPECT_TRUE( + ExecJs(web_contents(), + JsReplace(R"( + createImpressionTag("link" /* id */, + $1 /* url */, + "1" /* impression data */, + $2 /* conversion_destination */);)", + conversion_url, url::Origin::Create(conversion_url)))); + + TestNavigationObserver observer(web_contents()); + EXPECT_TRUE(ExecJs(shell(), "simulateClick('link');")); + observer.Wait(); + + // Register a conversion with the original page as the reporting origin. + EXPECT_TRUE( + ExecJs(web_contents(), JsReplace("registerConversionForOrigin(7, $1)", + url::Origin::Create(impression_url)))); + + EXPECT_EQ(expected_report.expected_url, expected_report.WaitForRequestUrl()); +} + +IN_PROC_BROWSER_TEST_F(ConversionsBrowserTest, + ImpressionFromCrossOriginSubframe_ReportSent) { + ExpectedReportWaiter expected_report( + GURL( + "https://a.test/.well-known/" + "register-conversion?impression-data=1&conversion-data=7&credit=100"), + https_server()); + ASSERT_TRUE(https_server()->Start()); + + GURL page_url = https_server()->GetURL("a.test", "/page_with_iframe.html"); + EXPECT_TRUE(NavigateToURL(web_contents(), page_url)); + + GURL subframe_url = https_server()->GetURL( + "c.test", "/conversions/page_with_impression_creator.html"); + EXPECT_TRUE(ExecJs(shell(), R"( + let frame= document.getElementById('test_iframe'); + frame.setAttribute('allow', 'conversion-measurement');)")); + NavigateIframeToURL(web_contents(), "test_iframe", subframe_url); + RenderFrameHost* subframe = ChildFrameAt(web_contents()->GetMainFrame(), 0); + + // Create an impression tag in the subframe and target a popup window. + GURL conversion_url = https_server()->GetURL( + "b.test", "/conversions/page_with_conversion_redirect.html"); + EXPECT_TRUE(ExecJs(subframe, JsReplace(R"( + createImpressionTagWithTarget("link" /* id */, + $1 /* url */, + "1" /* impression data */, + $2 /* conversion_destination */, + "new_frame" /* target */);)", + conversion_url, + url::Origin::Create(conversion_url)))); + + ShellAddedObserver new_shell_observer; + TestNavigationObserver observer(nullptr); + observer.StartWatchingNewWebContents(); + EXPECT_TRUE(ExecJs(subframe, "simulateClick('link');")); + WebContents* popup_contents = new_shell_observer.GetShell()->web_contents(); + observer.Wait(); + + // Register a conversion with the original page as the reporting origin. + EXPECT_TRUE( + ExecJs(popup_contents, JsReplace("registerConversionForOrigin(7, $1)", + url::Origin::Create(page_url)))); + + EXPECT_EQ(expected_report.expected_url, expected_report.WaitForRequestUrl()); +} + +IN_PROC_BROWSER_TEST_F( + ConversionsBrowserTest, + MultipleImpressionsPerConversion_ReportsSentWithAttribution) { + std::vector<ExpectedReportWaiter> expected_reports; + expected_reports.emplace_back( + GURL("https://d.test/.well-known/" + "register-conversion?impression-data=1&conversion-data=7&credit=0"), + https_server()); + expected_reports.emplace_back( + GURL( + "https://d.test/.well-known/" + "register-conversion?impression-data=2&conversion-data=7&credit=100"), + https_server()); + ASSERT_TRUE(https_server()->Start()); + + GURL first_impression_url = https_server()->GetURL( + "a.test", "/conversions/page_with_impression_creator.html"); + EXPECT_TRUE(NavigateToURL(web_contents(), first_impression_url)); + + GURL second_impression_url = https_server()->GetURL( + "c.test", "/conversions/page_with_impression_creator.html"); + Shell* shell2 = + Shell::CreateNewWindow(shell()->web_contents()->GetBrowserContext(), + GURL(), nullptr, gfx::Size(100, 100)); + EXPECT_TRUE(NavigateToURL(shell2->web_contents(), second_impression_url)); + + // Register impressions from both windows. + GURL conversion_url = https_server()->GetURL( + "b.test", "/conversions/page_with_conversion_redirect.html"); + url::Origin reporting_origin = + url::Origin::Create(https_server()->GetURL("d.test", "/")); + std::string impression_js = R"( + createImpressionTagWithReporting("link" /* id */, + $1 /* url */, + $2 /* impression data */, + $3 /* conversion_destination */, + $4 /* reporting_origin */);)"; + + TestNavigationObserver first_nav_observer(shell()->web_contents()); + EXPECT_TRUE( + ExecJs(shell(), + JsReplace(impression_js, conversion_url, "1" /* impression_data */, + url::Origin::Create(conversion_url), reporting_origin))); + EXPECT_TRUE(ExecJs(shell(), "simulateClick('link');")); + first_nav_observer.Wait(); + + TestNavigationObserver second_nav_observer(shell2->web_contents()); + EXPECT_TRUE( + ExecJs(shell2, + JsReplace(impression_js, conversion_url, "2" /* impression_data */, + url::Origin::Create(conversion_url), reporting_origin))); + EXPECT_TRUE(ExecJs(shell2, "simulateClick('link');")); + second_nav_observer.Wait(); + + // Register a conversion after both impressions have been registered. + EXPECT_TRUE(ExecJs(shell2, JsReplace("registerConversionForOrigin(7, $1)", + reporting_origin))); + + for (auto& report : expected_reports) { + if (!report.response->http_request()) + report.response->WaitForRequest(); + EXPECT_EQ(report.expected_url, report.WaitForRequestUrl()); + } +} + +} // namespace content diff --git a/chromium/content/browser/conversions/impression_declaration_browsertest.cc b/chromium/content/browser/conversions/impression_declaration_browsertest.cc new file mode 100644 index 00000000000..9e12221ca75 --- /dev/null +++ b/chromium/content/browser/conversions/impression_declaration_browsertest.cc @@ -0,0 +1,478 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <stdint.h> +#include <memory> + +#include "base/bind.h" +#include "base/run_loop.h" +#include "base/test/scoped_feature_list.h" +#include "base/time/time.h" +#include "content/browser/web_contents/web_contents_impl.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/common/content_features.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/content_browser_test.h" +#include "content/public/test/content_browser_test_utils.h" +#include "content/shell/browser/shell.h" +#include "net/dns/mock_host_resolver.h" +#include "net/test/embedded_test_server/default_handlers.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "url/gurl.h" + +namespace content { + +// WebContentsObserver that waits until an impression is available on a +// navigation handle for a finished navigation. +class ImpressionObserver : public WebContentsObserver { + public: + explicit ImpressionObserver(WebContents* contents) + : WebContentsObserver(contents) {} + + // WebContentsObserver + void DidFinishNavigation(NavigationHandle* navigation_handle) override { + if (!navigation_handle->GetImpression()) { + if (waiting_for_null_impression_) + impression_loop_.Quit(); + return; + } + + last_impression_ = *(navigation_handle->GetImpression()); + + if (!waiting_for_null_impression_) + impression_loop_.Quit(); + } + + const Impression& last_impression() { return *last_impression_; } + + const Impression& WaitForImpression() { + impression_loop_.Run(); + return last_impression(); + } + + bool WaitForNavigationWithNoImpression() { + waiting_for_null_impression_ = true; + impression_loop_.Run(); + waiting_for_null_impression_ = false; + return true; + } + + private: + base::Optional<Impression> last_impression_; + bool waiting_for_null_impression_ = false; + base::RunLoop impression_loop_; +}; + +class ImpressionDeclarationBrowserTest : public ContentBrowserTest { + public: + ImpressionDeclarationBrowserTest() { + feature_list_.InitAndEnableFeature(features::kConversionMeasurement); + } + + void SetUpOnMainThread() override { + host_resolver()->AddRule("*", "127.0.0.1"); + embedded_test_server()->ServeFilesFromSourceDirectory( + "content/test/data/conversions"); + embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data"); + content::SetupCrossSiteRedirector(embedded_test_server()); + ASSERT_TRUE(embedded_test_server()->Start()); + + https_server_ = std::make_unique<net::EmbeddedTestServer>( + net::EmbeddedTestServer::TYPE_HTTPS); + https_server_->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); + net::test_server::RegisterDefaultHandlers(https_server_.get()); + https_server_->ServeFilesFromSourceDirectory( + "content/test/data/conversions"); + https_server_->ServeFilesFromSourceDirectory("content/test/data"); + SetupCrossSiteRedirector(https_server_.get()); + ASSERT_TRUE(https_server_->Start()); + } + + WebContents* web_contents() { return shell()->web_contents(); } + + net::EmbeddedTestServer* https_server() { return https_server_.get(); } + + private: + base::test::ScopedFeatureList feature_list_; + std::unique_ptr<net::EmbeddedTestServer> https_server_; +}; + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + ImpressionTagClicked_ImpressionReceived) { + ImpressionObserver impression_observer(web_contents()); + GURL page_url = + https_server()->GetURL("b.test", "/page_with_impression_creator.html"); + EXPECT_TRUE(NavigateToURL(web_contents(), page_url)); + + // Create an anchor tag with impression attributes and click the link. By + // default the target is set to "_top". + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTagWithReportingAndExpiry("link" /* id */, + "page_with_conversion_redirect.html" /* url */, + "1" /* impression data */, + "https://a.com" /* conversion_destination */, + "https://report.com" /* report_origin */, + 1000 /* expiry */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');")); + + // Wait for the impression to be seen by the observer. + Impression last_impression = impression_observer.WaitForImpression(); + + // Verify the attributes of the impression are set as expected. + EXPECT_EQ(1UL, last_impression.impression_data); + EXPECT_EQ(url::Origin::Create(GURL("https://a.com")), + last_impression.conversion_destination); + EXPECT_EQ(url::Origin::Create(GURL("https://report.com")), + last_impression.reporting_origin); + EXPECT_EQ(base::TimeDelta::FromMilliseconds(1000), *last_impression.expiry); +} + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + ImpressionTagNavigatesRemoteFrame_ImpressionReceived) { + EXPECT_TRUE(NavigateToURL( + web_contents(), + https_server()->GetURL("b.test", "/page_with_impression_creator.html"))); + + ShellAddedObserver new_shell_observer; + + // Create an impression tag with a target frame that does not exist, which + // will open a new window to navigate. + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTagWithTarget("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */, + "target" /* target */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');")); + + ImpressionObserver impression_observer( + new_shell_observer.GetShell()->web_contents()); + + // Wait for the impression to be seen by the observer. + Impression last_impression = impression_observer.WaitForImpression(); + EXPECT_EQ(1UL, impression_observer.last_impression().impression_data); +} + +// Test frequently flakes due to timeout. ( https://crbug.com/1084201 ) +IN_PROC_BROWSER_TEST_F( + ImpressionDeclarationBrowserTest, + DISABLED_ImpressionTagNavigatesExistingRemoteFrame_ImpressionReceived) { + EXPECT_TRUE(NavigateToURL( + web_contents(), + https_server()->GetURL("b.test", "/page_with_impression_creator.html"))); + + WebContents* initial_web_contents = web_contents(); + + ShellAddedObserver new_shell_observer; + GURL remote_url = https_server()->GetURL("c.test", "/title1.html"); + EXPECT_TRUE(ExecJs(web_contents(), + JsReplace("window.open($1, 'target');", remote_url))); + + // Get the new web contents associated with the remote frame. + WebContents* remote_web_contents = + new_shell_observer.GetShell()->web_contents(); + + // Click on the impression and target the existing remote frame. + EXPECT_TRUE(ExecJs(initial_web_contents, R"( + createImpressionTagWithTarget("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */, + "target" /* target */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');")); + + ImpressionObserver impression_observer(remote_web_contents); + + // Wait for the impression to be seen by the observer. + Impression last_impression = impression_observer.WaitForImpression(); + EXPECT_EQ(1UL, impression_observer.last_impression().impression_data); +} + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + ImpressionTagWithOutOfBoundData_DefaultedTo0) { + ImpressionObserver impression_observer(web_contents()); + EXPECT_TRUE(NavigateToURL( + web_contents(), + https_server()->GetURL("b.test", "/page_with_impression_creator.html"))); + + // The provided data overflows an unsigned 64 bit int, and should be handled + // properly. + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTag("link", + "page_with_conversion_redirect.html", + "FFFFFFFFFFFFFFFFFFFFFF" /* impression data */, + "https://a.com" /* conversion_destination */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');")); + + // Wait for the impression to be seen by the observer. + Impression last_impression = impression_observer.WaitForImpression(); + EXPECT_EQ(0UL, impression_observer.last_impression().impression_data); +} + +IN_PROC_BROWSER_TEST_F( + ImpressionDeclarationBrowserTest, + ImpressionTagNavigatesFromMiddleClick_ImpressionReceived) { + GURL page_url = + https_server()->GetURL("b.test", "/page_with_impression_creator.html"); + EXPECT_TRUE(NavigateToURL(web_contents(), page_url)); + + ShellAddedObserver new_shell_observer; + + // Create an impression tag that is opened via middle click. This navigates in + // a new WebContents. + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTag("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateMiddleClick(\'link\');")); + + ImpressionObserver impression_observer( + new_shell_observer.GetShell()->web_contents()); + + Impression last_impression = impression_observer.WaitForImpression(); + + // Verify the attributes of the impression are set as expected. + EXPECT_EQ(1UL, last_impression.impression_data); +} + +IN_PROC_BROWSER_TEST_F( + ImpressionDeclarationBrowserTest, + ImpressionTagNavigatesFromEnterPress_ImpressionReceived) { + GURL page_url = + https_server()->GetURL("b.test", "/page_with_impression_creator.html"); + EXPECT_TRUE(NavigateToURL(web_contents(), page_url)); + + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTag("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */);)")); + + // Focus the element, wait for it to receive focus, and simulate an enter + // press. + base::string16 expected_title = base::ASCIIToUTF16("focused"); + content::TitleWatcher title_watcher(web_contents(), expected_title); + EXPECT_TRUE(ExecJs(shell(), R"( + let link = document.getElementById('link'); + link.addEventListener('focus', function() { document.title = 'focused'; }); + link.focus();)")); + EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); + content::SimulateKeyPress(web_contents(), ui::DomKey::ENTER, + ui::DomCode::ENTER, ui::VKEY_RETURN, false, false, + false, false); + + ImpressionObserver impression_observer(web_contents()); + Impression last_impression = impression_observer.WaitForImpression(); + + // Verify the attributes of the impression are set as expected. + EXPECT_EQ(1UL, last_impression.impression_data); +} + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + ImpressionOnInsecureSite_NotRegistered) { + // Navigate to a page with the non-https server. + EXPECT_TRUE(NavigateToURL( + web_contents(), embedded_test_server()->GetURL( + "b.test", "/page_with_impression_creator.html"))); + + ImpressionObserver impression_observer(web_contents()); + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTag("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');")); + + // We should see a null impression on the navigation + EXPECT_TRUE(impression_observer.WaitForNavigationWithNoImpression()); +} + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + ImpressionWithInsecureDestination_NotRegistered) { + // Navigate to a page with the non-https server. + EXPECT_TRUE(NavigateToURL( + web_contents(), + https_server()->GetURL("b.test", "/page_with_impression_creator.html"))); + + ImpressionObserver impression_observer(web_contents()); + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTag("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "http://a.com" /* conversion_destination */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');")); + + // We should see a null impression on the navigation + EXPECT_TRUE(impression_observer.WaitForNavigationWithNoImpression()); +} + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + ImpressionWithInsecureReportingOrigin_NotRegistered) { + // Navigate to a page with the non-https server. + EXPECT_TRUE(NavigateToURL( + web_contents(), + https_server()->GetURL("b.test", "/page_with_impression_creator.html"))); + + ImpressionObserver impression_observer(web_contents()); + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTagWithReportingAndExpiry("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */, + "http://reporting.com" /* report_origin */, + 1000 /* expiry */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');")); + + // We should see a null impression on the navigation + EXPECT_TRUE(impression_observer.WaitForNavigationWithNoImpression()); +} + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + ImpressionWithFeaturePolicyDisabled_NotRegistered) { + EXPECT_TRUE(NavigateToURL( + web_contents(), + https_server()->GetURL( + "b.test", "/page_with_conversion_measurement_disabled.html"))); + + ImpressionObserver impression_observer(web_contents()); + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTag("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateClick('link');")); + + // We should see a null impression on the navigation + EXPECT_TRUE(impression_observer.WaitForNavigationWithNoImpression()); +} + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + ImpressionInSubframeWithoutFeaturePolicy_NotRegistered) { + GURL page_url = https_server()->GetURL("b.test", "/page_with_iframe.html"); + EXPECT_TRUE(NavigateToURL(web_contents(), page_url)); + + GURL subframe_url = + https_server()->GetURL("c.test", "/page_with_impression_creator.html"); + NavigateIframeToURL(web_contents(), "test_iframe", subframe_url); + + ImpressionObserver impression_observer(web_contents()); + RenderFrameHost* subframe = ChildFrameAt(web_contents()->GetMainFrame(), 0); + EXPECT_TRUE(ExecJs(subframe, R"( + createImpressionTag("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */);)")); + EXPECT_TRUE(ExecJs(subframe, "simulateClick('link');")); + + // We should see a null impression on the navigation + EXPECT_TRUE(impression_observer.WaitForNavigationWithNoImpression()); +} + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + ImpressionInSubframeWithFeaturePolicy_Registered) { + GURL page_url = https_server()->GetURL("b.test", "/page_with_iframe.html"); + EXPECT_TRUE(NavigateToURL(web_contents(), page_url)); + EXPECT_TRUE(ExecJs(shell(), R"( + let frame = document.getElementById('test_iframe'); + frame.setAttribute('allow', 'conversion-measurement');)")); + + GURL subframe_url = + https_server()->GetURL("c.test", "/page_with_impression_creator.html"); + NavigateIframeToURL(web_contents(), "test_iframe", subframe_url); + + ImpressionObserver impression_observer(web_contents()); + RenderFrameHost* subframe = ChildFrameAt(web_contents()->GetMainFrame(), 0); + EXPECT_TRUE(ExecJs(subframe, R"( + createImpressionTag("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */);)")); + EXPECT_TRUE(ExecJs(subframe, "simulateClick('link');")); + + // We should see a null impression on the navigation + EXPECT_EQ(1u, impression_observer.WaitForImpression().impression_data); +} + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + ImpressionNavigationReloads_NoImpression) { + EXPECT_TRUE(NavigateToURL( + web_contents(), + https_server()->GetURL("b.test", "/page_with_impression_creator.html"))); + + ImpressionObserver impression_observer(web_contents()); + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTag("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');")); + EXPECT_EQ(1UL, impression_observer.WaitForImpression().impression_data); + + ImpressionObserver reload_observer(web_contents()); + shell()->Reload(); + + // The reload navigation should not have an impression set. + EXPECT_TRUE(reload_observer.WaitForNavigationWithNoImpression()); +} + +// Same as the above test but via a renderer initiated reload. +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + RendererReloadImpressionNavigation_NoImpression) { + EXPECT_TRUE(NavigateToURL( + web_contents(), + https_server()->GetURL("b.test", "/page_with_impression_creator.html"))); + + ImpressionObserver impression_observer(web_contents()); + EXPECT_TRUE(ExecJs(web_contents(), R"( + createImpressionTag("link", + "page_with_conversion_redirect.html", + "1" /* impression data */, + "https://a.com" /* conversion_destination */);)")); + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'link\');")); + EXPECT_EQ(1UL, impression_observer.WaitForImpression().impression_data); + + ImpressionObserver reload_observer(web_contents()); + EXPECT_TRUE(ExecJs(web_contents(), "window.location.reload()")); + + // The reload navigation should not have an impression set. + EXPECT_TRUE(reload_observer.WaitForNavigationWithNoImpression()); +} + +IN_PROC_BROWSER_TEST_F(ImpressionDeclarationBrowserTest, + BackNavigateToImpressionNavigation_NoImpression) { + EXPECT_TRUE(NavigateToURL( + web_contents(), + https_server()->GetURL("b.test", "/page_with_impression_creator.html"))); + + ImpressionObserver impression_observer(web_contents()); + + // Click the default impression on the page. + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'impression_tag\');")); + EXPECT_EQ(1UL, impression_observer.WaitForImpression().impression_data); + + // Navigate away so we can back navigate to the impression's navigated page. + EXPECT_TRUE(NavigateToURL(web_contents(), GURL("about:blank"))); + + // The back navigation should not have an impression set. + ImpressionObserver back_nav_observer(web_contents()); + shell()->GoBackOrForward(-1); + EXPECT_TRUE(back_nav_observer.WaitForNavigationWithNoImpression()); + + // Navigate back to the original page and ensure subsequent clicks also log + // impressions. + ImpressionObserver second_back_nav_observer(web_contents()); + shell()->GoBackOrForward(-1); + EXPECT_TRUE(second_back_nav_observer.WaitForNavigationWithNoImpression()); + + // Wait for the page to load and render the impression tag. + WaitForLoadStop(web_contents()); + ImpressionObserver second_impression_observer(web_contents()); + EXPECT_TRUE(ExecJs(shell(), "simulateClick(\'impression_tag\');")); + EXPECT_EQ(1UL, + second_impression_observer.WaitForImpression().impression_data); +} + +} // namespace content diff --git a/chromium/content/browser/conversions/storable_conversion.cc b/chromium/content/browser/conversions/storable_conversion.cc index d3db20c1a7b..215554be145 100644 --- a/chromium/content/browser/conversions/storable_conversion.cc +++ b/chromium/content/browser/conversions/storable_conversion.cc @@ -4,7 +4,7 @@ #include "content/browser/conversions/storable_conversion.h" -#include "base/logging.h" +#include "base/check.h" namespace content { diff --git a/chromium/content/browser/conversions/storable_impression.cc b/chromium/content/browser/conversions/storable_impression.cc index 0236f690782..ae300e47a7e 100644 --- a/chromium/content/browser/conversions/storable_impression.cc +++ b/chromium/content/browser/conversions/storable_impression.cc @@ -4,7 +4,7 @@ #include "content/browser/conversions/storable_impression.h" -#include "base/logging.h" +#include "base/check_op.h" namespace content { diff --git a/chromium/content/browser/conversions/storable_impression.h b/chromium/content/browser/conversions/storable_impression.h index ccbc0d9ffa7..e6cea3ffbf5 100644 --- a/chromium/content/browser/conversions/storable_impression.h +++ b/chromium/content/browser/conversions/storable_impression.h @@ -56,6 +56,9 @@ class CONTENT_EXPORT StorableImpression { // If null, an ID has not been assigned yet. base::Optional<int64_t> impression_id_; + + // When adding new members, the ImpressionsEqual() testing utility in + // conversion_test_utils.h should also be updated. }; } // namespace content |