From f230ea652747249c79b28f0c2b3a83107aac2f69 Mon Sep 17 00:00:00 2001 From: Yao Xiao Date: Tue, 29 Mar 2022 02:26:02 +0000 Subject: [PATCH] [Merge to M101][Topics] Implement BrowsingTopicsServiceImpl - Implement the BrowsingTopicsServiceImpl responsible for scheduling the topics calculation, observing and handling history/cookies deletion, and calculating the topics for the JS API and for the UX. - Integrate with the renderer side API. - Update the BrowsingTopicsSiteDataManager/Storage::OnBrowsingTopicsApiUsed API() to let it get the timestamp from the main thread and pass it to the backend thread. This way, it's consistent with the query operation, so that there's no races (i.e. a Query(/*end_time=*/Now()) is guaranteed to return previously stored entries). - Update model_version type from "int" to "int64_t": the model version is expected to exceed the limit of int (i.e. it's using the timestamp). (cherry picked from commit cc37939dc5f9987a6d649d5357627f85ad9f1e03) Binary-Size: Size increase is unavoidable due to new feature. Bug: 1310012 Change-Id: I45bc0ff3e7b8e9df28c940d14a95f076d9a69944 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3526273 Reviewed-by: Josh Karlin Reviewed-by: Daniel Cheng Reviewed-by: Avi Drissman Commit-Queue: Yao Xiao Cr-Original-Commit-Position: refs/heads/main@{#985500} Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3552463 Reviewed-by: Yao Xiao Cr-Commit-Position: refs/branch-heads/4951@{#220} Cr-Branched-From: 27de6227ca357da0d57ae2c7b18da170c4651438-refs/heads/main@{#982481} --- .../browsing_topics_service_browsertest.cc | 567 +++++++- .../browsing_topics_service_factory.cc | 42 +- .../browser/chrome_content_browser_client.cc | 20 + .../browser/chrome_content_browser_client.h | 4 + chrome/test/BUILD.gn | 2 + ...rame_page_sandboxed.html.mock-http-headers | 2 + .../one_sandboxed_iframe_page.html | 6 + components/browsing_topics/BUILD.gn | 13 +- components/browsing_topics/DEPS | 4 +- .../browsing_topics_calculator.cc | 10 +- .../browsing_topics_calculator.h | 3 +- .../browsing_topics_calculator_unittest.cc | 55 +- .../browsing_topics_page_load_data_tracker.cc | 93 ++ .../browsing_topics_page_load_data_tracker.h | 53 + ..._topics_page_load_data_tracker_unittest.cc | 266 ++++ .../browsing_topics/browsing_topics_service.h | 10 + .../browsing_topics_service_impl.cc | 351 ++++- .../browsing_topics_service_impl.h | 123 +- .../browsing_topics_service_impl_unittest.cc | 1222 +++++++++++++++++ .../browsing_topics/browsing_topics_state.cc | 63 +- .../browsing_topics/browsing_topics_state.h | 31 +- .../browsing_topics_state_unittest.cc | 232 +++- components/browsing_topics/epoch_topics.cc | 50 +- components/browsing_topics/epoch_topics.h | 23 +- .../browsing_topics/epoch_topics_unittest.cc | 50 +- components/browsing_topics/test_util.cc | 68 + components/browsing_topics/test_util.h | 29 +- content/browser/BUILD.gn | 2 + content/browser/browser_interface_binders.cc | 5 + .../browsing_topics_document_host.cc | 61 + .../browsing_topics_document_host.h | 48 + .../browsing_topics_site_data_manager_impl.cc | 6 +- .../browsing_topics_site_data_manager_impl.h | 3 +- ..._topics_site_data_manager_impl_unittest.cc | 3 +- .../browsing_topics_site_data_storage.cc | 8 +- .../browsing_topics_site_data_storage.h | 3 +- ...wsing_topics_site_data_storage_unittest.cc | 38 +- .../renderer_host/render_frame_host_impl.cc | 8 + .../renderer_host/render_frame_host_impl.h | 1 + .../browsing_topics_site_data_manager.h | 3 +- .../public/browser/content_browser_client.cc | 8 + .../public/browser/content_browser_client.h | 7 + content/public/browser/render_frame_host.h | 3 + .../public/test/browsing_topics_test_util.cc | 42 +- .../public/test/browsing_topics_test_util.h | 8 +- third_party/blink/common/features.cc | 13 + third_party/blink/public/common/features.h | 4 + third_party/blink/public/mojom/BUILD.gn | 1 + .../blink/public/mojom/browsing_topics/OWNERS | 2 + .../browsing_topics/browsing_topics.mojom | 40 + .../browsing_topics_document_supplement.cc | 59 +- .../browsing_topics_document_supplement.h | 5 + 52 files changed, 3591 insertions(+), 182 deletions(-) create mode 100644 chrome/test/data/browsing_topics/one_iframe_page_sandboxed.html.mock-http-headers create mode 100644 chrome/test/data/browsing_topics/one_sandboxed_iframe_page.html create mode 100644 components/browsing_topics/browsing_topics_page_load_data_tracker.cc create mode 100644 components/browsing_topics/browsing_topics_page_load_data_tracker.h create mode 100644 components/browsing_topics/browsing_topics_page_load_data_tracker_unittest.cc create mode 100644 components/browsing_topics/browsing_topics_service_impl_unittest.cc create mode 100644 content/browser/browsing_topics/browsing_topics_document_host.cc create mode 100644 content/browser/browsing_topics/browsing_topics_document_host.h create mode 100644 third_party/blink/public/mojom/browsing_topics/OWNERS create mode 100644 third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom diff --git a/chrome/browser/browsing_topics/browsing_topics_service_browsertest.cc b/chrome/browser/browsing_topics/browsing_topics_service_browsertest.cc index d8b2cc0adcc4c1..8e3308319d0e88 100644 --- a/chrome/browser/browsing_topics/browsing_topics_service_browsertest.cc +++ b/chrome/browser/browsing_topics/browsing_topics_service_browsertest.cc @@ -2,21 +2,131 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "base/json/json_file_value_serializer.h" +#include "base/json/values_util.h" +#include "base/path_service.h" #include "base/test/scoped_feature_list.h" #include "chrome/browser/browsing_topics/browsing_topics_service_factory.h" +#include "chrome/browser/history/history_service_factory.h" +#include "chrome/browser/optimization_guide/browser_test_util.h" +#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h" +#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h" +#include "chrome/browser/optimization_guide/page_content_annotations_service_factory.h" +#include "chrome/browser/privacy_sandbox/privacy_sandbox_settings_factory.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/test/base/in_process_browser_test.h" #include "chrome/test/base/ui_test_utils.h" #include "components/browsing_topics/browsing_topics_service.h" +#include "components/browsing_topics/browsing_topics_service_impl.h" +#include "components/browsing_topics/epoch_topics.h" +#include "components/browsing_topics/test_util.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/optimization_guide/content/browser/page_content_annotations_service.h" +#include "components/optimization_guide/content/browser/test_page_content_annotator.h" +#include "components/optimization_guide/core/optimization_guide_features.h" +#include "components/optimization_guide/core/test_model_info_builder.h" +#include "components/optimization_guide/core/test_optimization_guide_model_provider.h" +#include "components/privacy_sandbox/privacy_sandbox_settings.h" +#include "content/public/browser/browsing_topics_site_data_manager.h" +#include "content/public/browser/storage_partition.h" #include "content/public/common/content_features.h" #include "content/public/test/browser_test.h" +#include "content/public/test/browsing_topics_test_util.h" +#include "content/public/test/fenced_frame_test_util.h" #include "net/dns/mock_host_resolver.h" #include "net/test/embedded_test_server/request_handler_util.h" +#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" #include "third_party/blink/public/common/features.h" namespace browsing_topics { +namespace { + +constexpr browsing_topics::HmacKey kTestKey = {1}; + +constexpr base::Time kTime1 = + base::Time::FromDeltaSinceWindowsEpoch(base::Days(1)); +constexpr base::Time kTime2 = + base::Time::FromDeltaSinceWindowsEpoch(base::Days(2)); + +constexpr size_t kTaxonomySize = 349; +constexpr int kTaxonomyVersion = 1; +constexpr int64_t kModelVersion = 2; +constexpr size_t kPaddedTopTopicsStartIndex = 3; + +constexpr char kExpectedResultOrder1[] = + "[{\"configVersion\":\"chrome.1\",\"modelVersion\":\"2\"," + "\"taxonomyVersion\":\"1\",\"topic\":1,\"version\":\"chrome.1:1:2\"};{" + "\"configVersion\":\"chrome.1\",\"modelVersion\":\"2\"," + "\"taxonomyVersion\":\"1\",\"topic\":10,\"version\":\"chrome.1:1:2\"};]"; + +constexpr char kExpectedResultOrder2[] = + "[{\"configVersion\":\"chrome.1\",\"modelVersion\":\"2\"," + "\"taxonomyVersion\":\"1\",\"topic\":10,\"version\":\"chrome.1:1:2\"};{" + "\"configVersion\":\"chrome.1\",\"modelVersion\":\"2\"," + "\"taxonomyVersion\":\"1\",\"topic\":1,\"version\":\"chrome.1:1:2\"};]"; + +EpochTopics CreateTestEpochTopics( + const std::vector>>& topics, + base::Time calculation_time) { + DCHECK_EQ(topics.size(), 5u); + + std::vector top_topics_and_observing_domains; + for (size_t i = 0; i < 5; ++i) { + top_topics_and_observing_domains.emplace_back(topics[i].first, + topics[i].second); + } + + return EpochTopics(std::move(top_topics_and_observing_domains), + kPaddedTopTopicsStartIndex, kTaxonomySize, + kTaxonomyVersion, kModelVersion, calculation_time); +} + +} // namespace + +// A tester class that allows waiting for the first calculation to finish. +class TesterBrowsingTopicsService : public BrowsingTopicsServiceImpl { + public: + TesterBrowsingTopicsService( + const base::FilePath& profile_path, + privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings, + history::HistoryService* history_service, + content::BrowsingTopicsSiteDataManager* site_data_manager, + optimization_guide::PageContentAnnotationsService* annotations_service, + base::OnceClosure calculation_finish_callback) + : BrowsingTopicsServiceImpl(profile_path, + privacy_sandbox_settings, + history_service, + site_data_manager, + annotations_service), + calculation_finish_callback_(std::move(calculation_finish_callback)) {} + + ~TesterBrowsingTopicsService() override = default; + + TesterBrowsingTopicsService(const TesterBrowsingTopicsService&) = delete; + TesterBrowsingTopicsService& operator=(const TesterBrowsingTopicsService&) = + delete; + TesterBrowsingTopicsService(TesterBrowsingTopicsService&&) = delete; + TesterBrowsingTopicsService& operator=(TesterBrowsingTopicsService&&) = + delete; + + const BrowsingTopicsState& browsing_topics_state() override { + return BrowsingTopicsServiceImpl::browsing_topics_state(); + } + + void OnCalculateBrowsingTopicsCompleted(EpochTopics epoch_topics) override { + BrowsingTopicsServiceImpl::OnCalculateBrowsingTopicsCompleted( + std::move(epoch_topics)); + + if (calculation_finish_callback_) + std::move(calculation_finish_callback_).Run(); + } + + private: + base::OnceClosure calculation_finish_callback_; +}; + class BrowsingTopicsBrowserTestBase : public InProcessBrowserTest { public: void SetUpOnMainThread() override { @@ -31,6 +141,8 @@ class BrowsingTopicsBrowserTestBase : public InProcessBrowserTest { ASSERT_TRUE(embedded_test_server()->Start()); } + ~BrowsingTopicsBrowserTestBase() override = default; + std::string InvokeTopicsAPI(const content::ToRenderFrameHost& adapter) { return EvalJs(adapter, R"( if (!(document.browsingTopics instanceof Function)) { @@ -64,7 +176,8 @@ class BrowsingTopicsDisabledBrowserTest : public BrowsingTopicsBrowserTestBase { public: BrowsingTopicsDisabledBrowserTest() { scoped_feature_list_.InitWithFeatures( - /*enabled_features=*/{}, + /*enabled_features=*/{optimization_guide::features:: + kPageContentAnnotations}, /*disabled_features=*/{blink::features::kBrowsingTopics}); } @@ -91,17 +204,241 @@ class BrowsingTopicsBrowserTest : public BrowsingTopicsBrowserTestBase { public: BrowsingTopicsBrowserTest() { scoped_feature_list_.InitWithFeatures( - /*enabled_features=*/{blink::features::kBrowsingTopics, - features::kPrivacySandboxAdsAPIsOverride}, + /*enabled_features=*/ + {optimization_guide::features::kPageContentAnnotations, + blink::features::kBrowsingTopics, + blink::features::kBrowsingTopicsBypassIPIsPubliclyRoutableCheck, + features::kPrivacySandboxAdsAPIsOverride}, /*disabled_features=*/{}); } - browsing_topics::BrowsingTopicsService* browsing_topics_service() { - return BrowsingTopicsServiceFactory::GetForProfile(browser()->profile()); + ~BrowsingTopicsBrowserTest() override = default; + + void SetUpOnMainThread() override { + BrowsingTopicsBrowserTestBase::SetUpOnMainThread(); + + for (auto& profile_and_calculation_finish_waiter : + calculation_finish_waiters_) { + profile_and_calculation_finish_waiter.second->Run(); + } + } + + // BrowserTestBase::SetUpInProcessBrowserTestFixture + void SetUpInProcessBrowserTestFixture() override { + subscription_ = + BrowserContextDependencyManager::GetInstance() + ->RegisterCreateServicesCallbackForTesting(base::BindRepeating( + &BrowsingTopicsBrowserTest::OnWillCreateBrowserContextServices, + base::Unretained(this))); } protected: + void ExpectResultTopicsEqual( + const std::vector& result, + std::vector>> expected) { + DCHECK_EQ(expected.size(), 5u); + EXPECT_EQ(result.size(), 5u); + + for (int i = 0; i < 5; ++i) { + EXPECT_EQ(result[i].topic(), expected[i].first); + EXPECT_EQ(result[i].hashed_domains(), expected[i].second); + } + } + + HashedDomain GetHashedDomain(const std::string& domain) { + return HashContextDomainForStorage(kTestKey, domain); + } + + void CreateBrowsingTopicsStateFile( + const base::FilePath& profile_path, + const std::vector& epochs, + base::Time next_scheduled_calculation_time) { + base::Value::List epochs_list; + for (const EpochTopics& epoch : epochs) { + epochs_list.Append(epoch.ToDictValue()); + } + + base::Value::Dict dict; + dict.Set("epochs", std::move(epochs_list)); + dict.Set("next_scheduled_calculation_time", + base::TimeToValue(next_scheduled_calculation_time)); + dict.Set("hex_encoded_hmac_key", base::HexEncode(kTestKey)); + dict.Set("config_version", 1); + + JSONFileValueSerializer( + profile_path.Append(FILE_PATH_LITERAL("BrowsingTopicsState"))) + .Serialize(dict); + } + + content::BrowsingTopicsSiteDataManager* browsing_topics_site_data_manager() { + return browser() + ->profile() + ->GetDefaultStoragePartition() + ->GetBrowsingTopicsSiteDataManager(); + } + + TesterBrowsingTopicsService* browsing_topics_service() { + return static_cast( + BrowsingTopicsServiceFactory::GetForProfile(browser()->profile())); + } + + const BrowsingTopicsState& browsing_topics_state() { + return browsing_topics_service()->browsing_topics_state(); + } + + std::vector TopicsAndWeight( + const std::vector& topics, + double weight) { + std::vector result; + for (int32_t topic : topics) { + result.emplace_back(topic, weight); + } + + return result; + } + + void OnWillCreateBrowserContextServices(content::BrowserContext* context) { + PageContentAnnotationsServiceFactory::GetInstance()->SetTestingFactory( + context, + base::BindRepeating( + &BrowsingTopicsBrowserTest::CreatePageContentAnnotationsService, + base::Unretained(this))); + + browsing_topics::BrowsingTopicsServiceFactory::GetInstance() + ->SetTestingFactory( + context, + base::BindRepeating( + &BrowsingTopicsBrowserTest::CreateBrowsingTopicsService, + base::Unretained(this))); + } + + std::unique_ptr CreatePageContentAnnotationsService( + content::BrowserContext* context) { + Profile* profile = Profile::FromBrowserContext(context); + + history::HistoryService* history_service = + HistoryServiceFactory::GetForProfile( + profile, ServiceAccessType::IMPLICIT_ACCESS); + + DCHECK(!base::Contains(optimization_guide_model_providers_, profile)); + optimization_guide_model_providers_.emplace( + profile, std::make_unique< + optimization_guide::TestOptimizationGuideModelProvider>()); + + auto page_content_annotations_service = + std::make_unique( + "en-US", optimization_guide_model_providers_.at(profile).get(), + history_service, nullptr, base::FilePath(), nullptr); + + page_content_annotations_service->OverridePageContentAnnotatorForTesting( + &test_page_content_annotator_); + + return page_content_annotations_service; + } + + void InitializePreexistingState( + history::HistoryService* history_service, + content::BrowsingTopicsSiteDataManager* site_data_manager, + const base::FilePath& profile_path) { + // Configure the (mock) model. + test_page_content_annotator_.UsePageTopics( + *optimization_guide::TestModelInfoBuilder().SetVersion(1).Build(), + {{"foo6 com", TopicsAndWeight({1, 2, 3, 4, 5, 6}, 0.1)}, + {"foo5 com", TopicsAndWeight({2, 3, 4, 5, 6}, 0.1)}, + {"foo4 com", TopicsAndWeight({3, 4, 5, 6}, 0.1)}, + {"foo3 com", TopicsAndWeight({4, 5, 6}, 0.1)}, + {"foo2 com", TopicsAndWeight({5, 6}, 0.1)}, + {"foo1 com", TopicsAndWeight({6}, 0.1)}}); + + // Add some initial history. + history::HistoryAddPageArgs add_page_args; + add_page_args.time = base::Time::Now(); + add_page_args.context_id = reinterpret_cast(1); + add_page_args.nav_entry_id = 1; + + // Note: foo6.com isn't in the initial history. + for (int i = 1; i <= 5; ++i) { + add_page_args.url = + GURL(base::StrCat({"https://foo", base::NumberToString(i), ".com"})); + history_service->AddPage(add_page_args); + history_service->SetBrowsingTopicsAllowed(add_page_args.context_id, + add_page_args.nav_entry_id, + add_page_args.url); + } + + // Add some API usage contexts data. + site_data_manager->OnBrowsingTopicsApiUsed( + HashMainFrameHostForStorage("foo1.com"), {HashedDomain(1)}, + base::Time::Now()); + + // Initialize the `BrowsingTopicsState`. + std::vector preexisting_epochs; + preexisting_epochs.push_back( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("a.test")}}, + {Topic(2), {GetHashedDomain("a.test")}}, + {Topic(3), {GetHashedDomain("a.test")}}, + {Topic(4), {GetHashedDomain("a.test")}}, + {Topic(5), {GetHashedDomain("a.test")}}}, + kTime1)); + preexisting_epochs.push_back( + CreateTestEpochTopics({{Topic(6), {GetHashedDomain("a.test")}}, + {Topic(7), {GetHashedDomain("a.test")}}, + {Topic(8), {GetHashedDomain("a.test")}}, + {Topic(9), {GetHashedDomain("a.test")}}, + {Topic(10), {GetHashedDomain("a.test")}}}, + kTime2)); + + CreateBrowsingTopicsStateFile( + profile_path, std::move(preexisting_epochs), + /*next_scheduled_calculation_time=*/base::Time::Now() - base::Days(1)); + } + + std::unique_ptr CreateBrowsingTopicsService( + content::BrowserContext* context) { + Profile* profile = Profile::FromBrowserContext(context); + + privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings = + PrivacySandboxSettingsFactory::GetForProfile(profile); + + history::HistoryService* history_service = + HistoryServiceFactory::GetForProfile( + profile, ServiceAccessType::IMPLICIT_ACCESS); + + content::BrowsingTopicsSiteDataManager* site_data_manager = + context->GetDefaultStoragePartition() + ->GetBrowsingTopicsSiteDataManager(); + + optimization_guide::PageContentAnnotationsService* annotations_service = + PageContentAnnotationsServiceFactory::GetForProfile(profile); + + InitializePreexistingState(history_service, site_data_manager, + profile->GetPath()); + + DCHECK(!base::Contains(calculation_finish_waiters_, profile)); + calculation_finish_waiters_.emplace(profile, + std::make_unique()); + + return std::make_unique( + profile->GetPath(), privacy_sandbox_settings, history_service, + site_data_manager, annotations_service, + calculation_finish_waiters_.at(profile)->QuitClosure()); + } + + content::test::FencedFrameTestHelper fenced_frame_test_helper_; + base::test::ScopedFeatureList scoped_feature_list_; + + std::map< + Profile*, + std::unique_ptr> + optimization_guide_model_providers_; + + std::map> + calculation_finish_waiters_; + + optimization_guide::TestPageContentAnnotator test_page_content_annotator_; + + base::CallbackListSubscription subscription_; }; IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, HasBrowsingTopicsService) { @@ -123,19 +460,186 @@ IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, NoServiceInIncognitoMode) { } IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, BrowsingTopicsStateOnStart) { - EXPECT_TRUE(browsing_topics_service() - ->GetTopicsForSiteForDisplay(/*top_origin=*/url::Origin()) - .empty()); - EXPECT_TRUE(browsing_topics_service()->GetTopTopicsForDisplay().empty()); + GURL main_frame_url = + https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); + + base::Time now = base::Time::Now(); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 3u); + EXPECT_EQ(browsing_topics_state().epochs()[0].calculation_time(), kTime1); + EXPECT_EQ(browsing_topics_state().epochs()[1].calculation_time(), kTime2); + EXPECT_GT(browsing_topics_state().epochs()[2].calculation_time(), + now - base::Minutes(1)); + EXPECT_LT(browsing_topics_state().epochs()[2].calculation_time(), now); + + ExpectResultTopicsEqual( + browsing_topics_state().epochs()[0].top_topics_and_observing_domains(), + {{Topic(1), {GetHashedDomain("a.test")}}, + {Topic(2), {GetHashedDomain("a.test")}}, + {Topic(3), {GetHashedDomain("a.test")}}, + {Topic(4), {GetHashedDomain("a.test")}}, + {Topic(5), {GetHashedDomain("a.test")}}}); + + ExpectResultTopicsEqual( + browsing_topics_state().epochs()[1].top_topics_and_observing_domains(), + {{Topic(6), {GetHashedDomain("a.test")}}, + {Topic(7), {GetHashedDomain("a.test")}}, + {Topic(8), {GetHashedDomain("a.test")}}, + {Topic(9), {GetHashedDomain("a.test")}}, + {Topic(10), {GetHashedDomain("a.test")}}}); + + ExpectResultTopicsEqual( + browsing_topics_state().epochs()[2].top_topics_and_observing_domains(), + {{Topic(6), {HashedDomain(1)}}, + {Topic(5), {}}, + {Topic(4), {}}, + {Topic(3), {}}, + {Topic(2), {}}}); + + EXPECT_GT(browsing_topics_state().next_scheduled_calculation_time(), + now + base::Days(7) - base::Minutes(1)); + EXPECT_LT(browsing_topics_state().next_scheduled_calculation_time(), + now + base::Days(7)); +} + +IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, GetTopicsForSiteForDisplay) { + GURL main_frame_url = + https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); + + std::vector result = + browsing_topics_service()->GetTopicsForSiteForDisplay( + web_contents()->GetMainFrame()->GetLastCommittedOrigin()); + + // Epoch switch time has not arrived. So expect one topic from each of the + // first two epochs. + EXPECT_EQ(result.size(), 2u); + EXPECT_EQ(result[0].topic_id(), Topic(1)); + EXPECT_EQ(result[0].taxonomy_version(), 1); + EXPECT_EQ(result[1].topic_id(), Topic(10)); + EXPECT_EQ(result[1].taxonomy_version(), 1); +} + +IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, GetTopTopicsForDisplay) { + GURL main_frame_url = + https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); + + std::vector result = + browsing_topics_service()->GetTopTopicsForDisplay(); + + EXPECT_EQ(result.size(), 15u); + EXPECT_EQ(result[0].topic_id(), Topic(1)); + EXPECT_EQ(result[1].topic_id(), Topic(2)); + EXPECT_EQ(result[2].topic_id(), Topic(3)); + EXPECT_EQ(result[3].topic_id(), Topic(4)); + EXPECT_EQ(result[4].topic_id(), Topic(5)); + EXPECT_EQ(result[5].topic_id(), Topic(6)); + EXPECT_EQ(result[6].topic_id(), Topic(7)); + EXPECT_EQ(result[7].topic_id(), Topic(8)); + EXPECT_EQ(result[8].topic_id(), Topic(9)); + EXPECT_EQ(result[9].topic_id(), Topic(10)); + EXPECT_EQ(result[10].topic_id(), Topic(6)); + EXPECT_EQ(result[11].topic_id(), Topic(5)); + EXPECT_EQ(result[12].topic_id(), Topic(4)); + EXPECT_EQ(result[13].topic_id(), Topic(3)); + EXPECT_EQ(result[14].topic_id(), Topic(2)); } -IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, EmptyPage_TopicsAPI) { +IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, + TopicsAPI_ContextDomainNotFiltered_FromMainFrame) { + GURL main_frame_url = + https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); + + std::string result = InvokeTopicsAPI(web_contents()); + + EXPECT_TRUE(result == kExpectedResultOrder1 || + result == kExpectedResultOrder2); +} + +IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, + TopicsAPI_ContextDomainNotFiltered_FromSubframe) { GURL main_frame_url = + https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); + + GURL subframe_url = https_server_.GetURL("a.test", "/browsing_topics/empty_page.html"); + ASSERT_TRUE(content::NavigateIframeToURL(web_contents(), + /*iframe_id=*/"frame", + subframe_url)); + + std::string result = + InvokeTopicsAPI(content::ChildFrameAt(web_contents()->GetMainFrame(), 0)); + + EXPECT_TRUE(result == kExpectedResultOrder1 || + result == kExpectedResultOrder2); +} + +IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, + TopicsAPI_ContextDomainFiltered) { + GURL main_frame_url = + https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); + + GURL subframe_url = + https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); + + ASSERT_TRUE(content::NavigateIframeToURL(web_contents(), + /*iframe_id=*/"frame", + subframe_url)); + + // b.test has yet to call the API so it shouldn't receive a topic. + EXPECT_EQ("[]", InvokeTopicsAPI(content::ChildFrameAt( + web_contents()->GetMainFrame(), 0))); +} + +IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, + TopicsAPI_ContextDomainTracked) { + GURL main_frame_url = + https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); - EXPECT_EQ("[]", InvokeTopicsAPI(web_contents())); + GURL subframe_url = + https_server_.GetURL("b.test", "/browsing_topics/empty_page.html"); + + ASSERT_TRUE(content::NavigateIframeToURL(web_contents(), + /*iframe_id=*/"frame", + subframe_url)); + + // The usage is not tracked before the API call. The returned entry was from + // the pre-existing storage. + std::vector api_usage_contexts = + content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); + EXPECT_EQ(api_usage_contexts.size(), 1u); + + EXPECT_EQ("[]", InvokeTopicsAPI(content::ChildFrameAt( + web_contents()->GetMainFrame(), 0))); + + api_usage_contexts = + content::GetBrowsingTopicsApiUsage(browsing_topics_site_data_manager()); + + // The usage is tracked after the API call. + EXPECT_EQ(api_usage_contexts.size(), 2u); + EXPECT_EQ(api_usage_contexts[0].hashed_main_frame_host, + HashMainFrameHostForStorage("foo1.com")); + EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, HashedDomain(1)); + + EXPECT_EQ( + api_usage_contexts[1].hashed_main_frame_host, + HashMainFrameHostForStorage(https_server_.GetURL("a.test", "/").host())); + EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, + GetHashedDomain("b.test")); } IN_PROC_BROWSER_TEST_F( @@ -181,7 +685,9 @@ IN_PROC_BROWSER_TEST_F( /*iframe_id=*/"frame", subframe_url)); - EXPECT_EQ("[]", InvokeTopicsAPI(web_contents())); + std::string result = InvokeTopicsAPI(web_contents()); + EXPECT_TRUE(result == kExpectedResultOrder1 || + result == kExpectedResultOrder2); EXPECT_EQ( "The \"browsing-topics\" Permissions Policy denied the use of " @@ -204,7 +710,9 @@ IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); - EXPECT_EQ("[]", InvokeTopicsAPI(web_contents())); + std::string result = InvokeTopicsAPI(web_contents()); + EXPECT_TRUE(result == kExpectedResultOrder1 || + result == kExpectedResultOrder2); GURL subframe_url = https_server_.GetURL("c.test", "/browsing_topics/empty_page.html"); @@ -270,4 +778,37 @@ IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, )")); } +IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, + TopicsAPINotAllowedInOpaqueOriginDocument) { + GURL main_frame_url = https_server_.GetURL( + "a.test", "/browsing_topics/one_sandboxed_iframe_page.html"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); + + EXPECT_EQ( + "document.browsingTopics() is not allowed in an opaque origin context.", + InvokeTopicsAPI( + content::ChildFrameAt(web_contents()->GetMainFrame(), 0))); +} + +IN_PROC_BROWSER_TEST_F(BrowsingTopicsBrowserTest, + TopicsAPINotAllowedInFencedFrame) { + GURL main_frame_url = + https_server_.GetURL("a.test", "/browsing_topics/one_iframe_page.html"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_frame_url)); + + GURL fenced_frame_url = + https_server_.GetURL("b.test", "/fenced_frames/title1.html"); + + content::RenderFrameHostWrapper fenced_frame_rfh_wrapper( + fenced_frame_test_helper_.CreateFencedFrame( + web_contents()->GetMainFrame(), fenced_frame_url)); + + EXPECT_EQ( + "document.browsingTopics() is only allowed in the primary main frame or " + "in its child iframes.", + InvokeTopicsAPI(fenced_frame_rfh_wrapper.get())); +} + } // namespace browsing_topics diff --git a/chrome/browser/browsing_topics/browsing_topics_service_factory.cc b/chrome/browser/browsing_topics/browsing_topics_service_factory.cc index aa5a533c330636..1947a7fbf20594 100644 --- a/chrome/browser/browsing_topics/browsing_topics_service_factory.cc +++ b/chrome/browser/browsing_topics/browsing_topics_service_factory.cc @@ -4,10 +4,19 @@ #include "chrome/browser/browsing_topics/browsing_topics_service_factory.h" +#include "chrome/browser/history/history_service_factory.h" +#include "chrome/browser/optimization_guide/page_content_annotations_service_factory.h" +#include "chrome/browser/privacy_sandbox/privacy_sandbox_settings_factory.h" #include "chrome/browser/profiles/profile.h" +#include "components/browsing_topics/browsing_topics_service.h" #include "components/browsing_topics/browsing_topics_service_impl.h" +#include "components/history/core/browser/history_service.h" #include "components/keyed_service/content/browser_context_dependency_manager.h" #include "components/keyed_service/core/service_access_type.h" +#include "components/optimization_guide/content/browser/page_content_annotations_service.h" +#include "components/privacy_sandbox/privacy_sandbox_settings.h" +#include "content/public/browser/browsing_topics_site_data_manager.h" +#include "content/public/browser/storage_partition.h" #include "third_party/blink/public/common/features.h" namespace browsing_topics { @@ -28,7 +37,11 @@ BrowsingTopicsServiceFactory* BrowsingTopicsServiceFactory::GetInstance() { BrowsingTopicsServiceFactory::BrowsingTopicsServiceFactory() : BrowserContextKeyedServiceFactory( "BrowsingTopicsService", - BrowserContextDependencyManager::GetInstance()) {} + BrowserContextDependencyManager::GetInstance()) { + DependsOn(PrivacySandboxSettingsFactory::GetInstance()); + DependsOn(HistoryServiceFactory::GetInstance()); + DependsOn(PageContentAnnotationsServiceFactory::GetInstance()); +} BrowsingTopicsServiceFactory::~BrowsingTopicsServiceFactory() = default; @@ -37,7 +50,32 @@ KeyedService* BrowsingTopicsServiceFactory::BuildServiceInstanceFor( if (!base::FeatureList::IsEnabled(blink::features::kBrowsingTopics)) return nullptr; - return new BrowsingTopicsServiceImpl(); + Profile* profile = Profile::FromBrowserContext(context); + + privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings = + PrivacySandboxSettingsFactory::GetForProfile(profile); + if (!privacy_sandbox_settings) + return nullptr; + + history::HistoryService* history_service = + HistoryServiceFactory::GetForProfile(profile, + ServiceAccessType::IMPLICIT_ACCESS); + if (!history_service) + return nullptr; + + content::BrowsingTopicsSiteDataManager* site_data_manager = + context->GetDefaultStoragePartition()->GetBrowsingTopicsSiteDataManager(); + if (!site_data_manager) + return nullptr; + + optimization_guide::PageContentAnnotationsService* annotations_service = + PageContentAnnotationsServiceFactory::GetForProfile(profile); + if (!annotations_service) + return nullptr; + + return new BrowsingTopicsServiceImpl( + profile->GetPath(), privacy_sandbox_settings, history_service, + site_data_manager, annotations_service); } bool BrowsingTopicsServiceFactory::ServiceIsCreatedWithBrowserContext() const { diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc index 8f28441f6639ba..95935148138235 100644 --- a/chrome/browser/chrome_content_browser_client.cc +++ b/chrome/browser/chrome_content_browser_client.cc @@ -35,6 +35,7 @@ #include "chrome/browser/browser_about_handler.h" #include "chrome/browser/browser_features.h" #include "chrome/browser/browser_process.h" +#include "chrome/browser/browsing_topics/browsing_topics_service_factory.h" #include "chrome/browser/captive_portal/captive_portal_service_factory.h" #include "chrome/browser/chrome_content_browser_client_binder_policies.h" #include "chrome/browser/chrome_content_browser_client_parts.h" @@ -173,6 +174,7 @@ #include "chrome/installer/util/google_update_settings.h" #include "components/autofill/core/common/autofill_switches.h" #include "components/blocked_content/popup_blocker.h" +#include "components/browsing_topics/browsing_topics_service.h" #include "components/captive_portal/core/buildflags.h" #include "components/cloud_devices/common/cloud_devices_switches.h" #include "components/content_settings/browser/page_specific_content_settings.h" @@ -308,6 +310,7 @@ #include "third_party/blink/public/common/loader/url_loader_throttle.h" #include "third_party/blink/public/common/navigation/navigation_policy.h" #include "third_party/blink/public/common/switches.h" +#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" #include "third_party/blink/public/public_buildflags.h" #include "third_party/widevine/cdm/buildflags.h" #include "ui/base/clipboard/clipboard_format_type.h" @@ -5974,6 +5977,23 @@ void ChromeContentBrowserClient::AugmentNavigationDownloadPolicy( } } +std::vector +ChromeContentBrowserClient::GetBrowsingTopicsForJsApi( + const url::Origin& context_origin, + content::RenderFrameHost* main_frame) { + browsing_topics::BrowsingTopicsService* browsing_topics_service = + browsing_topics::BrowsingTopicsServiceFactory::GetForProfile( + Profile::FromBrowserContext( + content::WebContents::FromRenderFrameHost(main_frame) + ->GetBrowserContext())); + + if (!browsing_topics_service) + return {}; + + return browsing_topics_service->GetBrowsingTopicsForJsApi(context_origin, + main_frame); +} + bool ChromeContentBrowserClient::IsBluetoothScanningBlocked( content::BrowserContext* browser_context, const url::Origin& requesting_origin, diff --git a/chrome/browser/chrome_content_browser_client.h b/chrome/browser/chrome_content_browser_client.h index 4aa99834bd6c70..e87e967efce09d 100644 --- a/chrome/browser/chrome_content_browser_client.h +++ b/chrome/browser/chrome_content_browser_client.h @@ -680,6 +680,10 @@ class ChromeContentBrowserClient : public content::ContentBrowserClient { bool user_gesture, blink::NavigationDownloadPolicy* download_policy) override; + std::vector GetBrowsingTopicsForJsApi( + const url::Origin& context_origin, + content::RenderFrameHost* main_frame) override; + bool IsBluetoothScanningBlocked(content::BrowserContext* browser_context, const url::Origin& requesting_origin, const url::Origin& embedding_origin) override; diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn index 3883b62bf65713..75b463d5880b5d 100644 --- a/chrome/test/BUILD.gn +++ b/chrome/test/BUILD.gn @@ -221,6 +221,7 @@ static_library("test_support") { "//chrome/utility", "//components/autofill/core/browser:test_support", "//components/bookmarks/test", + "//components/browsing_topics:test_support", "//components/captive_portal/core:test_support", "//components/consent_auditor:test_support", "//components/content_settings/core/browser", @@ -238,6 +239,7 @@ static_library("test_support") { "//components/network_time", "//components/network_time:network_time_test_support", "//components/omnibox/browser:test_support", + "//components/optimization_guide/content/browser:test_support", "//components/os_crypt", "//components/password_manager/core/browser:test_support", "//components/payments/core:test_support", diff --git a/chrome/test/data/browsing_topics/one_iframe_page_sandboxed.html.mock-http-headers b/chrome/test/data/browsing_topics/one_iframe_page_sandboxed.html.mock-http-headers new file mode 100644 index 00000000000000..3dd48caab8b1fb --- /dev/null +++ b/chrome/test/data/browsing_topics/one_iframe_page_sandboxed.html.mock-http-headers @@ -0,0 +1,2 @@ +HTTP/1.1 200 OK +Content-Security-Policy: sandbox allow-scripts; diff --git a/chrome/test/data/browsing_topics/one_sandboxed_iframe_page.html b/chrome/test/data/browsing_topics/one_sandboxed_iframe_page.html new file mode 100644 index 00000000000000..3e94c6aa614699 --- /dev/null +++ b/chrome/test/data/browsing_topics/one_sandboxed_iframe_page.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/components/browsing_topics/BUILD.gn b/components/browsing_topics/BUILD.gn index 0d0288131f977b..ffa5838c7889d1 100644 --- a/components/browsing_topics/BUILD.gn +++ b/components/browsing_topics/BUILD.gn @@ -6,6 +6,8 @@ source_set("browsing_topics") { sources = [ "browsing_topics_calculator.cc", "browsing_topics_calculator.h", + "browsing_topics_page_load_data_tracker.cc", + "browsing_topics_page_load_data_tracker.h", "browsing_topics_service.h", "browsing_topics_service_impl.cc", "browsing_topics_service_impl.h", @@ -28,7 +30,9 @@ source_set("browsing_topics") { "//components/optimization_guide/content/browser", "//components/privacy_sandbox", "//content/public/browser", + "//content/public/common:common", "//crypto", + "//net/base/registry_controlled_domains", "//third_party/blink/public/common", ] } @@ -42,13 +46,19 @@ source_set("test_support") { public_deps = [ "//base" ] - deps = [ ":browsing_topics" ] + deps = [ + ":browsing_topics", + "//base/test:test_support", + "//components/history/core/browser:browser", + ] } source_set("unit_tests") { testonly = true sources = [ "browsing_topics_calculator_unittest.cc", + "browsing_topics_page_load_data_tracker_unittest.cc", + "browsing_topics_service_impl_unittest.cc", "browsing_topics_state_unittest.cc", "epoch_topics_unittest.cc", "topic_and_domains_unittest.cc", @@ -61,6 +71,7 @@ source_set("unit_tests") { "//base", "//base/test:test_support", "//components/content_settings/core/test:test_support", + "//components/history/content/browser:browser", "//components/history/core/browser:browser", "//components/history/core/test", "//components/optimization_guide/content/browser:browser", diff --git a/components/browsing_topics/DEPS b/components/browsing_topics/DEPS index 26ef4ba5ef54dd..48e8027adfb68c 100644 --- a/components/browsing_topics/DEPS +++ b/components/browsing_topics/DEPS @@ -5,8 +5,10 @@ include_rules = [ "+components/privacy_sandbox", "+content/public/browser", "+content/public/test", + "+content/test", "+crypto", - "+third_party/blink/public/common", + "+net/base/registry_controlled_domains", + "+third_party/blink/public", ] specific_include_rules = { diff --git a/components/browsing_topics/browsing_topics_calculator.cc b/components/browsing_topics/browsing_topics_calculator.cc index 88ade005a33f2c..a7e9dc04a79d6d 100644 --- a/components/browsing_topics/browsing_topics_calculator.cc +++ b/components/browsing_topics/browsing_topics_calculator.cc @@ -277,6 +277,11 @@ void BrowsingTopicsCalculator::OnGetRecentlyVisitedURLsCompleted( std::vector raw_hosts_vector(raw_hosts.begin(), raw_hosts.end()); + if (raw_hosts_vector.empty()) { + OnGetTopicsForHostsCompleted(/*raw_hosts=*/{}, /*results=*/{}); + return; + } + annotations_service_->BatchAnnotatePageTopics( base::BindOnce(&BrowsingTopicsCalculator::OnGetTopicsForHostsCompleted, weak_ptr_factory_.GetWeakPtr(), raw_hosts_vector), @@ -303,7 +308,7 @@ void BrowsingTopicsCalculator::OnGetTopicsForHostsCompleted( return; } - const int model_version = base::checked_cast(model_info->GetVersion()); + const int64_t model_version = model_info->GetVersion(); DCHECK_GT(model_version, 0); std::map> host_topics_map; @@ -356,8 +361,7 @@ void BrowsingTopicsCalculator::OnGetTopicsForHostsCompleted( void BrowsingTopicsCalculator::OnCalculateCompleted( CalculatorResultStatus status, EpochTopics epoch_topics) { - DCHECK(status != CalculatorResultStatus::kSuccess || - epoch_topics.HasValidTopics()); + DCHECK(status != CalculatorResultStatus::kSuccess || !epoch_topics.empty()); base::UmaHistogramEnumeration( "BrowsingTopics.EpochTopicsCalculation.CalculatorResultStatus", status); diff --git a/components/browsing_topics/browsing_topics_calculator.h b/components/browsing_topics/browsing_topics_calculator.h index 1232da3752e7ec..c7b8e8249721c0 100644 --- a/components/browsing_topics/browsing_topics_calculator.h +++ b/components/browsing_topics/browsing_topics_calculator.h @@ -76,6 +76,7 @@ class BrowsingTopicsCalculator { protected: // This method exists for the purposes of overriding in tests. virtual uint64_t GenerateRandUint64(); + virtual void CheckCanCalculate(); private: // Get the top `kBrowsingTopicsNumberOfTopTopicsPerEpoch` topics. If there @@ -91,8 +92,6 @@ class BrowsingTopicsCalculator { std::vector& top_topics, size_t& padded_top_topics_start_index); - void CheckCanCalculate(); - void OnGetRecentBrowsingTopicsApiUsagesCompleted( browsing_topics::ApiUsageContextQueryResult result); diff --git a/components/browsing_topics/browsing_topics_calculator_unittest.cc b/components/browsing_topics/browsing_topics_calculator_unittest.cc index 3f0a05c2bc4366..2ebe8b1fef95d5 100644 --- a/components/browsing_topics/browsing_topics_calculator_unittest.cc +++ b/components/browsing_topics/browsing_topics_calculator_unittest.cc @@ -33,22 +33,22 @@ namespace browsing_topics { namespace { -const size_t kTaxonomySize = 349; -const int kTaxonomyVersion = 1; - -const std::string kHost1 = "www.foo1.com"; -const std::string kHost2 = "www.foo2.com"; -const std::string kHost3 = "www.foo3.com"; -const std::string kHost4 = "www.foo4.com"; -const std::string kHost5 = "www.foo5.com"; -const std::string kHost6 = "www.foo6.com"; - -const std::string kTokenizedHost1 = "foo1 com"; -const std::string kTokenizedHost2 = "foo2 com"; -const std::string kTokenizedHost3 = "foo3 com"; -const std::string kTokenizedHost4 = "foo4 com"; -const std::string kTokenizedHost5 = "foo5 com"; -const std::string kTokenizedHost6 = "foo6 com"; +constexpr size_t kTaxonomySize = 349; +constexpr int kTaxonomyVersion = 1; + +constexpr char kHost1[] = "www.foo1.com"; +constexpr char kHost2[] = "www.foo2.com"; +constexpr char kHost3[] = "www.foo3.com"; +constexpr char kHost4[] = "www.foo4.com"; +constexpr char kHost5[] = "www.foo5.com"; +constexpr char kHost6[] = "www.foo6.com"; + +constexpr char kTokenizedHost1[] = "foo1 com"; +constexpr char kTokenizedHost2[] = "foo2 com"; +constexpr char kTokenizedHost3[] = "foo3 com"; +constexpr char kTokenizedHost4[] = "foo4 com"; +constexpr char kTokenizedHost5[] = "foo5 com"; +constexpr char kTokenizedHost6[] = "foo6 com"; } // namespace @@ -62,10 +62,10 @@ class BrowsingTopicsCalculatorTest : public testing::Test { HostContentSettingsMap::RegisterProfilePrefs(prefs_.registry()); privacy_sandbox::RegisterProfilePrefs(prefs_.registry()); - host_content_settings_map_ = new HostContentSettingsMap( + host_content_settings_map_ = base::MakeRefCounted( &prefs_, /*is_off_the_record=*/false, /*store_last_modified=*/false, /*restore_session=*/false); - cookie_settings_ = new content_settings::CookieSettings( + cookie_settings_ = base::MakeRefCounted( host_content_settings_map_.get(), &prefs_, false, "chrome-extension"); auto privacy_sandbox_delegate = std::make_unique< privacy_sandbox_test_util::MockPrivacySandboxSettingsDelegate>(); @@ -146,21 +146,22 @@ class BrowsingTopicsCalculatorTest : public testing::Test { } void AddApiUsageContextEntries( - std::vector>> + const std::vector>>& main_frame_hosts_with_context_domains) { for (auto& [main_frame_host, context_domains] : main_frame_hosts_with_context_domains) { topics_site_data_manager_->OnBrowsingTopicsApiUsed( HashMainFrameHostForStorage(main_frame_host), base::flat_set(context_domains.begin(), - context_domains.end())); + context_domains.end()), + base::Time::Now()); } task_environment_.RunUntilIdle(); } std::vector TopicsAndWeight( - std::vector topics, + const std::vector& topics, double weight) { std::vector result; for (int32_t topic : topics) { @@ -213,7 +214,7 @@ TEST_F(BrowsingTopicsCalculatorTest, PermissionDenied) { privacy_sandbox_settings_->SetPrivacySandboxEnabled(false); EpochTopics result = CalculateTopics(); - EXPECT_FALSE(result.HasValidTopics()); + EXPECT_TRUE(result.empty()); histograms.ExpectUniqueSample( "BrowsingTopics.EpochTopicsCalculation.CalculatorResultStatus", @@ -227,7 +228,7 @@ TEST_F(BrowsingTopicsCalculatorTest, ApiUsageContextQueryError) { topics_site_data_manager_->SetQueryFailureOverride(); EpochTopics result = CalculateTopics(); - EXPECT_FALSE(result.HasValidTopics()); + EXPECT_TRUE(result.empty()); histograms.ExpectUniqueSample( "BrowsingTopics.EpochTopicsCalculation.CalculatorResultStatus", @@ -239,7 +240,7 @@ TEST_F(BrowsingTopicsCalculatorTest, AnnotationExecutionError) { base::HistogramTester histograms; EpochTopics result = CalculateTopics(); - EXPECT_FALSE(result.HasValidTopics()); + EXPECT_TRUE(result.empty()); histograms.ExpectUniqueSample( "BrowsingTopics.EpochTopicsCalculation.CalculatorResultStatus", @@ -267,7 +268,7 @@ TEST_F(BrowsingTopicsCalculatorUnsupporedTaxonomyVersionTest, *optimization_guide::TestModelInfoBuilder().SetVersion(1).Build(), {}); EpochTopics result = CalculateTopics(); - EXPECT_FALSE(result.HasValidTopics()); + EXPECT_TRUE(result.empty()); histograms.ExpectUniqueSample( "BrowsingTopics.EpochTopicsCalculation.CalculatorResultStatus", @@ -283,7 +284,7 @@ TEST_F(BrowsingTopicsCalculatorTest, TopicsMetadata) { *optimization_guide::TestModelInfoBuilder().SetVersion(1).Build(), {}); EpochTopics result1 = CalculateTopics(); - EXPECT_TRUE(result1.HasValidTopics()); + EXPECT_FALSE(result1.empty()); EXPECT_EQ(result1.taxonomy_size(), kTaxonomySize); EXPECT_EQ(result1.taxonomy_version(), kTaxonomyVersion); EXPECT_EQ(result1.model_version(), 1); @@ -300,7 +301,7 @@ TEST_F(BrowsingTopicsCalculatorTest, TopicsMetadata) { *optimization_guide::TestModelInfoBuilder().SetVersion(50).Build(), {}); EpochTopics result2 = CalculateTopics(); - EXPECT_TRUE(result2.HasValidTopics()); + EXPECT_FALSE(result2.empty()); EXPECT_EQ(result2.taxonomy_size(), kTaxonomySize); EXPECT_EQ(result2.taxonomy_version(), kTaxonomyVersion); EXPECT_EQ(result2.model_version(), 50); diff --git a/components/browsing_topics/browsing_topics_page_load_data_tracker.cc b/components/browsing_topics/browsing_topics_page_load_data_tracker.cc new file mode 100644 index 00000000000000..8e351b430afe15 --- /dev/null +++ b/components/browsing_topics/browsing_topics_page_load_data_tracker.cc @@ -0,0 +1,93 @@ +// Copyright 2022 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 "components/browsing_topics/browsing_topics_page_load_data_tracker.h" + +#include "components/browsing_topics/util.h" +#include "components/history/content/browser/history_context_helper.h" +#include "components/history/core/browser/history_service.h" +#include "content/public/browser/browsing_topics_site_data_manager.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/render_process_host.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/browser/web_contents.h" +#include "third_party/blink/public/common/features.h" +#include "third_party/blink/public/mojom/permissions_policy/permissions_policy_feature.mojom.h" + +namespace browsing_topics { + +BrowsingTopicsPageLoadDataTracker::~BrowsingTopicsPageLoadDataTracker() = + default; + +BrowsingTopicsPageLoadDataTracker::BrowsingTopicsPageLoadDataTracker( + content::Page& page) + : content::PageUserData(page), + hashed_main_frame_host_(HashMainFrameHostForStorage( + page.GetMainDocument().GetLastCommittedOrigin().host())) { + DCHECK(page.IsPrimary()); + + // TODO(yaoxia): consider dropping the permissions policy checks. We require + // that the API is used in the page, and that already implies that the + // permissions policy is allowed. + + if ((page.GetMainDocument().IsLastCommitIPAddressPubliclyRoutable() || + base::FeatureList::IsEnabled( + blink::features::kBrowsingTopicsBypassIPIsPubliclyRoutableCheck)) && + page.GetMainDocument().IsFeatureEnabled( + blink::mojom::PermissionsPolicyFeature::kBrowsingTopics) && + page.GetMainDocument().IsFeatureEnabled( + blink::mojom::PermissionsPolicyFeature:: + kBrowsingTopicsBackwardCompatible)) { + eligible_to_commit_ = true; + } +} + +void BrowsingTopicsPageLoadDataTracker::OnBrowsingTopicsApiUsed( + const HashedDomain& hashed_context_domain, + history::HistoryService* history_service) { + if (!eligible_to_commit_) + return; + + // On the first API usage in the page, set the allowed bit in history. + if (observed_hashed_context_domains_.empty()) { + content::WebContents* web_contents = + content::WebContents::FromRenderFrameHost(&page().GetMainDocument()); + + history_service->SetBrowsingTopicsAllowed( + history::ContextIDForWebContents(web_contents), + web_contents->GetController().GetLastCommittedEntry()->GetUniqueID(), + web_contents->GetLastCommittedURL()); + } + + // Ignore this context if we've already added it. + if (observed_hashed_context_domains_.count(hashed_context_domain)) + return; + + // Cap the number of context domains per page load. This is used to limit + // disk memory usage. + if (observed_hashed_context_domains_.size() >= + static_cast( + blink::features:: + kBrowsingTopicsMaxNumberOfApiUsageContextDomainsToStorePerPageLoad + .Get())) { + return; + } + + // Persist the usage now rather than at the end of the page load, as when the + // app enters background, it may be killed without further notification. + page() + .GetMainDocument() + .GetProcess() + ->GetStoragePartition() + ->GetBrowsingTopicsSiteDataManager() + ->OnBrowsingTopicsApiUsed(hashed_main_frame_host_, + {hashed_context_domain}, base::Time::Now()); + + observed_hashed_context_domains_.insert(hashed_context_domain); +} + +PAGE_USER_DATA_KEY_IMPL(BrowsingTopicsPageLoadDataTracker); + +} // namespace browsing_topics diff --git a/components/browsing_topics/browsing_topics_page_load_data_tracker.h b/components/browsing_topics/browsing_topics_page_load_data_tracker.h new file mode 100644 index 00000000000000..256efbd4f96f82 --- /dev/null +++ b/components/browsing_topics/browsing_topics_page_load_data_tracker.h @@ -0,0 +1,53 @@ +// Copyright 2022 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 COMPONENTS_BROWSING_TOPICS_BROWSING_TOPICS_PAGE_LOAD_DATA_TRACKER_H_ +#define COMPONENTS_BROWSING_TOPICS_BROWSING_TOPICS_PAGE_LOAD_DATA_TRACKER_H_ + +#include "base/containers/flat_set.h" +#include "components/browsing_topics/common/common_types.h" +#include "content/public/browser/page_user_data.h" + +namespace history { +class HistoryService; +} // namespace history + +namespace browsing_topics { + +// Tracks page-level (i.e. primary main frame document) signals to determine +// whether the page is eligible to be included in browsing topics calculation. +// Also tracks the context domains that have used the Topics API in the page. +class BrowsingTopicsPageLoadDataTracker + : public content::PageUserData { + public: + BrowsingTopicsPageLoadDataTracker(const BrowsingTopicsPageLoadDataTracker&) = + delete; + BrowsingTopicsPageLoadDataTracker& operator=( + const BrowsingTopicsPageLoadDataTracker&) = delete; + + ~BrowsingTopicsPageLoadDataTracker() override; + + // Called when the document.browsingTopics() API is used in the page. + void OnBrowsingTopicsApiUsed(const HashedDomain& hashed_context_domain, + history::HistoryService* history_service); + + private: + friend class PageUserData; + + explicit BrowsingTopicsPageLoadDataTracker(content::Page& page); + + // |eligible_to_commit_| means all the commit time prerequisites are met + // (i.e. IP was publicly routable AND permissions policy is "allow"). + bool eligible_to_commit_ = false; + + HashedHost hashed_main_frame_host_; + + base::flat_set observed_hashed_context_domains_; + + PAGE_USER_DATA_KEY_DECL(); +}; + +} // namespace browsing_topics + +#endif // COMPONENTS_BROWSING_TOPICS_BROWSING_TOPICS_PAGE_LOAD_DATA_TRACKER_H_ diff --git a/components/browsing_topics/browsing_topics_page_load_data_tracker_unittest.cc b/components/browsing_topics/browsing_topics_page_load_data_tracker_unittest.cc new file mode 100644 index 00000000000000..131f839e7a1c95 --- /dev/null +++ b/components/browsing_topics/browsing_topics_page_load_data_tracker_unittest.cc @@ -0,0 +1,266 @@ +// Copyright 2022 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 "components/browsing_topics/browsing_topics_page_load_data_tracker.h" + +#include "base/memory/raw_ptr.h" +#include "base/test/bind.h" +#include "base/test/scoped_feature_list.h" +#include "components/browsing_topics/test_util.h" +#include "components/history/content/browser/history_context_helper.h" +#include "components/history/core/browser/history_database_params.h" +#include "components/history/core/browser/history_service.h" +#include "components/history/core/test/test_history_database.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/test/browsing_topics_test_util.h" +#include "content/public/test/navigation_simulator.h" +#include "content/public/test/test_utils.h" +#include "content/public/test/web_contents_tester.h" +#include "content/test/test_render_view_host.h" + +namespace browsing_topics { + +class BrowsingTopicsPageLoadDataTrackerTest + : public content::RenderViewHostTestHarness { + public: + BrowsingTopicsPageLoadDataTrackerTest() { + scoped_feature_list_.InitWithFeatures( + /*enabled_features=*/{blink::features::kBrowsingTopics}, + /*disabled_features=*/{}); + + EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); + + history_service_ = std::make_unique(); + history_service_->Init( + history::TestHistoryDatabaseParamsForPath(temp_dir_.GetPath())); + } + + ~BrowsingTopicsPageLoadDataTrackerTest() override = default; + + void TearDown() override { + DCHECK(history_service_); + + base::RunLoop run_loop; + history_service_->SetOnBackendDestroyTask(run_loop.QuitClosure()); + history_service_.reset(); + run_loop.Run(); + + content::RenderViewHostTestHarness::TearDown(); + } + + void NavigateToPage(const GURL& url, + bool publicly_routable, + bool browsing_topics_permissions_policy_allowed, + bool interest_cohort_permissions_policy_allowed) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + url, web_contents()); + simulator->SetTransition(ui::PageTransition::PAGE_TRANSITION_TYPED); + + if (!publicly_routable) { + net::IPAddress address; + EXPECT_TRUE(address.AssignFromIPLiteral("0.0.0.0")); + simulator->SetSocketAddress(net::IPEndPoint(address, /*port=*/0)); + } + + blink::ParsedPermissionsPolicy policy; + + if (!browsing_topics_permissions_policy_allowed) { + policy.emplace_back( + blink::mojom::PermissionsPolicyFeature::kBrowsingTopics, + /*values=*/std::vector(), /*matches_all_origins=*/false, + /*matches_opaque_src=*/false); + } + + if (!interest_cohort_permissions_policy_allowed) { + policy.emplace_back(blink::mojom::PermissionsPolicyFeature:: + kBrowsingTopicsBackwardCompatible, + /*values=*/std::vector(), + /*matches_all_origins=*/false, + /*matches_opaque_src=*/false); + } + + simulator->SetPermissionsPolicyHeader(std::move(policy)); + + simulator->Commit(); + + history_service_->AddPage( + url, base::Time::Now(), + history::ContextIDForWebContents(web_contents()), + web_contents()->GetController().GetLastCommittedEntry()->GetUniqueID(), + /*referrer=*/GURL(), + /*redirects=*/{}, ui::PageTransition::PAGE_TRANSITION_TYPED, + history::VisitSource::SOURCE_BROWSED, + /*did_replace_entry=*/false, + /*floc_allowed=*/false); + } + + BrowsingTopicsPageLoadDataTracker* GetBrowsingTopicsPageLoadDataTracker() { + return BrowsingTopicsPageLoadDataTracker::GetOrCreateForPage( + web_contents()->GetMainFrame()->GetPage()); + } + + content::BrowsingTopicsSiteDataManager* topics_site_data_manager() { + return web_contents() + ->GetMainFrame() + ->GetProcess() + ->GetStoragePartition() + ->GetBrowsingTopicsSiteDataManager(); + } + + protected: + base::test::ScopedFeatureList scoped_feature_list_; + + std::unique_ptr history_service_; + + base::ScopedTempDir temp_dir_; +}; + +TEST_F(BrowsingTopicsPageLoadDataTrackerTest, OneUsage) { + GURL url("https://foo.com"); + NavigateToPage(url, /*publicly_routable=*/true, + /*browsing_topics_permissions_policy_allowed=*/true, + /*interest_cohort_permissions_policy_allowed=*/true); + + EXPECT_FALSE(BrowsingTopicsEligibleForURLVisit(history_service_.get(), url)); + EXPECT_TRUE( + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).empty()); + + GetBrowsingTopicsPageLoadDataTracker()->OnBrowsingTopicsApiUsed( + HashedDomain(123), history_service_.get()); + + EXPECT_TRUE(BrowsingTopicsEligibleForURLVisit(history_service_.get(), url)); + + std::vector api_usage_contexts = + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()); + EXPECT_EQ(api_usage_contexts.size(), 1u); + EXPECT_EQ(api_usage_contexts[0].hashed_main_frame_host, + HashMainFrameHostForStorage("foo.com")); + EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, HashedDomain(123)); +} + +TEST_F(BrowsingTopicsPageLoadDataTrackerTest, TwoUsages) { + GURL url("https://foo.com"); + NavigateToPage(url, /*publicly_routable=*/true, + /*browsing_topics_permissions_policy_allowed=*/true, + /*interest_cohort_permissions_policy_allowed=*/true); + + GetBrowsingTopicsPageLoadDataTracker()->OnBrowsingTopicsApiUsed( + HashedDomain(123), history_service_.get()); + GetBrowsingTopicsPageLoadDataTracker()->OnBrowsingTopicsApiUsed( + HashedDomain(456), history_service_.get()); + + EXPECT_TRUE(BrowsingTopicsEligibleForURLVisit(history_service_.get(), url)); + + std::vector api_usage_contexts = + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()); + EXPECT_EQ(api_usage_contexts.size(), 2u); + EXPECT_EQ(api_usage_contexts[0].hashed_main_frame_host, + HashMainFrameHostForStorage("foo.com")); + EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, HashedDomain(123)); + EXPECT_EQ(api_usage_contexts[1].hashed_main_frame_host, + HashMainFrameHostForStorage("foo.com")); + EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, HashedDomain(456)); +} + +TEST_F(BrowsingTopicsPageLoadDataTrackerTest, DuplicateDomains) { + GURL url("https://foo.com"); + NavigateToPage(url, /*publicly_routable=*/true, + /*browsing_topics_permissions_policy_allowed=*/true, + /*interest_cohort_permissions_policy_allowed=*/true); + + GetBrowsingTopicsPageLoadDataTracker()->OnBrowsingTopicsApiUsed( + HashedDomain(123), history_service_.get()); + GetBrowsingTopicsPageLoadDataTracker()->OnBrowsingTopicsApiUsed( + HashedDomain(456), history_service_.get()); + GetBrowsingTopicsPageLoadDataTracker()->OnBrowsingTopicsApiUsed( + HashedDomain(123), history_service_.get()); + + EXPECT_TRUE(BrowsingTopicsEligibleForURLVisit(history_service_.get(), url)); + + std::vector api_usage_contexts = + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()); + EXPECT_EQ(api_usage_contexts.size(), 2u); + EXPECT_EQ(api_usage_contexts[0].hashed_main_frame_host, + HashMainFrameHostForStorage("foo.com")); + EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, HashedDomain(123)); + EXPECT_EQ(api_usage_contexts[1].hashed_main_frame_host, + HashMainFrameHostForStorage("foo.com")); + EXPECT_EQ(api_usage_contexts[1].hashed_context_domain, HashedDomain(456)); + + // The second HashedDomain(123) shouldn't update the database. Verify this by + // verifying that the timestamp for HashedDomain(123) is no greater than the + // timestamp for HashedDomain(456). + EXPECT_LE(api_usage_contexts[0].time, api_usage_contexts[1].time); +} + +TEST_F(BrowsingTopicsPageLoadDataTrackerTest, NumberOfDomainsExceedsLimit) { + GURL url("https://foo.com"); + NavigateToPage(url, /*publicly_routable=*/true, + /*browsing_topics_permissions_policy_allowed=*/true, + /*interest_cohort_permissions_policy_allowed=*/true); + + for (int i = 0; i < 31; ++i) { + GetBrowsingTopicsPageLoadDataTracker()->OnBrowsingTopicsApiUsed( + HashedDomain(i), history_service_.get()); + } + + EXPECT_TRUE(BrowsingTopicsEligibleForURLVisit(history_service_.get(), url)); + + std::vector api_usage_contexts = + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()); + + EXPECT_EQ(api_usage_contexts.size(), 30u); + + for (int i = 0; i < 30; ++i) { + EXPECT_EQ(api_usage_contexts[i].hashed_main_frame_host, + HashMainFrameHostForStorage("foo.com")); + EXPECT_EQ(api_usage_contexts[i].hashed_context_domain, HashedDomain(i)); + } +} + +TEST_F(BrowsingTopicsPageLoadDataTrackerTest, NotPubliclyRoutable) { + GURL url("https://foo.com"); + NavigateToPage(url, /*publicly_routable=*/false, + /*browsing_topics_permissions_policy_allowed=*/true, + /*interest_cohort_permissions_policy_allowed=*/true); + + GetBrowsingTopicsPageLoadDataTracker()->OnBrowsingTopicsApiUsed( + HashedDomain(123), history_service_.get()); + + EXPECT_FALSE(BrowsingTopicsEligibleForURLVisit(history_service_.get(), url)); + EXPECT_TRUE( + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).empty()); +} + +TEST_F(BrowsingTopicsPageLoadDataTrackerTest, + BrowsingTopicsPermissionsPolicyNotAllowed) { + GURL url("https://foo.com"); + NavigateToPage(url, /*publicly_routable=*/true, + /*browsing_topics_permissions_policy_allowed=*/false, + /*interest_cohort_permissions_policy_allowed=*/true); + + GetBrowsingTopicsPageLoadDataTracker()->OnBrowsingTopicsApiUsed( + HashedDomain(123), history_service_.get()); + + EXPECT_FALSE(BrowsingTopicsEligibleForURLVisit(history_service_.get(), url)); + EXPECT_TRUE( + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).empty()); +} + +TEST_F(BrowsingTopicsPageLoadDataTrackerTest, + InterestCohortPermissionsPolicyNotAllowed) { + GURL url("https://foo.com"); + NavigateToPage(url, /*publicly_routable=*/true, + /*browsing_topics_permissions_policy_allowed=*/true, + /*interest_cohort_permissions_policy_allowed=*/false); + + GetBrowsingTopicsPageLoadDataTracker()->OnBrowsingTopicsApiUsed( + HashedDomain(123), history_service_.get()); + + EXPECT_FALSE(BrowsingTopicsEligibleForURLVisit(history_service_.get(), url)); + EXPECT_TRUE( + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).empty()); +} + +} // namespace browsing_topics diff --git a/components/browsing_topics/browsing_topics_service.h b/components/browsing_topics/browsing_topics_service.h index 9dc40e929e9313..b0faf1f0ffd52d 100644 --- a/components/browsing_topics/browsing_topics_service.h +++ b/components/browsing_topics/browsing_topics_service.h @@ -7,6 +7,8 @@ #include "components/keyed_service/core/keyed_service.h" #include "components/privacy_sandbox/canonical_topic.h" +#include "content/public/browser/render_frame_host.h" +#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom-forward.h" #include "url/origin.h" namespace browsing_topics { @@ -15,6 +17,14 @@ namespace browsing_topics { // to other internal components (e.g. UX). class BrowsingTopicsService : public KeyedService { public: + // Return the browsing topics for a particular requesting context. The + // calling context and top context information will also be used for the + // access permission check, and for the `BrowsingTopicsPageLoadDataTracker` to + // track the API usage. + virtual std::vector GetBrowsingTopicsForJsApi( + const url::Origin& context_origin, + content::RenderFrameHost* main_frame) = 0; + // Return the topics (i.e. one topic from each epoch) that can be potentially // exposed to a given site. Up to `kBrowsingTopicsNumberOfEpochsToExpose` // epochs' topics can be returned. diff --git a/components/browsing_topics/browsing_topics_service_impl.cc b/components/browsing_topics/browsing_topics_service_impl.cc index bd8ec9fb2ae7b7..3da4b7d26b9974 100644 --- a/components/browsing_topics/browsing_topics_service_impl.cc +++ b/components/browsing_topics/browsing_topics_service_impl.cc @@ -4,21 +4,366 @@ #include "components/browsing_topics/browsing_topics_service_impl.h" +#include + +#include "base/rand_util.h" +#include "components/browsing_topics/browsing_topics_calculator.h" +#include "components/browsing_topics/browsing_topics_page_load_data_tracker.h" +#include "components/browsing_topics/util.h" +#include "components/optimization_guide/content/browser/page_content_annotations_service.h" +#include "content/public/browser/browsing_topics_site_data_manager.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include "third_party/blink/public/common/features.h" +#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" + namespace browsing_topics { +namespace { + +bool ShouldClearTopicsOnTopicsDataAccessibleSinceUpdated( + const BrowsingTopicsState& browsing_topics_state, + base::Time browsing_topics_data_accessible_since) { + // Here we rely on the fact that `browsing_topics_data_accessible_since` can + // only be updated to base::Time::Now() due to data deletion. So we'll either + // need to clear all topics data, or no-op. If this assumption no longer + // holds, we'd need to iterate over all epochs, check their calculation time, + // and selectively delete the epochs. + return !browsing_topics_state.epochs().empty() && + browsing_topics_data_accessible_since > + browsing_topics_state.epochs().back().calculation_time(); +} + +struct StartupCalculateDecision { + bool clear_topics_data = true; + base::TimeDelta next_calculation_delay; +}; + +StartupCalculateDecision GetStartupCalculationDecision( + const BrowsingTopicsState& browsing_topics_state, + base::Time browsing_topics_data_accessible_since) { + // The topics have never been calculated. This could happen with a fresh + // profile or the if the config has updated. In case of a config update, the + // topics should have already been cleared when initializing the + // `BrowsingTopicsState`. + if (browsing_topics_state.next_scheduled_calculation_time().is_null()) { + return StartupCalculateDecision{ + .clear_topics_data = false, + .next_calculation_delay = base::TimeDelta()}; + } + + // This could happen when clear-on-exit is turned on and has caused the + // cookies to be deleted on startup. + bool should_clear_topics_data = + ShouldClearTopicsOnTopicsDataAccessibleSinceUpdated( + browsing_topics_state, browsing_topics_data_accessible_since); + + base::TimeDelta presumed_next_calculation_delay = + browsing_topics_state.next_scheduled_calculation_time() - + base::Time::Now(); + + // The scheduled calculation time was reached before the startup. + if (presumed_next_calculation_delay <= base::TimeDelta()) { + return StartupCalculateDecision{ + .clear_topics_data = should_clear_topics_data, + .next_calculation_delay = base::TimeDelta()}; + } + + // This could happen if the machine time has changed since the last + // calculation. Recalculate immediately to align with the expected schedule + // rather than potentially stop computing for a very long time. + if (presumed_next_calculation_delay >= + 2 * blink::features::kBrowsingTopicsTimePeriodPerEpoch.Get()) { + return StartupCalculateDecision{ + .clear_topics_data = should_clear_topics_data, + .next_calculation_delay = base::TimeDelta()}; + } + + return StartupCalculateDecision{ + .clear_topics_data = should_clear_topics_data, + .next_calculation_delay = presumed_next_calculation_delay}; +} + +} // namespace + BrowsingTopicsServiceImpl::~BrowsingTopicsServiceImpl() = default; -BrowsingTopicsServiceImpl::BrowsingTopicsServiceImpl() = default; +BrowsingTopicsServiceImpl::BrowsingTopicsServiceImpl( + const base::FilePath& profile_path, + privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings, + history::HistoryService* history_service, + content::BrowsingTopicsSiteDataManager* site_data_manager, + optimization_guide::PageContentAnnotationsService* annotations_service) + : privacy_sandbox_settings_(privacy_sandbox_settings), + history_service_(history_service), + site_data_manager_(site_data_manager), + annotations_service_(annotations_service), + browsing_topics_state_( + profile_path, + base::BindOnce( + &BrowsingTopicsServiceImpl::OnBrowsingTopicsStateLoaded, + base::Unretained(this))) { + privacy_sandbox_settings_observation_.Observe(privacy_sandbox_settings); + history_service_observation_.Observe(history_service); + + // Request the model now, to be able to run BatchAnnotatePageTopics() later. + // No need to wait for the callback (i.e. this is the expected usage). + annotations_service_->RequestAndNotifyWhenModelAvailable( + optimization_guide::AnnotationType::kPageTopics, base::DoNothing()); +} + +std::vector +BrowsingTopicsServiceImpl::GetBrowsingTopicsForJsApi( + const url::Origin& context_origin, + content::RenderFrameHost* main_frame) { + if (!browsing_topics_state_loaded_) + return {}; + + if (!privacy_sandbox_settings_->IsTopicsAllowed()) + return {}; + + if (!privacy_sandbox_settings_->IsTopicsAllowedForContext( + context_origin.GetURL(), main_frame->GetLastCommittedOrigin())) { + return {}; + } + + std::string context_domain = + net::registry_controlled_domains::GetDomainAndRegistry( + context_origin.GetURL(), + net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); + + HashedDomain hashed_context_domain = HashContextDomainForStorage( + browsing_topics_state_.hmac_key(), context_domain); + + // Track the API usage context after the permissions check. + BrowsingTopicsPageLoadDataTracker::GetOrCreateForPage(main_frame->GetPage()) + ->OnBrowsingTopicsApiUsed(hashed_context_domain, history_service_); + + std::string top_domain = + net::registry_controlled_domains::GetDomainAndRegistry( + main_frame->GetLastCommittedOrigin().GetURL(), + net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); + + std::vector result_topics; + for (const EpochTopics* epoch : + browsing_topics_state_.EpochsForSite(top_domain)) { + absl::optional topic = epoch->TopicForSite( + top_domain, hashed_context_domain, browsing_topics_state_.hmac_key()); + + // Only add a non-empty topic to the result. + if (!topic) + continue; + + if (!privacy_sandbox_settings_->IsTopicAllowed( + privacy_sandbox::CanonicalTopic(*topic, + epoch->taxonomy_version()))) { + return {}; + } + + blink::mojom::EpochTopicPtr result_topic = blink::mojom::EpochTopic::New(); + result_topic->topic = topic.value().value(); + result_topic->config_version = base::StrCat( + {"chrome.", base::NumberToString( + blink::features::kBrowsingTopicsConfigVersion.Get())}); + result_topic->model_version = base::NumberToString(epoch->model_version()); + result_topic->taxonomy_version = + base::NumberToString(epoch->taxonomy_version()); + result_topic->version = base::StrCat({result_topic->config_version, ":", + result_topic->taxonomy_version, ":", + result_topic->model_version}); + result_topics.push_back(std::move(result_topic)); + } + + // Remove duplicate entries. + std::sort(result_topics.begin(), result_topics.end()); + result_topics.erase(std::unique(result_topics.begin(), result_topics.end()), + result_topics.end()); + + // Shuffle the entries. + base::RandomShuffle(result_topics.begin(), result_topics.end()); + + return result_topics; +} std::vector BrowsingTopicsServiceImpl::GetTopicsForSiteForDisplay( const url::Origin& top_origin) const { - return {}; + if (!browsing_topics_state_loaded_) + return {}; + + std::string top_domain = + net::registry_controlled_domains::GetDomainAndRegistry( + top_origin.GetURL(), + net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); + + std::vector result; + + for (const EpochTopics* epoch : + browsing_topics_state_.EpochsForSite(top_domain)) { + absl::optional topic = epoch->TopicForSiteNoFiltering( + top_domain, browsing_topics_state_.hmac_key()); + + if (!topic) + continue; + + result.emplace_back(*topic, epoch->taxonomy_version()); + } + + return result; } std::vector BrowsingTopicsServiceImpl::GetTopTopicsForDisplay() const { - return {}; + if (!browsing_topics_state_loaded_) + return {}; + + std::vector result; + + for (const EpochTopics& epoch : browsing_topics_state_.epochs()) { + for (const TopicAndDomains& topic : + epoch.top_topics_and_observing_domains()) { + if (!topic.IsValid()) + continue; + + result.emplace_back(topic.topic(), epoch.taxonomy_version()); + } + } + + return result; +} + +std::unique_ptr +BrowsingTopicsServiceImpl::CreateCalculator( + privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings, + history::HistoryService* history_service, + content::BrowsingTopicsSiteDataManager* site_data_manager, + optimization_guide::PageContentAnnotationsService* annotations_service, + BrowsingTopicsCalculator::CalculateCompletedCallback callback) { + return std::make_unique( + privacy_sandbox_settings, history_service, site_data_manager, + annotations_service, std::move(callback)); +} + +const BrowsingTopicsState& BrowsingTopicsServiceImpl::browsing_topics_state() { + return browsing_topics_state_; +} + +void BrowsingTopicsServiceImpl::ScheduleBrowsingTopicsCalculation( + base::TimeDelta delay) { + DCHECK(browsing_topics_state_loaded_); + + // `this` owns the timer, which is automatically cancelled on destruction, so + // base::Unretained(this) is safe. + schedule_calculate_timer_.Start( + FROM_HERE, delay, + base::BindOnce(&BrowsingTopicsServiceImpl::CalculateBrowsingTopics, + base::Unretained(this))); +} + +void BrowsingTopicsServiceImpl::CalculateBrowsingTopics() { + DCHECK(browsing_topics_state_loaded_); + + DCHECK(!topics_calculator_); + + // `this` owns `topics_calculator_` so `topics_calculator_` should not invoke + // the callback once it's destroyed. + topics_calculator_ = CreateCalculator( + privacy_sandbox_settings_, history_service_, site_data_manager_, + annotations_service_, + base::BindOnce( + &BrowsingTopicsServiceImpl::OnCalculateBrowsingTopicsCompleted, + base::Unretained(this))); +} + +void BrowsingTopicsServiceImpl::OnCalculateBrowsingTopicsCompleted( + EpochTopics epoch_topics) { + DCHECK(browsing_topics_state_loaded_); + + DCHECK(topics_calculator_); + topics_calculator_.reset(); + + browsing_topics_state_.AddEpoch(std::move(epoch_topics)); + browsing_topics_state_.UpdateNextScheduledCalculationTime(); + + ScheduleBrowsingTopicsCalculation( + blink::features::kBrowsingTopicsTimePeriodPerEpoch.Get()); +} + +void BrowsingTopicsServiceImpl::OnBrowsingTopicsStateLoaded() { + DCHECK(!browsing_topics_state_loaded_); + browsing_topics_state_loaded_ = true; + + base::Time browsing_topics_data_sccessible_since = + privacy_sandbox_settings_->TopicsDataAccessibleSince(); + + StartupCalculateDecision decision = GetStartupCalculationDecision( + browsing_topics_state_, browsing_topics_data_sccessible_since); + + if (decision.clear_topics_data) + browsing_topics_state_.ClearAllTopics(); + + site_data_manager_->ExpireDataBefore(browsing_topics_data_sccessible_since); + + ScheduleBrowsingTopicsCalculation(decision.next_calculation_delay); +} + +void BrowsingTopicsServiceImpl::Shutdown() { + privacy_sandbox_settings_observation_.Reset(); + history_service_observation_.Reset(); +} + +void BrowsingTopicsServiceImpl::OnTopicsDataAccessibleSinceUpdated() { + if (!browsing_topics_state_loaded_) + return; + + if (ShouldClearTopicsOnTopicsDataAccessibleSinceUpdated( + browsing_topics_state_, + privacy_sandbox_settings_->TopicsDataAccessibleSince())) { + browsing_topics_state_.ClearAllTopics(); + site_data_manager_->ExpireDataBefore( + privacy_sandbox_settings_->TopicsDataAccessibleSince()); + } + + // Abort the outstanding topics calculation and restart immediately. + if (topics_calculator_) { + DCHECK(!schedule_calculate_timer_.IsRunning()); + + topics_calculator_.reset(); + CalculateBrowsingTopics(); + } +} + +void BrowsingTopicsServiceImpl::OnURLsDeleted( + history::HistoryService* history_service, + const history::DeletionInfo& deletion_info) { + if (!browsing_topics_state_loaded_) + return; + + // Ignore invalid time_range. + if (!deletion_info.IsAllHistory() && !deletion_info.time_range().IsValid()) + return; + + for (size_t i = 0; i < browsing_topics_state_.epochs().size(); ++i) { + const EpochTopics& epoch_topics = browsing_topics_state_.epochs()[i]; + + if (epoch_topics.empty()) + continue; + + bool time_range_overlap = + epoch_topics.calculation_time() >= deletion_info.time_range().begin() && + DeriveHistoryDataStartTime(epoch_topics.calculation_time()) <= + deletion_info.time_range().end(); + + if (time_range_overlap) + browsing_topics_state_.ClearOneEpoch(i); + } + + // If there's an outstanding topics calculation, abort and restart it. + if (topics_calculator_) { + DCHECK(!schedule_calculate_timer_.IsRunning()); + + topics_calculator_.reset(); + CalculateBrowsingTopics(); + } } } // namespace browsing_topics diff --git a/components/browsing_topics/browsing_topics_service_impl.h b/components/browsing_topics/browsing_topics_service_impl.h index cd72258497ca63..0aaa0452fdf9ad 100644 --- a/components/browsing_topics/browsing_topics_service_impl.h +++ b/components/browsing_topics/browsing_topics_service_impl.h @@ -6,13 +6,35 @@ #define COMPONENTS_BROWSING_TOPICS_BROWSING_TOPICS_SERVICE_IMPL_H_ #include "base/memory/weak_ptr.h" +#include "base/scoped_observation.h" +#include "base/timer/timer.h" +#include "components/browsing_topics/browsing_topics_calculator.h" #include "components/browsing_topics/browsing_topics_service.h" +#include "components/browsing_topics/browsing_topics_state.h" +#include "components/history/core/browser/history_service.h" +#include "components/history/core/browser/history_service_observer.h" +#include "components/privacy_sandbox/privacy_sandbox_settings.h" + +namespace content { +class BrowsingTopicsSiteDataManager; +} // namespace content + +namespace optimization_guide { +class PageContentAnnotationsService; +} // namespace optimization_guide namespace browsing_topics { -// A profile keyed service for providing the topics to a requesting context or -// to other internal components (e.g. UX). -class BrowsingTopicsServiceImpl : public BrowsingTopicsService { +// A profile keyed service for scheduling browsing topics calculation, +// calculating the topics to give to a requesting context or to other internal +// components (e.g. UX), and handling relevant data deletion. Browsing topics +// calculation will happen periodically every time period of +// `kBrowsingTopicsTimePeriodPerEpoch`. See the `BrowsingTopicsCalculator` class +// for the calculation details. +class BrowsingTopicsServiceImpl + : public BrowsingTopicsService, + public privacy_sandbox::PrivacySandboxSettings::Observer, + public history::HistoryServiceObserver { public: BrowsingTopicsServiceImpl(const BrowsingTopicsServiceImpl&) = delete; BrowsingTopicsServiceImpl& operator=(const BrowsingTopicsServiceImpl&) = @@ -22,16 +44,109 @@ class BrowsingTopicsServiceImpl : public BrowsingTopicsService { ~BrowsingTopicsServiceImpl() override; + std::vector GetBrowsingTopicsForJsApi( + const url::Origin& context_origin, + content::RenderFrameHost* main_frame) override; + std::vector GetTopicsForSiteForDisplay( const url::Origin& top_origin) const override; std::vector GetTopTopicsForDisplay() const override; + protected: + // The following methods are marked protected so that they may be overridden + // by tests. + + virtual std::unique_ptr CreateCalculator( + privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings, + history::HistoryService* history_service, + content::BrowsingTopicsSiteDataManager* site_data_manager, + optimization_guide::PageContentAnnotationsService* annotations_service, + BrowsingTopicsCalculator::CalculateCompletedCallback callback); + + // Allow tests to access `browsing_topics_state_`. + virtual const BrowsingTopicsState& browsing_topics_state(); + + // privacy_sandbox::PrivacySandboxSettings::Observer: + // + // When the floc-accessible-since time is updated (due to e.g. cookies + // deletion), we'll invalidate the underlying browsing topics. + void OnTopicsDataAccessibleSinceUpdated() override; + + // history::HistoryServiceObserver: + // + // On history deletion, the top topics of history epochs will be invalidated + // if the deletion time range overlaps with the time range of the underlying + // data used to derive the topics. + void OnURLsDeleted(history::HistoryService* history_service, + const history::DeletionInfo& deletion_info) override; + + // Called when the outstanding calculation completes. It's going to reset + // `topics_calculator_`, add the new `epoch_topics` to `browsing_topics_`, and + // schedule the next calculation. + virtual void OnCalculateBrowsingTopicsCompleted(EpochTopics epoch_topics); + private: friend class BrowsingTopicsServiceFactory; + friend class BrowsingTopicsBrowserTest; + friend class TesterBrowsingTopicsService; + + BrowsingTopicsServiceImpl( + const base::FilePath& profile_path, + privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings, + history::HistoryService* history_service, + content::BrowsingTopicsSiteDataManager* site_data_manager, + optimization_guide::PageContentAnnotationsService* annotations_service); + + void ScheduleBrowsingTopicsCalculation(base::TimeDelta delay); + + // Initialize `topics_calculator_` to start calculating this epoch's top + // topics and context observed topics. + void CalculateBrowsingTopics(); + + // Set `browsing_topics_state_loaded_` to true. Start scheduling the topics + // calculation. + void OnBrowsingTopicsStateLoaded(); + + // KeyedService: + void Shutdown() override; + + // These pointers are safe to hold and use throughout the lifetime of + // `this`: + // - For `privacy_sandbox_settings_`, `history_service_` and + // `annotations_service_`: the dependency declared in + // `BrowsingTopicsServiceFactory`'s constructor guarantees that + // `BrowsingTopicsService` will be destroyed first before those depend-on + // services. + // - For `site_data_manager_`: it lives in the StoragePartition which lives + // in the BrowserContext, and thus outlives all BrowserContext's KeyedService. + raw_ptr privacy_sandbox_settings_; + raw_ptr history_service_; + raw_ptr site_data_manager_; + raw_ptr + annotations_service_; + + BrowsingTopicsState browsing_topics_state_; + + // Whether the `browsing_topics_state_` has finished loading. Before the + // loading finishes, accessor methods will use a default handling (i.e. return + // an empty value; skip usage tracking; ignore data deletions). This is fine + // in practice, as the loading should be reasonably fast, and normally the API + // usage or data deletion won't happen at the browser start. + bool browsing_topics_state_loaded_ = false; + + std::unique_ptr topics_calculator_; + + base::OneShotTimer schedule_calculate_timer_; + + base::ScopedObservation + privacy_sandbox_settings_observation_{this}; - BrowsingTopicsServiceImpl(); + base::ScopedObservation + history_service_observation_{this}; base::WeakPtrFactory weak_ptr_factory_{this}; }; diff --git a/components/browsing_topics/browsing_topics_service_impl_unittest.cc b/components/browsing_topics/browsing_topics_service_impl_unittest.cc new file mode 100644 index 00000000000000..1f5c02e1d065b4 --- /dev/null +++ b/components/browsing_topics/browsing_topics_service_impl_unittest.cc @@ -0,0 +1,1222 @@ +// Copyright 2022 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 "components/browsing_topics/browsing_topics_service_impl.h" + +#include "base/json/json_file_value_serializer.h" +#include "base/json/values_util.h" +#include "base/memory/raw_ptr.h" +#include "base/test/bind.h" +#include "base/test/scoped_feature_list.h" +#include "components/browsing_topics/test_util.h" +#include "components/browsing_topics/util.h" +#include "components/content_settings/core/browser/cookie_settings.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/history/core/browser/history_database_params.h" +#include "components/history/core/browser/history_service.h" +#include "components/history/core/test/test_history_database.h" +#include "components/optimization_guide/content/browser/page_content_annotations_service.h" +#include "components/optimization_guide/content/browser/test_page_content_annotator.h" +#include "components/optimization_guide/core/test_model_info_builder.h" +#include "components/optimization_guide/core/test_optimization_guide_model_provider.h" +#include "components/privacy_sandbox/privacy_sandbox_prefs.h" +#include "components/privacy_sandbox/privacy_sandbox_settings.h" +#include "components/privacy_sandbox/privacy_sandbox_test_util.h" +#include "components/sync_preferences/testing_pref_service_syncable.h" +#include "content/public/browser/browsing_topics_site_data_manager.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/test/browsing_topics_test_util.h" +#include "content/public/test/navigation_simulator.h" +#include "content/public/test/test_utils.h" +#include "content/public/test/web_contents_tester.h" +#include "content/test/test_render_view_host.h" +#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" + +namespace browsing_topics { + +namespace { + +constexpr base::TimeDelta kCalculatorDelay = base::Seconds(10); + +constexpr browsing_topics::HmacKey kTestKey = {1}; + +constexpr base::Time kTime1 = + base::Time::FromDeltaSinceWindowsEpoch(base::Days(1)); +constexpr base::Time kTime2 = + base::Time::FromDeltaSinceWindowsEpoch(base::Days(2)); + +constexpr size_t kTaxonomySize = 349; +constexpr int kTaxonomyVersion = 1; +constexpr int64_t kModelVersion = 5000000000LL; +constexpr size_t kPaddedTopTopicsStartIndex = 3; + +EpochTopics CreateTestEpochTopics( + const std::vector>>& topics, + base::Time calculation_time) { + DCHECK_EQ(topics.size(), 5u); + + std::vector top_topics_and_observing_domains; + for (size_t i = 0; i < 5; ++i) { + top_topics_and_observing_domains.emplace_back(topics[i].first, + topics[i].second); + } + + return EpochTopics(std::move(top_topics_and_observing_domains), + kPaddedTopTopicsStartIndex, kTaxonomySize, + kTaxonomyVersion, kModelVersion, calculation_time); +} + +} // namespace + +// A tester class that allows mocking the topics calculators (i.e. the result +// and the finish delay). +class TesterBrowsingTopicsService : public BrowsingTopicsServiceImpl { + public: + TesterBrowsingTopicsService( + const base::FilePath& profile_path, + privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings, + history::HistoryService* history_service, + content::BrowsingTopicsSiteDataManager* site_data_manager, + optimization_guide::PageContentAnnotationsService* annotations_service, + base::queue mock_calculator_results, + base::TimeDelta calculator_finish_delay) + : BrowsingTopicsServiceImpl(profile_path, + privacy_sandbox_settings, + history_service, + site_data_manager, + annotations_service), + mock_calculator_results_(std::move(mock_calculator_results)), + calculator_finish_delay_(calculator_finish_delay) {} + + ~TesterBrowsingTopicsService() override = default; + + TesterBrowsingTopicsService(const TesterBrowsingTopicsService&) = delete; + TesterBrowsingTopicsService& operator=(const TesterBrowsingTopicsService&) = + delete; + TesterBrowsingTopicsService(TesterBrowsingTopicsService&&) = delete; + TesterBrowsingTopicsService& operator=(TesterBrowsingTopicsService&&) = + delete; + + std::unique_ptr CreateCalculator( + privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings, + history::HistoryService* history_service, + content::BrowsingTopicsSiteDataManager* site_data_manager, + optimization_guide::PageContentAnnotationsService* annotations_service, + BrowsingTopicsCalculator::CalculateCompletedCallback callback) override { + DCHECK(!mock_calculator_results_.empty()); + + ++started_calculations_count_; + + EpochTopics next_epoch = std::move(mock_calculator_results_.front()); + mock_calculator_results_.pop(); + + return std::make_unique( + std::move(callback), std::move(next_epoch), calculator_finish_delay_); + } + + const BrowsingTopicsState& browsing_topics_state() override { + return BrowsingTopicsServiceImpl::browsing_topics_state(); + } + + void OnTopicsDataAccessibleSinceUpdated() override { + BrowsingTopicsServiceImpl::OnTopicsDataAccessibleSinceUpdated(); + } + + void OnURLsDeleted(history::HistoryService* history_service, + const history::DeletionInfo& deletion_info) override { + BrowsingTopicsServiceImpl::OnURLsDeleted(history_service, deletion_info); + } + + // The number of calculations that have started, including those that have + // finished, those that are ongoing, and those that have been canceled. + size_t started_calculations_count() const { + return started_calculations_count_; + } + + private: + base::queue mock_calculator_results_; + base::TimeDelta calculator_finish_delay_; + + size_t started_calculations_count_ = 0u; +}; + +class BrowsingTopicsServiceImplTest + : public content::RenderViewHostTestHarness { + public: + BrowsingTopicsServiceImplTest() + : content::RenderViewHostTestHarness( + base::test::TaskEnvironment::TimeSource::MOCK_TIME) { + scoped_feature_list_.InitWithFeatures( + /*enabled_features=*/{blink::features::kBrowsingTopics}, + /*disabled_features=*/{}); + + OverrideHmacKeyForTesting(kTestKey); + + EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); + + content_settings::CookieSettings::RegisterProfilePrefs(prefs_.registry()); + HostContentSettingsMap::RegisterProfilePrefs(prefs_.registry()); + privacy_sandbox::RegisterProfilePrefs(prefs_.registry()); + + host_content_settings_map_ = base::MakeRefCounted( + &prefs_, /*is_off_the_record=*/false, /*store_last_modified=*/false, + /*restore_session=*/false); + cookie_settings_ = base::MakeRefCounted( + host_content_settings_map_.get(), &prefs_, false, "chrome-extension"); + + auto privacy_sandbox_delegate = std::make_unique< + privacy_sandbox_test_util::MockPrivacySandboxSettingsDelegate>(); + privacy_sandbox_delegate->SetupDefaultResponse(/*restricted=*/false, + /*confirmed=*/true); + privacy_sandbox_settings_ = + std::make_unique( + std::move(privacy_sandbox_delegate), + host_content_settings_map_.get(), cookie_settings_, &prefs_, + /*incognito_profile=*/false); + + history_service_ = std::make_unique(); + history_service_->Init( + history::TestHistoryDatabaseParamsForPath(temp_dir_.GetPath())); + + optimization_guide_model_provider_ = std::make_unique< + optimization_guide::TestOptimizationGuideModelProvider>(); + page_content_annotations_service_ = + std::make_unique( + "en-US", optimization_guide_model_provider_.get(), + history_service_.get(), nullptr, base::FilePath(), nullptr); + + page_content_annotations_service_->OverridePageContentAnnotatorForTesting( + &test_page_content_annotator_); + + task_environment()->RunUntilIdle(); + } + + ~BrowsingTopicsServiceImplTest() override = default; + + void TearDown() override { + DCHECK(history_service_); + + browsing_topics_service_.reset(); + + base::RunLoop run_loop; + history_service_->SetOnBackendDestroyTask(run_loop.QuitClosure()); + history_service_->Shutdown(); + run_loop.Run(); + + page_content_annotations_service_.reset(); + optimization_guide_model_provider_.reset(); + task_environment()->RunUntilIdle(); + + host_content_settings_map_->ShutdownOnUIThread(); + + content::RenderViewHostTestHarness::TearDown(); + } + + void NavigateToPage(const GURL& url) { + auto simulator = content::NavigationSimulator::CreateBrowserInitiated( + url, web_contents()); + simulator->SetTransition(ui::PageTransition::PAGE_TRANSITION_TYPED); + simulator->Commit(); + } + + content::BrowsingTopicsSiteDataManager* topics_site_data_manager() { + return web_contents() + ->GetMainFrame() + ->GetProcess() + ->GetStoragePartition() + ->GetBrowsingTopicsSiteDataManager(); + } + + base::FilePath BrowsingTopicsStateFilePath() { + return temp_dir_.GetPath().Append(FILE_PATH_LITERAL("BrowsingTopicsState")); + } + + void CreateBrowsingTopicsStateFile( + const std::vector& epochs, + base::Time next_scheduled_calculation_time) { + base::Value::List epochs_list; + for (const EpochTopics& epoch : epochs) { + epochs_list.Append(epoch.ToDictValue()); + } + + base::Value::Dict dict; + dict.Set("epochs", std::move(epochs_list)); + dict.Set("next_scheduled_calculation_time", + base::TimeToValue(next_scheduled_calculation_time)); + dict.Set("hex_encoded_hmac_key", base::HexEncode(kTestKey)); + dict.Set("config_version", 1); + + JSONFileValueSerializer(BrowsingTopicsStateFilePath()).Serialize(dict); + } + + void InitializeBrowsingTopicsService( + base::queue mock_calculator_results) { + browsing_topics_service_ = std::make_unique( + temp_dir_.GetPath(), privacy_sandbox_settings_.get(), + history_service_.get(), topics_site_data_manager(), + page_content_annotations_service_.get(), + std::move(mock_calculator_results), kCalculatorDelay); + } + + const BrowsingTopicsState& browsing_topics_state() { + DCHECK(browsing_topics_service_); + return browsing_topics_service_->browsing_topics_state(); + } + + HashedDomain GetHashedDomain(const std::string& domain) { + return HashContextDomainForStorage(kTestKey, domain); + } + + protected: + base::test::ScopedFeatureList scoped_feature_list_; + + base::ScopedTempDir temp_dir_; + + sync_preferences::TestingPrefServiceSyncable prefs_; + scoped_refptr host_content_settings_map_; + scoped_refptr cookie_settings_; + std::unique_ptr + privacy_sandbox_settings_; + + std::unique_ptr history_service_; + + std::unique_ptr + optimization_guide_model_provider_; + std::unique_ptr + page_content_annotations_service_; + + optimization_guide::TestPageContentAnnotator test_page_content_annotator_; + + std::unique_ptr browsing_topics_service_; +}; + +TEST_F(BrowsingTopicsServiceImplTest, EmptyInitialState_CalculationScheduling) { + base::Time start_time = base::Time::Now(); + + base::queue mock_calculator_results; + mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}}, + {Topic(2), {}}, + {Topic(3), {}}, + {Topic(4), {}}, + {Topic(5), {}}}, + kTime1)); + mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}}, + {Topic(7), {}}, + {Topic(8), {}}, + {Topic(9), {}}, + {Topic(10), {}}}, + kTime2)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 0u); + + // Finish file loading. + task_environment()->RunUntilIdle(); + + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u); + + EXPECT_TRUE(browsing_topics_state().epochs().empty()); + + // Finish the calculation. + task_environment()->FastForwardBy(kCalculatorDelay); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 1u); + EXPECT_EQ(browsing_topics_state().epochs()[0].calculation_time(), kTime1); + EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(), + start_time + kCalculatorDelay + base::Days(7)); + + // Advance the time to right before the next scheduled calculation. The next + // calculation should not happen. + task_environment()->FastForwardBy(base::Days(7) - base::Seconds(1)); + + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u); + + // Advance the time to the scheduled calculation time. A calculation should + // happen. + task_environment()->FastForwardBy(base::Seconds(1)); + + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 2u); + + // Finish the calculation. + task_environment()->FastForwardBy(kCalculatorDelay); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 2u); + EXPECT_EQ(browsing_topics_state().epochs()[1].calculation_time(), kTime2); + EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(), + start_time + 2 * kCalculatorDelay + 2 * base::Days(7)); +} + +TEST_F(BrowsingTopicsServiceImplTest, + StartFromPreexistingState_CalculateAtScheduledTime) { + base::Time start_time = base::Time::Now(); + + std::vector preexisting_epochs; + preexisting_epochs.push_back(CreateTestEpochTopics({{Topic(1), {}}, + {Topic(2), {}}, + {Topic(3), {}}, + {Topic(4), {}}, + {Topic(5), {}}}, + kTime1)); + + CreateBrowsingTopicsStateFile( + std::move(preexisting_epochs), + /*next_scheduled_calculation_time=*/start_time + base::Days(1)); + + base::queue mock_calculator_results; + mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}}, + {Topic(7), {}}, + {Topic(8), {}}, + {Topic(9), {}}, + {Topic(10), {}}}, + kTime2)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish file loading. + task_environment()->RunUntilIdle(); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 1u); + EXPECT_EQ(browsing_topics_state().epochs()[0].calculation_time(), kTime1); + EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(), + start_time + base::Days(1)); + + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 0u); + + // Advance the time to the scheduled calculation time. A calculation should + // happen. + task_environment()->FastForwardBy(base::Days(1)); + + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u); + // Finish the calculation. + task_environment()->FastForwardBy(kCalculatorDelay); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 2u); + EXPECT_EQ(browsing_topics_state().epochs()[1].calculation_time(), kTime2); + EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(), + start_time + base::Days(1) + kCalculatorDelay + base::Days(7)); +} + +TEST_F( + BrowsingTopicsServiceImplTest, + StartFromPreexistingState_ScheduledTimeReachedBeforeStartup_CalculateImmediately) { + base::Time start_time = base::Time::Now(); + + std::vector preexisting_epochs; + preexisting_epochs.push_back(CreateTestEpochTopics({{Topic(1), {}}, + {Topic(2), {}}, + {Topic(3), {}}, + {Topic(4), {}}, + {Topic(5), {}}}, + kTime1)); + + CreateBrowsingTopicsStateFile( + std::move(preexisting_epochs), + /*next_scheduled_calculation_time=*/start_time - base::Seconds(1)); + + base::queue mock_calculator_results; + mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}}, + {Topic(7), {}}, + {Topic(8), {}}, + {Topic(9), {}}, + {Topic(10), {}}}, + kTime2)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish file loading. + task_environment()->RunUntilIdle(); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 1u); + EXPECT_EQ(browsing_topics_state().epochs()[0].calculation_time(), kTime1); + EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(), + start_time - base::Seconds(1)); + + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u); +} + +TEST_F( + BrowsingTopicsServiceImplTest, + StartFromPreexistingState_TopicsAccessibleSinceUpdated_ResetStateAndStorage_CalculateAtScheduledTime) { + base::Time start_time = base::Time::Now(); + + std::vector preexisting_epochs; + preexisting_epochs.push_back( + CreateTestEpochTopics({{Topic(1), {}}, + {Topic(2), {}}, + {Topic(3), {}}, + {Topic(4), {}}, + {Topic(5), {}}}, + start_time - base::Days(1))); + + // Add some arbitrary data to site data storage. The intent is just to test + // data deletion. + topics_site_data_manager()->OnBrowsingTopicsApiUsed( + HashMainFrameHostForStorage("a.com"), {HashedDomain(1)}, + base::Time::Now()); + + task_environment()->FastForwardBy(base::Seconds(1)); + privacy_sandbox_settings_->OnCookiesCleared(); + + EXPECT_EQ( + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(), + 1u); + + CreateBrowsingTopicsStateFile( + std::move(preexisting_epochs), + /*next_scheduled_calculation_time=*/start_time + base::Days(1)); + + base::queue mock_calculator_results; + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {}}, + {Topic(7), {}}, + {Topic(8), {}}, + {Topic(9), {}}, + {Topic(10), {}}}, + start_time - base::Days(1))); + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish file loading. + task_environment()->RunUntilIdle(); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 0u); + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 0u); + EXPECT_EQ( + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(), + 0u); +} + +TEST_F( + BrowsingTopicsServiceImplTest, + StartFromPreexistingState_UnexpectedNextCalculationDelay_ResetState_CalculateImmediately) { + base::Time start_time = base::Time::Now(); + + std::vector preexisting_epochs; + preexisting_epochs.push_back( + CreateTestEpochTopics({{Topic(1), {}}, + {Topic(2), {}}, + {Topic(3), {}}, + {Topic(4), {}}, + {Topic(5), {}}}, + start_time - base::Days(1))); + + privacy_sandbox_settings_->OnCookiesCleared(); + + CreateBrowsingTopicsStateFile( + std::move(preexisting_epochs), + /*next_scheduled_calculation_time=*/start_time + base::Days(15)); + + base::queue mock_calculator_results; + mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}}, + {Topic(7), {}}, + {Topic(8), {}}, + {Topic(9), {}}, + {Topic(10), {}}}, + kTime2)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish file loading. + task_environment()->RunUntilIdle(); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 0u); + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u); +} + +TEST_F(BrowsingTopicsServiceImplTest, + StartFromPreexistingState_DefaultHandlingBeforeLoadFinish) { + base::Time start_time = base::Time::Now(); + + std::vector preexisting_epochs; + preexisting_epochs.push_back( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + + CreateBrowsingTopicsStateFile( + std::move(preexisting_epochs), + /*next_scheduled_calculation_time=*/start_time + base::Days(1)); + + base::queue mock_calculator_results; + mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}}, + {Topic(7), {}}, + {Topic(8), {}}, + {Topic(9), {}}, + {Topic(10), {}}}, + kTime2)); + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + NavigateToPage(GURL("https://www.foo.com")); + + EXPECT_TRUE(browsing_topics_service_ + ->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create( + GURL("https://www.bar.com")), + web_contents()->GetMainFrame()) + .empty()); + EXPECT_TRUE(browsing_topics_service_ + ->GetTopicsForSiteForDisplay( + url::Origin::Create(GURL("https://www.bar.com"))) + .empty()); + EXPECT_TRUE(browsing_topics_service_->GetTopTopicsForDisplay().empty()); + + // Finish file loading. + task_environment()->RunUntilIdle(); + + EXPECT_FALSE(browsing_topics_service_ + ->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create( + GURL("https://www.bar.com")), + web_contents()->GetMainFrame()) + .empty()); + EXPECT_FALSE(browsing_topics_service_ + ->GetTopicsForSiteForDisplay( + url::Origin::Create(GURL("https://www.bar.com"))) + .empty()); + EXPECT_FALSE(browsing_topics_service_->GetTopTopicsForDisplay().empty()); +} + +TEST_F( + BrowsingTopicsServiceImplTest, + OnTopicsDataAccessibleSinceUpdated_ResetState_ClearTopicsSiteDataStorage) { + base::Time start_time = base::Time::Now(); + + base::queue mock_calculator_results; + mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}}, + {Topic(2), {}}, + {Topic(3), {}}, + {Topic(4), {}}, + {Topic(5), {}}}, + start_time)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {}}, + {Topic(7), {}}, + {Topic(8), {}}, + {Topic(9), {}}, + {Topic(10), {}}}, + start_time + kCalculatorDelay + base::Days(7))); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish file loading and two calculations. + task_environment()->FastForwardBy(2 * kCalculatorDelay + base::Days(7)); + + // Add some arbitrary data to site data storage. The intent is just to test + // data deletion. + topics_site_data_manager()->OnBrowsingTopicsApiUsed( + HashMainFrameHostForStorage("a.com"), {HashedDomain(1)}, + base::Time::Now()); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 2u); + EXPECT_EQ( + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(), + 1u); + + task_environment()->FastForwardBy(base::Seconds(1)); + privacy_sandbox_settings_->OnCookiesCleared(); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 0u); + EXPECT_EQ( + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(), + 0u); +} + +TEST_F(BrowsingTopicsServiceImplTest, + OnURLsDeleted_TimeRangeOverlapWithOneEpoch) { + base::Time start_time = base::Time::Now(); + + base::queue mock_calculator_results; + mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}}, + {Topic(2), {}}, + {Topic(3), {}}, + {Topic(4), {}}, + {Topic(5), {}}}, + start_time)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {}}, + {Topic(7), {}}, + {Topic(8), {}}, + {Topic(9), {}}, + {Topic(10), {}}}, + start_time + kCalculatorDelay + base::Days(7))); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish file loading and two calculations. + task_environment()->FastForwardBy(2 * kCalculatorDelay + base::Days(7)); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 2u); + EXPECT_FALSE(browsing_topics_state().epochs()[0].empty()); + EXPECT_FALSE(browsing_topics_state().epochs()[1].empty()); + + history::DeletionInfo deletion_info( + history::DeletionTimeRange(start_time + base::Days(5), + start_time + base::Days(6)), + /*is_from_expiration=*/false, /*deleted_rows=*/{}, /*favicon_urls=*/{}, + /*restrict_urls=*/absl::nullopt); + + browsing_topics_service_->OnURLsDeleted(history_service_.get(), + deletion_info); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 2u); + EXPECT_FALSE(browsing_topics_state().epochs()[0].empty()); + EXPECT_TRUE(browsing_topics_state().epochs()[1].empty()); +} + +TEST_F(BrowsingTopicsServiceImplTest, + OnURLsDeleted_TimeRangeOverlapWithAllEpochs) { + base::Time start_time = base::Time::Now(); + + base::queue mock_calculator_results; + mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}}, + {Topic(2), {}}, + {Topic(3), {}}, + {Topic(4), {}}, + {Topic(5), {}}}, + start_time)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {}}, + {Topic(7), {}}, + {Topic(8), {}}, + {Topic(9), {}}, + {Topic(10), {}}}, + start_time + kCalculatorDelay + base::Days(7))); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish file loading and two calculations. + task_environment()->FastForwardBy(2 * kCalculatorDelay + base::Days(7)); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 2u); + EXPECT_FALSE(browsing_topics_state().epochs()[0].empty()); + EXPECT_FALSE(browsing_topics_state().epochs()[1].empty()); + + history::DeletionInfo deletion_info( + history::DeletionTimeRange(start_time, start_time + base::Days(2)), + /*is_from_expiration=*/false, /*deleted_rows=*/{}, /*favicon_urls=*/{}, + /*restrict_urls=*/absl::nullopt); + + browsing_topics_service_->OnURLsDeleted(history_service_.get(), + deletion_info); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 2u); + EXPECT_TRUE(browsing_topics_state().epochs()[0].empty()); + EXPECT_TRUE(browsing_topics_state().epochs()[1].empty()); +} + +TEST_F(BrowsingTopicsServiceImplTest, Recalculate) { + base::Time start_time = base::Time::Now(); + + base::queue mock_calculator_results; + mock_calculator_results.push(CreateTestEpochTopics({{Topic(1), {}}, + {Topic(2), {}}, + {Topic(3), {}}, + {Topic(4), {}}, + {Topic(5), {}}}, + kTime1)); + mock_calculator_results.push(CreateTestEpochTopics({{Topic(6), {}}, + {Topic(7), {}}, + {Topic(8), {}}, + {Topic(9), {}}, + {Topic(10), {}}}, + kTime2)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + task_environment()->FastForwardBy(kCalculatorDelay - base::Seconds(1)); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 0u); + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 1u); + + // History deletion during a calculation should trigger the re-calculation. + history::DeletionInfo deletion_info( + history::DeletionTimeRange(start_time, start_time + base::Days(2)), + /*is_from_expiration=*/false, /*deleted_rows=*/{}, /*favicon_urls=*/{}, + /*restrict_urls=*/absl::nullopt); + browsing_topics_service_->OnURLsDeleted(history_service_.get(), + deletion_info); + + // The calculation shouldn't finish at the originally expected time, as it was + // dropped and a new calculation has started. + task_environment()->FastForwardBy(base::Seconds(1)); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 0u); + EXPECT_EQ(browsing_topics_service_->started_calculations_count(), 2u); + + // Finish the re-started calculation. + task_environment()->FastForwardBy(kCalculatorDelay - base::Seconds(1)); + EXPECT_EQ(browsing_topics_state().epochs().size(), 1u); + + // Expect that the result comes from the re-started calculator. + EXPECT_EQ(browsing_topics_state().epochs()[0].calculation_time(), kTime2); + EXPECT_EQ(browsing_topics_state().next_scheduled_calculation_time(), + base::Time::Now() + base::Days(7)); +} + +TEST_F(BrowsingTopicsServiceImplTest, + GetBrowsingTopicsForJsApi_PrivacySandboxSettingsDisabled) { + base::queue mock_calculator_results; + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + task_environment()->RunUntilIdle(); + + privacy_sandbox_settings_->SetPrivacySandboxEnabled(false); + + NavigateToPage(GURL("https://www.foo.com")); + + EXPECT_TRUE(browsing_topics_service_ + ->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create( + GURL("https://www.bar.com")), + web_contents()->GetMainFrame()) + .empty()); +} + +TEST_F(BrowsingTopicsServiceImplTest, GetBrowsingTopicsForJsApi_OneEpoch) { + base::queue mock_calculator_results; + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + task_environment()->FastForwardBy(kCalculatorDelay); + + NavigateToPage(GURL("https://www.foo.com")); + + // Current time is before the epoch switch time. + std::vector result = + browsing_topics_service_->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")), + web_contents()->GetMainFrame()); + + EXPECT_TRUE(result.empty()); + + // Advance to the time after the epoch switch time. + task_environment()->AdvanceClock(base::Days(7) - base::Seconds(1)); + + result = browsing_topics_service_->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")), + web_contents()->GetMainFrame()); + + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0]->topic, 2); + EXPECT_EQ(result[0]->config_version, "chrome.1"); + EXPECT_EQ(result[0]->taxonomy_version, "1"); + EXPECT_EQ(result[0]->model_version, "5000000000"); + EXPECT_EQ(result[0]->version, "chrome.1:1:5000000000"); +} + +TEST_F(BrowsingTopicsServiceImplTest, + GetBrowsingTopicsForJsApi_TopicNotAllowedByPrivacySandboxSettings) { + base::queue mock_calculator_results; + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + task_environment()->FastForwardBy(kCalculatorDelay); + + NavigateToPage(GURL("https://www.foo.com")); + + // Current time is before the epoch switch time. + std::vector result = + browsing_topics_service_->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")), + web_contents()->GetMainFrame()); + + EXPECT_TRUE(result.empty()); + + // Advance to the time after the epoch switch time. + task_environment()->AdvanceClock(base::Days(7) - base::Seconds(1)); + + privacy_sandbox_settings_->SetTopicAllowed( + privacy_sandbox::CanonicalTopic(Topic(2), /*taxonomy_version=*/1), false); + + result = browsing_topics_service_->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")), + web_contents()->GetMainFrame()); + + EXPECT_TRUE(result.empty()); +} + +TEST_F(BrowsingTopicsServiceImplTest, GetBrowsingTopicsForJsApi_FourEpochs) { + base::queue mock_calculator_results; + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}}, + {Topic(7), {GetHashedDomain("bar.com")}}, + {Topic(8), {GetHashedDomain("bar.com")}}, + {Topic(9), {GetHashedDomain("bar.com")}}, + {Topic(10), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(11), {GetHashedDomain("bar.com")}}, + {Topic(12), {GetHashedDomain("bar.com")}}, + {Topic(13), {GetHashedDomain("bar.com")}}, + {Topic(14), {GetHashedDomain("bar.com")}}, + {Topic(15), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(16), {GetHashedDomain("bar.com")}}, + {Topic(17), {GetHashedDomain("bar.com")}}, + {Topic(18), {GetHashedDomain("bar.com")}}, + {Topic(19), {GetHashedDomain("bar.com")}}, + {Topic(20), {GetHashedDomain("bar.com")}}}, + kTime1)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish all calculations. + task_environment()->FastForwardBy(4 * kCalculatorDelay + 3 * base::Days(7)); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 4u); + + NavigateToPage(GURL("https://www.foo.com")); + + // Current time is before the epoch switch time. + std::vector result = + browsing_topics_service_->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")), + web_contents()->GetMainFrame()); + + EXPECT_EQ(result.size(), 3u); + std::set result_set; + result_set.insert(result[0]->topic); + result_set.insert(result[1]->topic); + result_set.insert(result[2]->topic); + EXPECT_EQ(result_set, std::set({2, 7, 12})); + + // Advance to the time after the epoch switch time. + task_environment()->AdvanceClock(base::Days(7) - base::Seconds(1)); + + result = browsing_topics_service_->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")), + web_contents()->GetMainFrame()); + + EXPECT_EQ(result.size(), 3u); + result_set.clear(); + result_set.insert(result[0]->topic); + result_set.insert(result[1]->topic); + result_set.insert(result[2]->topic); + EXPECT_EQ(result_set, std::set({7, 12, 17})); +} + +TEST_F(BrowsingTopicsServiceImplTest, + GetBrowsingTopicsForJsApi_DuplicateTopicsRemoved) { + base::queue mock_calculator_results; + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}}, + {Topic(7), {GetHashedDomain("bar.com")}}, + {Topic(8), {GetHashedDomain("bar.com")}}, + {Topic(9), {GetHashedDomain("bar.com")}}, + {Topic(10), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}}, + {Topic(7), {GetHashedDomain("bar.com")}}, + {Topic(8), {GetHashedDomain("bar.com")}}, + {Topic(9), {GetHashedDomain("bar.com")}}, + {Topic(10), {GetHashedDomain("bar.com")}}}, + kTime1)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish all calculations. + task_environment()->FastForwardBy(4 * kCalculatorDelay + 3 * base::Days(7)); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 4u); + + NavigateToPage(GURL("https://www.foo.com")); + + // Current time is before the epoch switch time. + std::vector result = + browsing_topics_service_->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")), + web_contents()->GetMainFrame()); + + EXPECT_EQ(result.size(), 2u); + std::set result_set; + result_set.insert(result[0]->topic); + result_set.insert(result[1]->topic); + EXPECT_EQ(result_set, std::set({2, 7})); + + // Advance to the time after the epoch switch time. + task_environment()->AdvanceClock(base::Days(7) - base::Seconds(1)); + + result = browsing_topics_service_->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")), + web_contents()->GetMainFrame()); + + EXPECT_EQ(result.size(), 2u); + result_set.clear(); + result_set.insert(result[0]->topic); + result_set.insert(result[1]->topic); + EXPECT_EQ(result_set, std::set({2, 7})); +} + +// TODO(yaoxia): Re-enable. This test currently fails solely due to it's +// generating a lot of GMOCK WARNING output. +TEST_F(BrowsingTopicsServiceImplTest, + DISABLED_GetBrowsingTopicsForJsApi_TopicsReturnedInRandomOrder) { + base::queue mock_calculator_results; + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}}, + {Topic(7), {GetHashedDomain("bar.com")}}, + {Topic(8), {GetHashedDomain("bar.com")}}, + {Topic(9), {GetHashedDomain("bar.com")}}, + {Topic(10), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}}, + {Topic(7), {GetHashedDomain("bar.com")}}, + {Topic(8), {GetHashedDomain("bar.com")}}, + {Topic(9), {GetHashedDomain("bar.com")}}, + {Topic(10), {GetHashedDomain("bar.com")}}}, + kTime1)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish all calculations. + task_environment()->FastForwardBy(4 * kCalculatorDelay + 3 * base::Days(7)); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 4u); + + NavigateToPage(GURL("https://www.foo.com")); + + // Current time is before the epoch switch time. + + // Expect that in 100 rounds, both Topic(2) and Topic(7) should be seen being + // at the front. + bool seen_topic_2_at_front = false; + bool seen_topic_7_at_front = false; + + for (int i = 0; i < 100; ++i) { + std::vector result = + browsing_topics_service_->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")), + web_contents()->GetMainFrame()); + + EXPECT_EQ(result.size(), 2u); + std::set result_set; + result_set.insert(result[0]->topic); + result_set.insert(result[1]->topic); + EXPECT_EQ(result_set, std::set({2, 7})); + + seen_topic_2_at_front = seen_topic_2_at_front || (result[0]->topic == 2); + seen_topic_7_at_front = seen_topic_2_at_front || (result[0]->topic == 7); + } + + EXPECT_TRUE(seen_topic_2_at_front); + EXPECT_TRUE(seen_topic_7_at_front); +} + +TEST_F(BrowsingTopicsServiceImplTest, + GetBrowsingTopicsForJsApi_TrackedUsageContext) { + base::queue mock_calculator_results; + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + task_environment()->RunUntilIdle(); + + EXPECT_EQ( + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()).size(), + 0u); + + NavigateToPage(GURL("https://www.foo.com")); + browsing_topics_service_->GetBrowsingTopicsForJsApi( + /*context_origin=*/url::Origin::Create(GURL("https://www.bar.com")), + web_contents()->GetMainFrame()); + + std::vector api_usage_contexts = + content::GetBrowsingTopicsApiUsage(topics_site_data_manager()); + EXPECT_EQ(api_usage_contexts.size(), 1u); + EXPECT_EQ(api_usage_contexts[0].hashed_main_frame_host, + HashMainFrameHostForStorage("www.foo.com")); + EXPECT_EQ(api_usage_contexts[0].hashed_context_domain, + GetHashedDomain("bar.com")); +} + +TEST_F(BrowsingTopicsServiceImplTest, GetTopicsForSiteForDisplay) { + base::queue mock_calculator_results; + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}}, + {Topic(7), {GetHashedDomain("bar.com")}}, + {Topic(8), {GetHashedDomain("bar.com")}}, + {Topic(9), {GetHashedDomain("bar.com")}}, + {Topic(10), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}}, + {Topic(7), {GetHashedDomain("bar.com")}}, + {Topic(8), {GetHashedDomain("bar.com")}}, + {Topic(9), {GetHashedDomain("bar.com")}}, + {Topic(10), {GetHashedDomain("bar.com")}}}, + kTime1)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish all calculations. + task_environment()->FastForwardBy(4 * kCalculatorDelay + 3 * base::Days(7)); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 4u); + + NavigateToPage(GURL("https://www.foo.com")); + + // Current time is before the epoch switch time. + std::vector result = + browsing_topics_service_->GetTopicsForSiteForDisplay( + web_contents()->GetMainFrame()->GetLastCommittedOrigin()); + + EXPECT_EQ(result.size(), 3u); + EXPECT_EQ(result[0].topic_id(), Topic(2)); + EXPECT_EQ(result[1].topic_id(), Topic(7)); + EXPECT_EQ(result[2].topic_id(), Topic(2)); + EXPECT_EQ(result[0].taxonomy_version(), 1); + EXPECT_EQ(result[1].taxonomy_version(), 1); + EXPECT_EQ(result[2].taxonomy_version(), 1); +} + +TEST_F(BrowsingTopicsServiceImplTest, GetTopTopicsForDisplay) { + base::queue mock_calculator_results; + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}}, + {Topic(7), {GetHashedDomain("bar.com")}}, + {Topic(8), {GetHashedDomain("bar.com")}}, + {Topic(9), {GetHashedDomain("bar.com")}}, + {Topic(10), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(1), {GetHashedDomain("bar.com")}}, + {Topic(2), {GetHashedDomain("bar.com")}}, + {Topic(3), {GetHashedDomain("bar.com")}}, + {Topic(4), {GetHashedDomain("bar.com")}}, + {Topic(5), {GetHashedDomain("bar.com")}}}, + kTime1)); + mock_calculator_results.push( + CreateTestEpochTopics({{Topic(6), {GetHashedDomain("bar.com")}}, + {Topic(7), {GetHashedDomain("bar.com")}}, + {Topic(8), {GetHashedDomain("bar.com")}}, + {Topic(9), {GetHashedDomain("bar.com")}}, + {Topic(10), {GetHashedDomain("bar.com")}}}, + kTime1)); + + InitializeBrowsingTopicsService(std::move(mock_calculator_results)); + + // Finish all calculations. + task_environment()->FastForwardBy(4 * kCalculatorDelay + 3 * base::Days(7)); + + EXPECT_EQ(browsing_topics_state().epochs().size(), 4u); + + NavigateToPage(GURL("https://www.foo.com")); + + // Current time is before the epoch switch time. + std::vector result = + browsing_topics_service_->GetTopTopicsForDisplay(); + + EXPECT_EQ(result.size(), 20u); + EXPECT_EQ(result[0].topic_id(), Topic(1)); + EXPECT_EQ(result[1].topic_id(), Topic(2)); + EXPECT_EQ(result[2].topic_id(), Topic(3)); + EXPECT_EQ(result[3].topic_id(), Topic(4)); + EXPECT_EQ(result[4].topic_id(), Topic(5)); + EXPECT_EQ(result[5].topic_id(), Topic(6)); + EXPECT_EQ(result[6].topic_id(), Topic(7)); + EXPECT_EQ(result[7].topic_id(), Topic(8)); + EXPECT_EQ(result[8].topic_id(), Topic(9)); + EXPECT_EQ(result[9].topic_id(), Topic(10)); + EXPECT_EQ(result[10].topic_id(), Topic(1)); + EXPECT_EQ(result[11].topic_id(), Topic(2)); + EXPECT_EQ(result[12].topic_id(), Topic(3)); + EXPECT_EQ(result[13].topic_id(), Topic(4)); + EXPECT_EQ(result[14].topic_id(), Topic(5)); + EXPECT_EQ(result[15].topic_id(), Topic(6)); + EXPECT_EQ(result[16].topic_id(), Topic(7)); + EXPECT_EQ(result[17].topic_id(), Topic(8)); + EXPECT_EQ(result[18].topic_id(), Topic(9)); + EXPECT_EQ(result[19].topic_id(), Topic(10)); +} + +} // namespace browsing_topics diff --git a/components/browsing_topics/browsing_topics_state.cc b/components/browsing_topics/browsing_topics_state.cc index f0238f2a9f2c93..a8309414eb70c5 100644 --- a/components/browsing_topics/browsing_topics_state.cc +++ b/components/browsing_topics/browsing_topics_state.cc @@ -109,18 +109,75 @@ void BrowsingTopicsState::AddEpoch(EpochTopics epoch_topics) { ScheduleSave(); } -void BrowsingTopicsState::UpdateNextScheduledCalculationTime( - base::Time next_scheduled_calculation_time) { +void BrowsingTopicsState::UpdateNextScheduledCalculationTime() { DCHECK(loaded_); - next_scheduled_calculation_time_ = next_scheduled_calculation_time; + next_scheduled_calculation_time_ = + base::Time::Now() + + blink::features::kBrowsingTopicsTimePeriodPerEpoch.Get(); + ScheduleSave(); } +std::vector BrowsingTopicsState::EpochsForSite( + const std::string& top_domain) const { + DCHECK(loaded_); + + const size_t kNumberOfEpochsToExpose = static_cast( + blink::features::kBrowsingTopicsNumberOfEpochsToExpose.Get()); + + DCHECK_GT(kNumberOfEpochsToExpose, 0u); + + // Derive a per-user per-site time delta in the range of + // [0, kBrowsingTopicsTimePeriodPerEpoch). The latest epoch will be switched + // to use when the current time is within `site_sticky_time_delta` apart from + // the `next_scheduled_calculation_time_`. This way, each site will see a + // different epoch switch time. + base::TimeDelta site_sticky_time_delta = + CalculateSiteStickyTimeDelta(top_domain); + + size_t end_epoch_index = 0; + + if (base::Time::Now() + site_sticky_time_delta < + next_scheduled_calculation_time_) { + if (epochs_.size() < 2) + return {}; + + end_epoch_index = epochs_.size() - 2; + } else { + if (epochs_.empty()) + return {}; + + end_epoch_index = epochs_.size() - 1; + } + + size_t start_epoch_index = (end_epoch_index + 1 >= kNumberOfEpochsToExpose) + ? end_epoch_index + 1 - kNumberOfEpochsToExpose + : 0; + + std::vector result; + + for (size_t i = start_epoch_index; i <= end_epoch_index; ++i) { + result.emplace_back(&epochs_[i]); + } + + return result; +} + bool BrowsingTopicsState::HasScheduledSaveForTesting() const { return writer_.HasPendingWrite(); } +base::TimeDelta BrowsingTopicsState::CalculateSiteStickyTimeDelta( + const std::string& top_domain) const { + uint64_t epoch_switch_time_decision_hash = + HashTopDomainForEpochSwitchTimeDecision(hmac_key_, top_domain); + + return base::Seconds( + epoch_switch_time_decision_hash % + blink::features::kBrowsingTopicsTimePeriodPerEpoch.Get().InSeconds()); +} + base::ImportantFileWriter::BackgroundDataProducerCallback BrowsingTopicsState::GetSerializedDataProducerForBackgroundSequence() { DCHECK(loaded_); diff --git a/components/browsing_topics/browsing_topics_state.h b/components/browsing_topics/browsing_topics_state.h index 0e2ec5633af6a7..38ff641036d815 100644 --- a/components/browsing_topics/browsing_topics_state.h +++ b/components/browsing_topics/browsing_topics_state.h @@ -69,13 +69,20 @@ class BrowsingTopicsState // remove the entry from `epochs_`. void ClearOneEpoch(size_t epoch_index); - // Append `epoch_topics` to `epochs_`. + // Append `epoch_topics` to `epochs_`. This is invoked at the end of each + // epoch calculation. void AddEpoch(EpochTopics epoch_topics); - // Set `next_scheduled_calculation_time_` to - // `next_scheduled_calculation_time`. - void UpdateNextScheduledCalculationTime( - base::Time next_scheduled_calculation_time); + // Set `next_scheduled_calculation_time_` to one epoch later from + // base::Time::Now(). This is invoked at the end of each epoch calculation. + void UpdateNextScheduledCalculationTime(); + + // Calculate the candidate epochs to derive the topics from on `top_domain`. + // The caller (i.e. BrowsingTopicsServiceImpl, which also holds `this`) is + // responsible for ensuring that the `EpochTopic` objects that the pointers + // refer to remain alive when the caller is accessing them. + std::vector EpochsForSite( + const std::string& top_domain) const; const base::circular_deque& epochs() const { DCHECK(loaded_); @@ -95,6 +102,14 @@ class BrowsingTopicsState bool HasScheduledSaveForTesting() const; private: + FRIEND_TEST_ALL_PREFIXES(BrowsingTopicsStateTest, + EpochsForSite_OneEpoch_SwitchTimeNotArrived); + FRIEND_TEST_ALL_PREFIXES(BrowsingTopicsStateTest, + EpochsForSite_OneEpoch_SwitchTimeArrived); + + base::TimeDelta CalculateSiteStickyTimeDelta( + const std::string& top_domain) const; + // ImportantFileWriter::BackgroundDataSerializer implementation. base::ImportantFileWriter::BackgroundDataProducerCallback GetSerializedDataProducerForBackgroundSequence() override; @@ -127,10 +142,8 @@ class BrowsingTopicsState base::circular_deque epochs_; // The next time a calculation should occur. This will be updated when a - // calculation is scheduled: - // - at the start of a browser session. - // - or, after a topics calculation. - // - or, when the user explicitly resets the timer from the UX. + // calculation is scheduled at the end of a topics calculation and is always + // synchronously updated with `epochs_`. // // next_scheduled_calculation_time_.is_null() indicates this is a new profile // or there was an update to the configuration version when this diff --git a/components/browsing_topics/browsing_topics_state_unittest.cc b/components/browsing_topics/browsing_topics_state_unittest.cc index 7cd10734f802d4..bd91b334c8dc24 100644 --- a/components/browsing_topics/browsing_topics_state_unittest.cc +++ b/components/browsing_topics/browsing_topics_state_unittest.cc @@ -10,6 +10,7 @@ #include "base/files/scoped_temp_dir.h" #include "base/json/json_file_value_serializer.h" #include "base/json/values_util.h" +#include "base/strings/strcat.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "base/test/task_environment.h" @@ -21,20 +22,25 @@ namespace browsing_topics { namespace { -const base::Time kTime1 = base::Time::FromDeltaSinceWindowsEpoch(base::Days(1)); -const base::Time kTime2 = base::Time::FromDeltaSinceWindowsEpoch(base::Days(2)); -const base::Time kTime3 = base::Time::FromDeltaSinceWindowsEpoch(base::Days(3)); -const base::Time kTime4 = base::Time::FromDeltaSinceWindowsEpoch(base::Days(4)); -const base::Time kTime5 = base::Time::FromDeltaSinceWindowsEpoch(base::Days(5)); - -const browsing_topics::HmacKey kZeroKey = {}; -const browsing_topics::HmacKey kTestKey = {1}; -const browsing_topics::HmacKey kTestKey2 = {2}; - -const size_t kTaxonomySize = 349; -const int kTaxonomyVersion = 1; -const int kModelVersion = 2; -const size_t kPaddedTopTopicsStartIndex = 3; +constexpr base::Time kTime1 = + base::Time::FromDeltaSinceWindowsEpoch(base::Days(1)); +constexpr base::Time kTime2 = + base::Time::FromDeltaSinceWindowsEpoch(base::Days(2)); +constexpr base::Time kTime3 = + base::Time::FromDeltaSinceWindowsEpoch(base::Days(3)); +constexpr base::Time kTime4 = + base::Time::FromDeltaSinceWindowsEpoch(base::Days(4)); +constexpr base::Time kTime5 = + base::Time::FromDeltaSinceWindowsEpoch(base::Days(5)); + +constexpr browsing_topics::HmacKey kZeroKey = {}; +constexpr browsing_topics::HmacKey kTestKey = {1}; +constexpr browsing_topics::HmacKey kTestKey2 = {2}; + +constexpr size_t kTaxonomySize = 349; +constexpr int kTaxonomyVersion = 1; +constexpr int64_t kModelVersion = 2; +constexpr size_t kPaddedTopTopicsStartIndex = 3; EpochTopics CreateTestEpochTopics(base::Time calculation_time) { std::vector top_topics_and_observing_domains; @@ -101,8 +107,7 @@ class BrowsingTopicsStateTest : public testing::Test { dict.Set("hex_encoded_hmac_key", std::move(hex_encoded_hmac_key)); dict.Set("config_version", config_version); - JSONFileValueSerializer(TestFilePath()) - .Serialize(base::Value(std::move(dict))); + JSONFileValueSerializer(TestFilePath()).Serialize(dict); } void OnBrowsingTopicsStateLoaded() { observed_state_loaded_ = true; } @@ -172,10 +177,11 @@ TEST_F(BrowsingTopicsStateTest, task_environment_->FastForwardBy(base::Milliseconds(3000)); EXPECT_FALSE(state.HasScheduledSaveForTesting()); - state.UpdateNextScheduledCalculationTime(kTime1); + state.UpdateNextScheduledCalculationTime(); EXPECT_TRUE(state.epochs().empty()); - EXPECT_EQ(state.next_scheduled_calculation_time(), kTime1); + EXPECT_EQ(state.next_scheduled_calculation_time(), + base::Time::Now() + base::Days(7)); EXPECT_TRUE(std::equal(state.hmac_key().begin(), state.hmac_key().end(), kTestKey.begin())); @@ -187,11 +193,16 @@ TEST_F(BrowsingTopicsStateTest, task_environment_->FastForwardBy(base::Milliseconds(1)); EXPECT_FALSE(state.HasScheduledSaveForTesting()); - EXPECT_EQ( - GetTestFileContent(), - "{\"config_version\": 123,\"epochs\": [ ],\"hex_encoded_hmac_key\": " - "\"0100000000000000000000000000000000000000000000000000000000000000\"," - "\"next_scheduled_calculation_time\": \"86400000000\"}"); + std::string expected_content = base::StrCat( + {"{\"config_version\": 123,\"epochs\": [ ],\"hex_encoded_hmac_key\": " + "\"0100000000000000000000000000000000000000000000000000000000000000" + "\",\"next_scheduled_calculation_time\": \"", + base::NumberToString(state.next_scheduled_calculation_time() + .ToDeltaSinceWindowsEpoch() + .InMicroseconds()), + "\"}"}); + + EXPECT_EQ(GetTestFileContent(), expected_content); } TEST_F(BrowsingTopicsStateTest, AddEpoch) { @@ -202,47 +213,47 @@ TEST_F(BrowsingTopicsStateTest, AddEpoch) { state.AddEpoch(CreateTestEpochTopics(kTime1)); EXPECT_EQ(state.epochs().size(), 1u); - EXPECT_TRUE(state.epochs()[0].HasValidTopics()); + EXPECT_FALSE(state.epochs()[0].empty()); EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1); // Successful topics calculation at `kTime2`. state.AddEpoch(CreateTestEpochTopics(kTime2)); EXPECT_EQ(state.epochs().size(), 2u); - EXPECT_TRUE(state.epochs()[0].HasValidTopics()); + EXPECT_FALSE(state.epochs()[0].empty()); EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1); - EXPECT_TRUE(state.epochs()[1].HasValidTopics()); + EXPECT_FALSE(state.epochs()[1].empty()); EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2); // Failed topics calculation. state.AddEpoch(EpochTopics()); EXPECT_EQ(state.epochs().size(), 3u); - EXPECT_TRUE(state.epochs()[0].HasValidTopics()); + EXPECT_FALSE(state.epochs()[0].empty()); EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1); - EXPECT_TRUE(state.epochs()[1].HasValidTopics()); + EXPECT_FALSE(state.epochs()[1].empty()); EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2); - EXPECT_FALSE(state.epochs()[2].HasValidTopics()); + EXPECT_TRUE(state.epochs()[2].empty()); // Successful topics calculation at `kTime4`. state.AddEpoch(CreateTestEpochTopics(kTime4)); EXPECT_EQ(state.epochs().size(), 4u); - EXPECT_TRUE(state.epochs()[0].HasValidTopics()); + EXPECT_FALSE(state.epochs()[0].empty()); EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1); - EXPECT_TRUE(state.epochs()[1].HasValidTopics()); + EXPECT_FALSE(state.epochs()[1].empty()); EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2); - EXPECT_FALSE(state.epochs()[2].HasValidTopics()); - EXPECT_TRUE(state.epochs()[3].HasValidTopics()); + EXPECT_TRUE(state.epochs()[2].empty()); + EXPECT_FALSE(state.epochs()[3].empty()); EXPECT_EQ(state.epochs()[3].calculation_time(), kTime4); // Successful topics calculation at `kTime5`. When this epoch is added, the // first one should be evicted. state.AddEpoch(CreateTestEpochTopics(kTime5)); EXPECT_EQ(state.epochs().size(), 4u); - EXPECT_TRUE(state.epochs()[0].HasValidTopics()); + EXPECT_FALSE(state.epochs()[0].empty()); EXPECT_EQ(state.epochs()[0].calculation_time(), kTime2); - EXPECT_FALSE(state.epochs()[1].HasValidTopics()); - EXPECT_TRUE(state.epochs()[2].HasValidTopics()); + EXPECT_TRUE(state.epochs()[1].empty()); + EXPECT_FALSE(state.epochs()[2].empty()); EXPECT_EQ(state.epochs()[2].calculation_time(), kTime4); - EXPECT_TRUE(state.epochs()[3].HasValidTopics()); + EXPECT_FALSE(state.epochs()[3].empty()); EXPECT_EQ(state.epochs()[3].calculation_time(), kTime5); // The `next_scheduled_calculation_time` and `hmac_key` are unaffected. @@ -251,6 +262,123 @@ TEST_F(BrowsingTopicsStateTest, AddEpoch) { kTestKey.begin())); } +TEST_F(BrowsingTopicsStateTest, EpochsForSite_Empty) { + BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing()); + task_environment_->RunUntilIdle(); + + EXPECT_TRUE(state.EpochsForSite(/*top_domain=*/"foo.com").empty()); +} + +TEST_F(BrowsingTopicsStateTest, EpochsForSite_OneEpoch_SwitchTimeNotArrived) { + BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing()); + task_environment_->RunUntilIdle(); + + state.AddEpoch(CreateTestEpochTopics(kTime1)); + state.UpdateNextScheduledCalculationTime(); + + ASSERT_LT(state.CalculateSiteStickyTimeDelta("foo.com") + base::Hours(1), + base::Days(7)); + + task_environment_->FastForwardBy(base::Hours(1)); + EXPECT_TRUE(state.EpochsForSite(/*top_domain=*/"foo.com").empty()); +} + +TEST_F(BrowsingTopicsStateTest, EpochsForSite_OneEpoch_SwitchTimeArrived) { + BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing()); + task_environment_->RunUntilIdle(); + + state.AddEpoch(CreateTestEpochTopics(kTime1)); + state.UpdateNextScheduledCalculationTime(); + + ASSERT_GT(state.CalculateSiteStickyTimeDelta("foo.com") + base::Days(1), + base::Days(7)); + + task_environment_->FastForwardBy(base::Days(1)); + + std::vector epochs_for_site = + state.EpochsForSite(/*top_domain=*/"foo.com"); + EXPECT_EQ(epochs_for_site.size(), 1u); + EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]); +} + +TEST_F(BrowsingTopicsStateTest, + EpochsForSite_ThreeEpochs_SwitchTimeNotArrived) { + BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing()); + task_environment_->RunUntilIdle(); + + state.AddEpoch(CreateTestEpochTopics(kTime1)); + state.AddEpoch(CreateTestEpochTopics(kTime2)); + state.AddEpoch(CreateTestEpochTopics(kTime3)); + state.UpdateNextScheduledCalculationTime(); + + task_environment_->FastForwardBy(base::Hours(1)); + + std::vector epochs_for_site = + state.EpochsForSite(/*top_domain=*/"foo.com"); + EXPECT_EQ(epochs_for_site.size(), 2u); + EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]); + EXPECT_EQ(epochs_for_site[1], &state.epochs()[1]); +} + +TEST_F(BrowsingTopicsStateTest, EpochsForSite_ThreeEpochs_SwitchTimeArrived) { + BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing()); + task_environment_->RunUntilIdle(); + + state.AddEpoch(CreateTestEpochTopics(kTime1)); + state.AddEpoch(CreateTestEpochTopics(kTime2)); + state.AddEpoch(CreateTestEpochTopics(kTime3)); + state.UpdateNextScheduledCalculationTime(); + + task_environment_->FastForwardBy(base::Days(1)); + + std::vector epochs_for_site = + state.EpochsForSite(/*top_domain=*/"foo.com"); + EXPECT_EQ(epochs_for_site.size(), 3u); + EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]); + EXPECT_EQ(epochs_for_site[1], &state.epochs()[1]); + EXPECT_EQ(epochs_for_site[2], &state.epochs()[2]); +} + +TEST_F(BrowsingTopicsStateTest, EpochsForSite_FourEpochs_SwitchTimeNotArrived) { + BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing()); + task_environment_->RunUntilIdle(); + + state.AddEpoch(CreateTestEpochTopics(kTime1)); + state.AddEpoch(CreateTestEpochTopics(kTime2)); + state.AddEpoch(CreateTestEpochTopics(kTime3)); + state.AddEpoch(CreateTestEpochTopics(kTime4)); + state.UpdateNextScheduledCalculationTime(); + + task_environment_->FastForwardBy(base::Hours(1)); + + std::vector epochs_for_site = + state.EpochsForSite(/*top_domain=*/"foo.com"); + EXPECT_EQ(epochs_for_site.size(), 3u); + EXPECT_EQ(epochs_for_site[0], &state.epochs()[0]); + EXPECT_EQ(epochs_for_site[1], &state.epochs()[1]); + EXPECT_EQ(epochs_for_site[2], &state.epochs()[2]); +} + +TEST_F(BrowsingTopicsStateTest, EpochsForSite_FourEpochs_SwitchTimeArrived) { + BrowsingTopicsState state(temp_dir_.GetPath(), base::DoNothing()); + task_environment_->RunUntilIdle(); + + state.AddEpoch(CreateTestEpochTopics(kTime1)); + state.AddEpoch(CreateTestEpochTopics(kTime2)); + state.AddEpoch(CreateTestEpochTopics(kTime3)); + state.AddEpoch(CreateTestEpochTopics(kTime4)); + state.UpdateNextScheduledCalculationTime(); + + task_environment_->FastForwardBy(base::Days(1)); + + std::vector epochs_for_site = + state.EpochsForSite(/*top_domain=*/"foo.com"); + EXPECT_EQ(epochs_for_site.size(), 3u); + EXPECT_EQ(epochs_for_site[0], &state.epochs()[1]); + EXPECT_EQ(epochs_for_site[1], &state.epochs()[2]); + EXPECT_EQ(epochs_for_site[2], &state.epochs()[3]); +} + TEST_F(BrowsingTopicsStateTest, InitFromPreexistingFile_CorruptedHmacKey) { base::HistogramTester histograms; @@ -290,7 +418,7 @@ TEST_F(BrowsingTopicsStateTest, InitFromPreexistingFile_SameConfigVersion) { task_environment_->RunUntilIdle(); EXPECT_EQ(state.epochs().size(), 1u); - EXPECT_TRUE(state.epochs()[0].HasValidTopics()); + EXPECT_FALSE(state.epochs()[0].empty()); EXPECT_EQ(state.epochs()[0].model_version(), kModelVersion); EXPECT_EQ(state.next_scheduled_calculation_time(), kTime2); EXPECT_TRUE(std::equal(state.hmac_key().begin(), state.hmac_key().end(), @@ -333,25 +461,26 @@ TEST_F(BrowsingTopicsStateTest, ClearOneEpoch) { state.AddEpoch(CreateTestEpochTopics(kTime1)); EXPECT_EQ(state.epochs().size(), 1u); - EXPECT_TRUE(state.epochs()[0].HasValidTopics()); + EXPECT_FALSE(state.epochs()[0].empty()); EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1); state.AddEpoch(CreateTestEpochTopics(kTime2)); EXPECT_EQ(state.epochs().size(), 2u); - EXPECT_TRUE(state.epochs()[0].HasValidTopics()); + EXPECT_FALSE(state.epochs()[0].empty()); EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1); - EXPECT_TRUE(state.epochs()[1].HasValidTopics()); + EXPECT_FALSE(state.epochs()[1].empty()); EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2); - state.UpdateNextScheduledCalculationTime(kTime3); - state.ClearOneEpoch(/*epoch_index=*/0); EXPECT_EQ(state.epochs().size(), 2u); - EXPECT_FALSE(state.epochs()[0].HasValidTopics()); - EXPECT_TRUE(state.epochs()[1].HasValidTopics()); + EXPECT_TRUE(state.epochs()[0].empty()); + EXPECT_FALSE(state.epochs()[1].empty()); EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2); - EXPECT_EQ(state.next_scheduled_calculation_time(), kTime3); + state.UpdateNextScheduledCalculationTime(); + + EXPECT_EQ(state.next_scheduled_calculation_time(), + base::Time::Now() + base::Days(7)); EXPECT_TRUE(std::equal(state.hmac_key().begin(), state.hmac_key().end(), kTestKey.begin())); } @@ -363,22 +492,23 @@ TEST_F(BrowsingTopicsStateTest, ClearAllTopics) { state.AddEpoch(CreateTestEpochTopics(kTime1)); EXPECT_EQ(state.epochs().size(), 1u); - EXPECT_TRUE(state.epochs()[0].HasValidTopics()); + EXPECT_FALSE(state.epochs()[0].empty()); EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1); state.AddEpoch(CreateTestEpochTopics(kTime2)); EXPECT_EQ(state.epochs().size(), 2u); - EXPECT_TRUE(state.epochs()[0].HasValidTopics()); + EXPECT_FALSE(state.epochs()[0].empty()); EXPECT_EQ(state.epochs()[0].calculation_time(), kTime1); - EXPECT_TRUE(state.epochs()[1].HasValidTopics()); + EXPECT_FALSE(state.epochs()[1].empty()); EXPECT_EQ(state.epochs()[1].calculation_time(), kTime2); - state.UpdateNextScheduledCalculationTime(kTime3); + state.UpdateNextScheduledCalculationTime(); state.ClearAllTopics(); EXPECT_EQ(state.epochs().size(), 0u); - EXPECT_EQ(state.next_scheduled_calculation_time(), kTime3); + EXPECT_EQ(state.next_scheduled_calculation_time(), + base::Time::Now() + base::Days(7)); EXPECT_TRUE(std::equal(state.hmac_key().begin(), state.hmac_key().end(), kTestKey.begin())); } diff --git a/components/browsing_topics/epoch_topics.cc b/components/browsing_topics/epoch_topics.cc index 9d04943cd56c28..e70d102ef2c04d 100644 --- a/components/browsing_topics/epoch_topics.cc +++ b/components/browsing_topics/epoch_topics.cc @@ -38,7 +38,7 @@ EpochTopics::EpochTopics( size_t padded_top_topics_start_index, size_t taxonomy_size, int taxonomy_version, - int model_version, + int64_t model_version, base::Time calculation_time) : top_topics_and_observing_domains_( std::move(top_topics_and_observing_domains)), @@ -105,12 +105,17 @@ EpochTopics EpochTopics::FromDictValue(const base::Value::Dict& dict_value) { int taxonomy_version = *taxonomy_version_value; - absl::optional model_version_value = - dict_value.FindInt(kModelVersionNameKey); + const base::Value* model_version_value = + dict_value.Find(kModelVersionNameKey); if (!model_version_value) return EpochTopics(); - int model_version = *model_version_value; + absl::optional model_version_int64_value = + base::ValueToInt64(model_version_value); + if (!model_version_int64_value) + return EpochTopics(); + + int64_t model_version = *model_version_int64_value; const base::Value* calculation_time_value = dict_value.Find(kCalculationTimeNameKey); @@ -141,7 +146,7 @@ base::Value::Dict EpochTopics::ToDictValue() const { result_dict.Set(kTaxonomySizeNameKey, base::checked_cast(taxonomy_size_)); result_dict.Set(kTaxonomyVersionNameKey, taxonomy_version_); - result_dict.Set(kModelVersionNameKey, model_version_); + result_dict.Set(kModelVersionNameKey, base::Int64ToValue(model_version_)); result_dict.Set(kCalculationTimeNameKey, base::TimeToValue(calculation_time_)); return result_dict; @@ -151,8 +156,29 @@ absl::optional EpochTopics::TopicForSite( const std::string& top_domain, const HashedDomain& hashed_context_domain, ReadOnlyHmacKey hmac_key) const { + return TopicForSiteHelper(top_domain, /*need_filtering=*/true, + hashed_context_domain, hmac_key); +} + +absl::optional EpochTopics::TopicForSiteNoFiltering( + const std::string& top_domain, + ReadOnlyHmacKey hmac_key) const { + return TopicForSiteHelper(top_domain, /*need_filtering=*/false, + /*hashed_context_domain=*/{}, hmac_key); +} + +void EpochTopics::ClearTopics() { + top_topics_and_observing_domains_.clear(); + padded_top_topics_start_index_ = 0; +} + +absl::optional EpochTopics::TopicForSiteHelper( + const std::string& top_domain, + bool need_filtering, + const HashedDomain& hashed_context_domain, + ReadOnlyHmacKey hmac_key) const { // The topics calculation failed, or the topics has been cleared. - if (!HasValidTopics()) + if (empty()) return absl::nullopt; uint64_t random_or_top_topic_decision_hash = @@ -179,18 +205,16 @@ absl::optional EpochTopics::TopicForSite( const TopicAndDomains& topic_and_observing_domains = top_topics_and_observing_domains_[top_topic_index]; + if (!topic_and_observing_domains.IsValid()) + return absl::nullopt; + // Only add the topic if the context has observed it before. - if (!topic_and_observing_domains.hashed_domains().count( - hashed_context_domain)) { + if (need_filtering && !topic_and_observing_domains.hashed_domains().count( + hashed_context_domain)) { return absl::nullopt; } return topic_and_observing_domains.topic(); } -void EpochTopics::ClearTopics() { - top_topics_and_observing_domains_.clear(); - padded_top_topics_start_index_ = 0; -} - } // namespace browsing_topics diff --git a/components/browsing_topics/epoch_topics.h b/components/browsing_topics/epoch_topics.h index d38a3c2bf16cbc..88bea0a2359466 100644 --- a/components/browsing_topics/epoch_topics.h +++ b/components/browsing_topics/epoch_topics.h @@ -24,7 +24,7 @@ class EpochTopics { size_t padded_top_topics_start_index, size_t taxonomy_size, int taxonomy_version, - int model_version, + int64_t model_version, base::Time calculation_time); EpochTopics(const EpochTopics&) = delete; @@ -49,9 +49,14 @@ class EpochTopics { const HashedDomain& hashed_context_domain, ReadOnlyHmacKey hmac_key) const; - bool HasValidTopics() const { - return !top_topics_and_observing_domains_.empty(); - } + // Similar to `TopicForSite`, but without applying the filtering based on a + // calling context. This method is used for displaying the candidate topics + // for a site for the UX. + absl::optional TopicForSiteNoFiltering(const std::string& top_domain, + ReadOnlyHmacKey hmac_key) const; + + // Whether `top_topics_and_observing_domains_` is empty. + bool empty() const { return top_topics_and_observing_domains_.empty(); } // Clear `top_topics_and_observing_domains_` and // reset `padded_top_topics_start_index_` to 0. @@ -69,11 +74,17 @@ class EpochTopics { int taxonomy_version() const { return taxonomy_version_; } - int model_version() const { return model_version_; } + int64_t model_version() const { return model_version_; } base::Time calculation_time() const { return calculation_time_; } private: + absl::optional TopicForSiteHelper( + const std::string& top_domain, + bool need_filtering, + const HashedDomain& hashed_context_domain, + ReadOnlyHmacKey hmac_key) const; + // The top topics for this epoch, and the context domains that observed each // topic across // `kBrowsingTopicsNumberOfEpochsOfObservationDataToUseForFiltering` epochs. @@ -98,7 +109,7 @@ class EpochTopics { int taxonomy_version_ = 0; // The version of the model used to calculate this epoch's topics. - int model_version_ = 0; + int64_t model_version_ = 0; // The calculation start time. This also determines the time range of the // underlying topics data. diff --git a/components/browsing_topics/epoch_topics_unittest.cc b/components/browsing_topics/epoch_topics_unittest.cc index 52ccb1da5b3fce..69e2a49b40f248 100644 --- a/components/browsing_topics/epoch_topics_unittest.cc +++ b/components/browsing_topics/epoch_topics_unittest.cc @@ -12,13 +12,13 @@ namespace browsing_topics { namespace { -const base::Time kCalculationTime = +constexpr base::Time kCalculationTime = base::Time::FromDeltaSinceWindowsEpoch(base::Days(1)); -const browsing_topics::HmacKey kTestKey = {1}; -const size_t kTaxonomySize = 349; -const int kTaxonomyVersion = 1; -const int kModelVersion = 2; -const size_t kPaddedTopTopicsStartIndex = 3; +constexpr browsing_topics::HmacKey kTestKey = {1}; +constexpr size_t kTaxonomySize = 349; +constexpr int kTaxonomyVersion = 1; +constexpr int64_t kModelVersion = 2; +constexpr size_t kPaddedTopTopicsStartIndex = 3; EpochTopics CreateTestEpochTopics() { std::vector top_topics_and_observing_domains; @@ -44,10 +44,27 @@ EpochTopics CreateTestEpochTopics() { class EpochTopicsTest : public testing::Test {}; +TEST_F(EpochTopicsTest, TopicForSite_InvalidIndividualTopics) { + std::vector top_topics_and_observing_domains; + for (int i = 0; i < 5; ++i) { + top_topics_and_observing_domains.emplace_back(TopicAndDomains()); + } + + EpochTopics epoch_topics(std::move(top_topics_and_observing_domains), + kPaddedTopTopicsStartIndex, kTaxonomySize, + kTaxonomyVersion, kModelVersion, kCalculationTime); + EXPECT_FALSE(epoch_topics.empty()); + + std::string top_site = "foo.com"; + + EXPECT_EQ(epoch_topics.TopicForSiteNoFiltering(top_site, kTestKey), + absl::nullopt); +} + TEST_F(EpochTopicsTest, TopicForSite) { EpochTopics epoch_topics = CreateTestEpochTopics(); - EXPECT_TRUE(epoch_topics.HasValidTopics()); + EXPECT_FALSE(epoch_topics.empty()); EXPECT_EQ(epoch_topics.taxonomy_version(), kTaxonomyVersion); EXPECT_EQ(epoch_topics.model_version(), kModelVersion); EXPECT_EQ(epoch_topics.calculation_time(), kCalculationTime); @@ -76,6 +93,9 @@ TEST_F(EpochTopicsTest, TopicForSite) { Topic(2)); EXPECT_EQ(epoch_topics.TopicForSite(top_site, HashedDomain(3), kTestKey), absl::nullopt); + + EXPECT_EQ(epoch_topics.TopicForSiteNoFiltering(top_site, kTestKey), + Topic(2)); } { @@ -102,6 +122,9 @@ TEST_F(EpochTopicsTest, TopicForSite) { absl::nullopt); EXPECT_EQ(epoch_topics.TopicForSite(top_site, HashedDomain(3), kTestKey), Topic(3)); + + EXPECT_EQ(epoch_topics.TopicForSiteNoFiltering(top_site, kTestKey), + Topic(3)); } { @@ -127,17 +150,20 @@ TEST_F(EpochTopicsTest, TopicForSite) { Topic(186)); EXPECT_EQ(epoch_topics.TopicForSite(top_site, HashedDomain(3), kTestKey), Topic(186)); + + EXPECT_EQ(epoch_topics.TopicForSiteNoFiltering(top_site, kTestKey), + Topic(186)); } } TEST_F(EpochTopicsTest, ClearTopics) { EpochTopics epoch_topics = CreateTestEpochTopics(); - EXPECT_TRUE(epoch_topics.HasValidTopics()); + EXPECT_FALSE(epoch_topics.empty()); epoch_topics.ClearTopics(); - EXPECT_FALSE(epoch_topics.HasValidTopics()); + EXPECT_TRUE(epoch_topics.empty()); EXPECT_EQ(epoch_topics.TopicForSite(/*top_domain=*/"foo.com", HashedDomain(1), kTestKey), @@ -148,7 +174,7 @@ TEST_F(EpochTopicsTest, FromEmptyDictionaryValue) { EpochTopics read_epoch_topics = EpochTopics::FromDictValue(base::Value::Dict()); - EXPECT_FALSE(read_epoch_topics.HasValidTopics()); + EXPECT_TRUE(read_epoch_topics.empty()); EXPECT_EQ(read_epoch_topics.taxonomy_version(), 0); EXPECT_EQ(read_epoch_topics.model_version(), 0); EXPECT_EQ(read_epoch_topics.calculation_time(), base::Time()); @@ -166,7 +192,7 @@ TEST_F(EpochTopicsTest, EmptyEpochTopics_ToAndFromDictValue) { base::Value::Dict dict_value = epoch_topics.ToDictValue(); EpochTopics read_epoch_topics = EpochTopics::FromDictValue(dict_value); - EXPECT_FALSE(read_epoch_topics.HasValidTopics()); + EXPECT_TRUE(read_epoch_topics.empty()); EXPECT_EQ(read_epoch_topics.taxonomy_version(), 0); EXPECT_EQ(read_epoch_topics.model_version(), 0); EXPECT_EQ(read_epoch_topics.calculation_time(), base::Time()); @@ -183,7 +209,7 @@ TEST_F(EpochTopicsTest, PopulatedEpochTopics_ToAndFromValue) { base::Value::Dict dict_value = epoch_topics.ToDictValue(); EpochTopics read_epoch_topics = EpochTopics::FromDictValue(dict_value); - EXPECT_TRUE(read_epoch_topics.HasValidTopics()); + EXPECT_FALSE(read_epoch_topics.empty()); EXPECT_EQ(read_epoch_topics.taxonomy_version(), 1); EXPECT_EQ(read_epoch_topics.model_version(), 2); EXPECT_EQ(read_epoch_topics.calculation_time(), kCalculationTime); diff --git a/components/browsing_topics/test_util.cc b/components/browsing_topics/test_util.cc index e1630294ad3060..2fff59526b4447 100644 --- a/components/browsing_topics/test_util.cc +++ b/components/browsing_topics/test_util.cc @@ -4,8 +4,43 @@ #include "components/browsing_topics/test_util.h" +#include "base/run_loop.h" +#include "base/test/bind.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "components/history/core/browser/history_service.h" + namespace browsing_topics { +bool BrowsingTopicsEligibleForURLVisit(history::HistoryService* history_service, + const GURL& url) { + bool topics_eligible; + + history::QueryOptions options; + options.duplicate_policy = history::QueryOptions::KEEP_ALL_DUPLICATES; + + base::RunLoop run_loop; + base::CancelableTaskTracker tracker; + + history_service->QueryHistory( + std::u16string(), options, + base::BindLambdaForTesting([&](history::QueryResults results) { + size_t num_matches = 0; + const size_t* match_index = results.MatchesForURL(url, &num_matches); + + DCHECK_EQ(1u, num_matches); + + topics_eligible = + results[*match_index].content_annotations().annotation_flags & + history::VisitContentAnnotationFlag::kBrowsingTopicsEligible; + run_loop.Quit(); + }), + &tracker); + + run_loop.Run(); + + return topics_eligible; +} + TesterBrowsingTopicsCalculator::TesterBrowsingTopicsCalculator( privacy_sandbox::PrivacySandboxSettings* privacy_sandbox_settings, history::HistoryService* history_service, @@ -20,6 +55,20 @@ TesterBrowsingTopicsCalculator::TesterBrowsingTopicsCalculator( std::move(callback)), rand_uint64_queue_(std::move(rand_uint64_queue)) {} +TesterBrowsingTopicsCalculator::TesterBrowsingTopicsCalculator( + CalculateCompletedCallback callback, + EpochTopics mock_result, + base::TimeDelta mock_result_delay) + : BrowsingTopicsCalculator(nullptr, + nullptr, + nullptr, + nullptr, + base::DoNothing()), + use_mock_result_(true), + mock_result_(std::move(mock_result)), + mock_result_delay_(mock_result_delay), + finish_callback_(std::move(callback)) {} + TesterBrowsingTopicsCalculator::~TesterBrowsingTopicsCalculator() = default; uint64_t TesterBrowsingTopicsCalculator::GenerateRandUint64() { @@ -31,4 +80,23 @@ uint64_t TesterBrowsingTopicsCalculator::GenerateRandUint64() { return next_rand_uint64; } +void TesterBrowsingTopicsCalculator::CheckCanCalculate() { + if (use_mock_result_) { + base::SequencedTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&TesterBrowsingTopicsCalculator::MockDelayReached, + weak_ptr_factory_.GetWeakPtr()), + mock_result_delay_); + return; + } + + BrowsingTopicsCalculator::CheckCanCalculate(); +} + +void TesterBrowsingTopicsCalculator::MockDelayReached() { + DCHECK(use_mock_result_); + + std::move(finish_callback_).Run(std::move(mock_result_)); +} + } // namespace browsing_topics diff --git a/components/browsing_topics/test_util.h b/components/browsing_topics/test_util.h index cc9c2351bd0dde..f0e6b137ffd021 100644 --- a/components/browsing_topics/test_util.h +++ b/components/browsing_topics/test_util.h @@ -6,12 +6,20 @@ #define COMPONENTS_BROWSING_TOPICS_TEST_UTIL_H_ #include "base/containers/queue.h" + +#include "base/memory/weak_ptr.h" #include "components/browsing_topics/browsing_topics_calculator.h" #include "third_party/abseil-cpp/absl/types/optional.h" namespace browsing_topics { -// A tester class that allows mocking the generated random numbers. +// Returns whether the URL entry is eligible in topics calculation. +// Precondition: the history visits contain exactly one matching URL. +bool BrowsingTopicsEligibleForURLVisit(history::HistoryService* history_service, + const GURL& url); + +// A tester class that allows mocking the generated random numbers, or directly +// returning a mock result with a delay. class TesterBrowsingTopicsCalculator : public BrowsingTopicsCalculator { public: // Initialize a regular `BrowsingTopicsCalculator` with an additional @@ -24,6 +32,11 @@ class TesterBrowsingTopicsCalculator : public BrowsingTopicsCalculator { CalculateCompletedCallback callback, base::queue rand_uint64_queue); + // Initialize a mock `BrowsingTopicsCalculator` (with mock result and delay). + TesterBrowsingTopicsCalculator(CalculateCompletedCallback callback, + EpochTopics mock_result, + base::TimeDelta mock_result_delay); + ~TesterBrowsingTopicsCalculator() override; TesterBrowsingTopicsCalculator(const TesterBrowsingTopicsCalculator&) = @@ -38,8 +51,22 @@ class TesterBrowsingTopicsCalculator : public BrowsingTopicsCalculator { // `rand_uint64_queue_` is not empty. uint64_t GenerateRandUint64() override; + // If `use_mock_result_` is true, post a task with `mock_result_delay_` to + // directly invoke the `finish_callback_` with `mock_result_`; otherwise, use + // the default handling for `CheckCanCalculate`. + void CheckCanCalculate() override; + private: + void MockDelayReached(); + base::queue rand_uint64_queue_; + + bool use_mock_result_ = false; + EpochTopics mock_result_; + base::TimeDelta mock_result_delay_; + CalculateCompletedCallback finish_callback_; + + base::WeakPtrFactory weak_ptr_factory_{this}; }; } // namespace browsing_topics diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn index 7835771a140cc7..88890346ff1275 100644 --- a/content/browser/BUILD.gn +++ b/content/browser/BUILD.gn @@ -619,6 +619,8 @@ source_set("browser") { "browsing_data/storage_partition_code_cache_data_remover.h", "browsing_instance.cc", "browsing_instance.h", + "browsing_topics/browsing_topics_document_host.cc", + "browsing_topics/browsing_topics_document_host.h", "browsing_topics/browsing_topics_site_data_manager_impl.cc", "browsing_topics/browsing_topics_site_data_manager_impl.h", "browsing_topics/browsing_topics_site_data_storage.cc", diff --git a/content/browser/browser_interface_binders.cc b/content/browser/browser_interface_binders.cc index 30dc4144fae0ba..fe393e61bf8df7 100644 --- a/content/browser/browser_interface_binders.cc +++ b/content/browser/browser_interface_binders.cc @@ -20,6 +20,7 @@ #include "content/browser/bad_message.h" #include "content/browser/browser_context_impl.h" #include "content/browser/browser_main_loop.h" +#include "content/browser/browsing_topics/browsing_topics_document_host.h" #include "content/browser/contacts/contacts_manager_impl.h" #include "content/browser/content_index/content_index_service_impl.h" #include "content/browser/cookie_store/cookie_store_manager.h" @@ -1091,6 +1092,10 @@ void PopulateBinderMapWithContext( base::BindRepeating( &EmptyBinderForFrame< media::mojom::SpeechRecognitionClientBrowserInterface>)); + if (base::FeatureList::IsEnabled(blink::features::kBrowsingTopics)) { + map->Add( + base::BindRepeating(&BrowsingTopicsDocumentHost::CreateMojoService)); + } map->Add(base::BindRepeating( &EmptyBinderForFrame)); #endif diff --git a/content/browser/browsing_topics/browsing_topics_document_host.cc b/content/browser/browsing_topics/browsing_topics_document_host.cc new file mode 100644 index 00000000000000..cb42dbf76bd5e9 --- /dev/null +++ b/content/browser/browsing_topics/browsing_topics_document_host.cc @@ -0,0 +1,61 @@ +// Copyright 2022 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/browsing_topics/browsing_topics_document_host.h" + +#include "base/bind.h" +#include "content/browser/renderer_host/render_frame_host_impl.h" +#include "content/public/browser/content_browser_client.h" +#include "content/public/browser/document_service.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/content_client.h" +#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" + +namespace content { + +BrowsingTopicsDocumentHost::BrowsingTopicsDocumentHost( + RenderFrameHost* render_frame_host, + mojo::PendingReceiver receiver) + : DocumentService(render_frame_host, std::move(receiver)) {} + +// static +void BrowsingTopicsDocumentHost::CreateMojoService( + RenderFrameHost* render_frame_host, + mojo::PendingReceiver + receiver) { + DCHECK(render_frame_host); + + if (render_frame_host->GetLastCommittedOrigin().opaque()) { + mojo::ReportBadMessage( + "Unexpected BrowsingTopicsDocumentHost::CreateMojoService in an opaque " + "origin document"); + return; + } + + if (!render_frame_host->GetMainFrame()->IsInPrimaryMainFrame()) { + mojo::ReportBadMessage( + "Unexpected BrowsingTopicsDocumentHost::CreateMojoService in a " + "non-primary main frame context."); + return; + } + + // The object is bound to the lifetime of |render_frame_host| and the mojo + // connection. See DocumentService for details. + new BrowsingTopicsDocumentHost(render_frame_host, std::move(receiver)); +} + +void BrowsingTopicsDocumentHost::GetBrowsingTopics( + GetBrowsingTopicsCallback callback) { + std::vector browsing_topics = + GetContentClient()->browser()->GetBrowsingTopicsForJsApi( + render_frame_host()->GetLastCommittedOrigin(), + render_frame_host()->GetMainFrame()); + + std::move(callback).Run(std::move(browsing_topics)); +} + +BrowsingTopicsDocumentHost::~BrowsingTopicsDocumentHost() = default; + +} // namespace content diff --git a/content/browser/browsing_topics/browsing_topics_document_host.h b/content/browser/browsing_topics/browsing_topics_document_host.h new file mode 100644 index 00000000000000..a41e252a9e78fe --- /dev/null +++ b/content/browser/browsing_topics/browsing_topics_document_host.h @@ -0,0 +1,48 @@ +// Copyright 2022 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_BROWSING_TOPICS_BROWSING_TOPICS_DOCUMENT_HOST_H_ +#define CONTENT_BROWSER_BROWSING_TOPICS_BROWSING_TOPICS_DOCUMENT_HOST_H_ + +#include "content/common/content_export.h" +#include "content/public/browser/document_service.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" + +namespace content { + +class RenderFrameHost; + +// The object can only be bound to a document if the `kBrowsingTopics` feature +// is enabled, and if the document does not have an opaque origin. +class CONTENT_EXPORT BrowsingTopicsDocumentHost final + : public DocumentService { + public: + BrowsingTopicsDocumentHost( + RenderFrameHost* render_frame_host, + mojo::PendingReceiver + receiver); + + BrowsingTopicsDocumentHost(const BrowsingTopicsDocumentHost&) = delete; + BrowsingTopicsDocumentHost& operator=(const BrowsingTopicsDocumentHost&) = + delete; + BrowsingTopicsDocumentHost(BrowsingTopicsDocumentHost&&) = delete; + BrowsingTopicsDocumentHost& operator=(BrowsingTopicsDocumentHost&&) = delete; + + static void CreateMojoService( + RenderFrameHost* render_frame_host, + mojo::PendingReceiver + receiver); + + // blink::mojom::BrowsingTopicsDocumentService. + void GetBrowsingTopics(GetBrowsingTopicsCallback callback) override; + + private: + // |this| can only be destroyed by DocumentService. + ~BrowsingTopicsDocumentHost() override; +}; + +} // namespace content + +#endif // CONTENT_BROWSER_BROWSING_TOPICS_BROWSING_TOPICS_DOCUMENT_HOST_H_ diff --git a/content/browser/browsing_topics/browsing_topics_site_data_manager_impl.cc b/content/browser/browsing_topics/browsing_topics_site_data_manager_impl.cc index 157ca860c95f10..70a78fa1eb9ad9 100644 --- a/content/browser/browsing_topics/browsing_topics_site_data_manager_impl.cc +++ b/content/browser/browsing_topics/browsing_topics_site_data_manager_impl.cc @@ -54,10 +54,10 @@ void BrowsingTopicsSiteDataManagerImpl::GetBrowsingTopicsApiUsage( void BrowsingTopicsSiteDataManagerImpl::OnBrowsingTopicsApiUsed( const browsing_topics::HashedHost& hashed_main_frame_host, - const base::flat_set& - hashed_context_domains) { + const base::flat_set& hashed_context_domains, + base::Time time) { storage_.AsyncCall(&BrowsingTopicsSiteDataStorage::OnBrowsingTopicsApiUsed) - .WithArgs(hashed_main_frame_host, hashed_context_domains); + .WithArgs(hashed_main_frame_host, hashed_context_domains, time); } } // namespace content diff --git a/content/browser/browsing_topics/browsing_topics_site_data_manager_impl.h b/content/browser/browsing_topics/browsing_topics_site_data_manager_impl.h index 10dc0a5401d34c..3118a8ed825978 100644 --- a/content/browser/browsing_topics/browsing_topics_site_data_manager_impl.h +++ b/content/browser/browsing_topics/browsing_topics_site_data_manager_impl.h @@ -41,7 +41,8 @@ class CONTENT_EXPORT BrowsingTopicsSiteDataManagerImpl void OnBrowsingTopicsApiUsed( const browsing_topics::HashedHost& hashed_main_frame_host, const base::flat_set& - hashed_context_domains) override; + hashed_context_domains, + base::Time time) override; private: base::SequenceBound storage_; diff --git a/content/browser/browsing_topics/browsing_topics_site_data_manager_impl_unittest.cc b/content/browser/browsing_topics/browsing_topics_site_data_manager_impl_unittest.cc index 43990e012c4576..911dadc78734fc 100644 --- a/content/browser/browsing_topics/browsing_topics_site_data_manager_impl_unittest.cc +++ b/content/browser/browsing_topics/browsing_topics_site_data_manager_impl_unittest.cc @@ -37,7 +37,8 @@ TEST_F(BrowsingTopicsSiteDataManagerImplTest, GetBrowsingTopicsApiUsage) { topics_manager_->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}, + initial_time); size_t query_result_count = 0; diff --git a/content/browser/browsing_topics/browsing_topics_site_data_storage.cc b/content/browser/browsing_topics/browsing_topics_site_data_storage.cc index 2454ae71941f65..af8a94c385e6ad 100644 --- a/content/browser/browsing_topics/browsing_topics_site_data_storage.cc +++ b/content/browser/browsing_topics/browsing_topics_site_data_storage.cc @@ -105,8 +105,8 @@ BrowsingTopicsSiteDataStorage::GetBrowsingTopicsApiUsage(base::Time begin_time, void BrowsingTopicsSiteDataStorage::OnBrowsingTopicsApiUsed( const browsing_topics::HashedHost& hashed_main_frame_host, - const base::flat_set& - hashed_context_domains) { + const base::flat_set& hashed_context_domains, + base::Time time) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); if (!LazyInit()) @@ -116,8 +116,6 @@ void BrowsingTopicsSiteDataStorage::OnBrowsingTopicsApiUsed( if (!transaction.Begin()) return; - base::Time current_time = base::Time::Now(); - for (const browsing_topics::HashedDomain& hashed_context_domain : hashed_context_domains) { static constexpr char kInsertApiUsageSql[] = @@ -131,7 +129,7 @@ void BrowsingTopicsSiteDataStorage::OnBrowsingTopicsApiUsed( db_->GetCachedStatement(SQL_FROM_HERE, kInsertApiUsageSql)); insert_api_usage_statement.BindInt64(0, hashed_context_domain.value()); insert_api_usage_statement.BindInt64(1, hashed_main_frame_host.value()); - insert_api_usage_statement.BindTime(2, current_time); + insert_api_usage_statement.BindTime(2, time); if (!insert_api_usage_statement.Run()) return; diff --git a/content/browser/browsing_topics/browsing_topics_site_data_storage.h b/content/browser/browsing_topics/browsing_topics_site_data_storage.h index 590d3b583e6aff..36c8a37913f176 100644 --- a/content/browser/browsing_topics/browsing_topics_site_data_storage.h +++ b/content/browser/browsing_topics/browsing_topics_site_data_storage.h @@ -58,7 +58,8 @@ class CONTENT_EXPORT BrowsingTopicsSiteDataStorage { void OnBrowsingTopicsApiUsed( const browsing_topics::HashedHost& hashed_main_frame_host, const base::flat_set& - hashed_context_domains); + hashed_context_domains, + base::Time time); private: enum class InitStatus { diff --git a/content/browser/browsing_topics/browsing_topics_site_data_storage_unittest.cc b/content/browser/browsing_topics/browsing_topics_site_data_storage_unittest.cc index 1e3bec1ea902de..33f9444496ba09 100644 --- a/content/browser/browsing_topics/browsing_topics_site_data_storage_unittest.cc +++ b/content/browser/browsing_topics/browsing_topics_site_data_storage_unittest.cc @@ -207,7 +207,8 @@ TEST_F(BrowsingTopicsSiteDataStorageTest, OnBrowsingTopicsApiUsed_SingleEntry) { OpenDatabase(); topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}, + base::Time::Now()); CloseDatabase(); sql::Database db; @@ -237,17 +238,20 @@ TEST_F(BrowsingTopicsSiteDataStorageTest, OpenDatabase(); topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(123)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(123)}, + base::Time::Now()); task_environment_.FastForwardBy(base::Seconds(1)); topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(456), - browsing_topics::HashedDomain(789)}); + /*hashed_context_domains=*/ + {browsing_topics::HashedDomain(456), browsing_topics::HashedDomain(789)}, + base::Time::Now()); topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(456), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(789)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(789)}, + base::Time::Now()); CloseDatabase(); sql::Database db; @@ -318,13 +322,15 @@ TEST_F(BrowsingTopicsSiteDataStorageTest, GetBrowsingTopicsApiUsage) { topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(123)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(123)}, + base::Time::Now()); task_environment_.FastForwardBy(base::Seconds(1)); topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}, + base::Time::Now()); task_environment_.FastForwardBy(base::Seconds(1)); @@ -358,13 +364,15 @@ TEST_F(BrowsingTopicsSiteDataStorageTest, topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(123)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(123)}, + base::Time::Now()); task_environment_.FastForwardBy(base::Seconds(1)); topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}, + base::Time::Now()); task_environment_.FastForwardBy(base::Seconds(1)); @@ -395,13 +403,15 @@ TEST_F(BrowsingTopicsSiteDataStorageTest, ExpireDataBefore) { topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(123)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(123)}, + base::Time::Now()); task_environment_.FastForwardBy(base::Seconds(1)); topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}, + base::Time::Now()); task_environment_.FastForwardBy(base::Seconds(1)); @@ -449,13 +459,15 @@ TEST_F(BrowsingTopicsSiteDataStorageMaxEntriesToLoadTest, MaxEntriesToLoad) { topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(123)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(123)}, + base::Time::Now()); task_environment_.FastForwardBy(base::Seconds(1)); topics_storage()->OnBrowsingTopicsApiUsed( /*hashed_main_frame_host=*/browsing_topics::HashedHost(123), - /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}); + /*hashed_context_domains=*/{browsing_topics::HashedDomain(456)}, + base::Time::Now()); task_environment_.FastForwardBy(base::Seconds(1)); diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index 6c8760433c31b7..60020a14ba2e26 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -9193,6 +9193,14 @@ void RenderFrameHostImpl::InsertVisualStateCallback( GetRenderWidgetHost()->InsertVisualStateCallback(std::move(callback)); } +bool RenderFrameHostImpl::IsLastCommitIPAddressPubliclyRoutable() const { + net::IPEndPoint ip_end_point = + last_response_head().get() ? last_response_head().get()->remote_endpoint + : net::IPEndPoint(); + + return ip_end_point.address().IsPubliclyRoutable(); +} + bool RenderFrameHostImpl::IsRenderFrameCreated() { return is_render_frame_created(); } diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h index 9af480bffc35e2..aa3f46b07cd945 100644 --- a/content/browser/renderer_host/render_frame_host_impl.h +++ b/content/browser/renderer_host/render_frame_host_impl.h @@ -387,6 +387,7 @@ class CONTENT_EXPORT RenderFrameHostImpl service_manager::InterfaceProvider* GetRemoteInterfaces() override; blink::AssociatedInterfaceProvider* GetRemoteAssociatedInterfaces() override; content::PageVisibilityState GetVisibilityState() override; + bool IsLastCommitIPAddressPubliclyRoutable() const override; bool IsRenderFrameCreated() override; bool IsRenderFrameLive() override; LifecycleState GetLifecycleState() override; diff --git a/content/public/browser/browsing_topics_site_data_manager.h b/content/public/browser/browsing_topics_site_data_manager.h index 0b1948148ec05c..edc52280e7c052 100644 --- a/content/public/browser/browsing_topics_site_data_manager.h +++ b/content/public/browser/browsing_topics_site_data_manager.h @@ -42,7 +42,8 @@ class CONTENT_EXPORT BrowsingTopicsSiteDataManager { virtual void OnBrowsingTopicsApiUsed( const browsing_topics::HashedHost& hashed_main_frame_host, const base::flat_set& - hashed_context_domains) = 0; + hashed_context_domains, + base::Time time) = 0; }; } // namespace content diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc index 2ba5cc555c6918..bf827fb6d8c2ce 100644 --- a/content/public/browser/content_browser_client.cc +++ b/content/public/browser/content_browser_client.cc @@ -67,6 +67,7 @@ #include "third_party/blink/public/common/loader/url_loader_throttle.h" #include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h" #include "third_party/blink/public/common/user_agent/user_agent_metadata.h" +#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom.h" #include "ui/gfx/image/image_skia.h" #include "ui/shell_dialogs/select_file_policy.h" #include "url/gurl.h" @@ -1157,6 +1158,13 @@ void ContentBrowserClient::AugmentNavigationDownloadPolicy( bool user_gesture, blink::NavigationDownloadPolicy* download_policy) {} +std::vector +ContentBrowserClient::GetBrowsingTopicsForJsApi( + const url::Origin& context_origin, + RenderFrameHost* main_frame) { + return {}; +} + bool ContentBrowserClient::IsBluetoothScanningBlocked( content::BrowserContext* browser_context, const url::Origin& requesting_origin, diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h index 5c757ff4b622d9..de15701d1e8f31 100644 --- a/content/public/browser/content_browser_client.h +++ b/content/public/browser/content_browser_client.h @@ -61,6 +61,7 @@ #include "storage/browser/file_system/file_system_context.h" #include "third_party/abseil-cpp/absl/types/optional.h" #include "third_party/blink/public/common/user_agent/user_agent_metadata.h" +#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom-forward.h" #include "third_party/blink/public/mojom/manifest/manifest.mojom-forward.h" #include "ui/accessibility/ax_mode.h" #include "ui/base/page_transition_types.h" @@ -2008,6 +2009,12 @@ class CONTENT_EXPORT ContentBrowserClient { bool user_gesture, blink::NavigationDownloadPolicy* download_policy); + // Returns the browsing topics associated with the browser context of + // |main_frame|. + virtual std::vector GetBrowsingTopicsForJsApi( + const url::Origin& context_origin, + RenderFrameHost* main_frame); + // Returns whether a site is blocked to use Bluetooth scanning API. virtual bool IsBluetoothScanningBlocked( content::BrowserContext* browser_context, diff --git a/content/public/browser/render_frame_host.h b/content/public/browser/render_frame_host.h index 57b7fb9d2fe77f..b2ef3b338def4e 100644 --- a/content/public/browser/render_frame_host.h +++ b/content/public/browser/render_frame_host.h @@ -620,6 +620,9 @@ class CONTENT_EXPORT RenderFrameHost : public IPC::Listener, // of a frame are defined in Blink. virtual blink::mojom::PageVisibilityState GetVisibilityState() = 0; + // Returns whether the IP address of the last commit was publicly routable. + virtual bool IsLastCommitIPAddressPubliclyRoutable() const = 0; + // Returns true if WebContentsObserver::RenderFrameCreated notification has // been dispatched for this frame, and so a RenderFrameDeleted notification // will later be dispatched for this frame. diff --git a/content/public/test/browsing_topics_test_util.cc b/content/public/test/browsing_topics_test_util.cc index bba7d29ce1fed8..909ce14aaa2739 100644 --- a/content/public/test/browsing_topics_test_util.cc +++ b/content/public/test/browsing_topics_test_util.cc @@ -4,10 +4,46 @@ #include "content/public/test/browsing_topics_test_util.h" +#include "base/test/bind.h" #include "content/browser/browsing_topics/browsing_topics_site_data_manager_impl.h" namespace content { +std::vector GetBrowsingTopicsApiUsage( + content::BrowsingTopicsSiteDataManager* topics_site_data_manager) { + browsing_topics::ApiUsageContextQueryResult query_result; + + base::RunLoop run_loop; + topics_site_data_manager->GetBrowsingTopicsApiUsage( + base::Time(), base::Time::Now() + base::Days(1), + base::BindLambdaForTesting( + [&](browsing_topics::ApiUsageContextQueryResult result) { + query_result = std::move(result); + run_loop.Quit(); + })); + + run_loop.Run(); + + DCHECK(query_result.success); + + std::vector api_usage_contexts = + std::move(query_result.api_usage_contexts); + + std::sort(api_usage_contexts.begin(), api_usage_contexts.end(), + [](auto& left, auto& right) { + return left.hashed_context_domain != right.hashed_context_domain + ? left.hashed_context_domain < + right.hashed_context_domain + : (left.hashed_main_frame_host != + right.hashed_main_frame_host + ? left.hashed_main_frame_host < + right.hashed_main_frame_host + : left.time < right.time); + }); + + return api_usage_contexts; +} + TesterBrowsingTopicsSiteDataManager::TesterBrowsingTopicsSiteDataManager( const base::FilePath& user_data_directory) : manager_impl_( @@ -22,10 +58,10 @@ TesterBrowsingTopicsSiteDataManager::~TesterBrowsingTopicsSiteDataManager() = void TesterBrowsingTopicsSiteDataManager::OnBrowsingTopicsApiUsed( const browsing_topics::HashedHost& hashed_top_host, - const base::flat_set& - hashed_context_domains) { + const base::flat_set& hashed_context_domains, + base::Time time) { manager_impl_->OnBrowsingTopicsApiUsed(hashed_top_host, - hashed_context_domains); + hashed_context_domains, time); } void TesterBrowsingTopicsSiteDataManager::GetBrowsingTopicsApiUsage( diff --git a/content/public/test/browsing_topics_test_util.h b/content/public/test/browsing_topics_test_util.h index eab0e193ca90f2..0f986050b4898a 100644 --- a/content/public/test/browsing_topics_test_util.h +++ b/content/public/test/browsing_topics_test_util.h @@ -12,6 +12,11 @@ namespace content { class BrowsingTopicsSiteDataManagerImpl; +// Synchrnously get all the browsing topics api usage contexts. Entries are +// sorted based on [hashed_context_domain, hashed_main_frame_host, time] +std::vector GetBrowsingTopicsApiUsage( + BrowsingTopicsSiteDataManager* topics_site_data_manager); + // A tester class that allows mocking a query failure (e.g. database error). class TesterBrowsingTopicsSiteDataManager : public BrowsingTopicsSiteDataManager { @@ -37,7 +42,8 @@ class TesterBrowsingTopicsSiteDataManager void OnBrowsingTopicsApiUsed( const browsing_topics::HashedHost& hashed_top_host, const base::flat_set& - hashed_context_domains) override; + hashed_context_domains, + base::Time time) override; void SetQueryFailureOverride() { query_failure_override_ = true; } diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc index 5776b78fc7cc12..c9232540af3b57 100644 --- a/third_party/blink/common/features.cc +++ b/third_party/blink/common/features.cc @@ -1083,6 +1083,12 @@ const base::FeatureParam kBrowsingTopicsMaxNumberOfApiUsageContextEntriesToLoadPerEpoch{ &kBrowsingTopics, "max_number_of_api_usage_context_entries_to_load_per_epoch", 100000}; +// The max number of API usage context domains allowed to be stored per page +// load. +const base::FeatureParam + kBrowsingTopicsMaxNumberOfApiUsageContextDomainsToStorePerPageLoad{ + &kBrowsingTopics, + "max_number_of_api_usage_context_domains_to_store_per_page_load", 30}; // Encodes the configuration parameters above. Each version number should only // be mapped to one configuration set. In practice, this can be guaranteed by // always bumping up the version number whenever parameters are updated. @@ -1093,6 +1099,13 @@ const base::FeatureParam kBrowsingTopicsConfigVersion{&kBrowsingTopics, const base::FeatureParam kBrowsingTopicsTaxonomyVersion{ &kBrowsingTopics, "taxonomy_version", 1}; +// If enabled, the check for whether the IP address is publicly routable will be +// bypassed when determining the eligibility for a page to be included in topics +// calculation. This is useful for developers to test in local environment. +const base::Feature kBrowsingTopicsBypassIPIsPubliclyRoutableCheck{ + "BrowsingTopicsBypassIPIsPubliclyRoutableCheck", + base::FEATURE_DISABLED_BY_DEFAULT}; + // Enable the ability to minimize processing in the WebRTC APM when all audio // tracks are disabled. If disabled, the APM in WebRTC will ignore attempts to // set it in a low-processing mode when all audio tracks are disabled. diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h index a04cbe49afce91..cb0cb3c5858af9 100644 --- a/third_party/blink/public/common/features.h +++ b/third_party/blink/public/common/features.h @@ -482,10 +482,14 @@ BLINK_COMMON_EXPORT extern const base::FeatureParam kBrowsingTopicsMaxNumberOfApiUsageContextDomainsToKeepPerTopic; BLINK_COMMON_EXPORT extern const base::FeatureParam kBrowsingTopicsMaxNumberOfApiUsageContextEntriesToLoadPerEpoch; +BLINK_COMMON_EXPORT extern const base::FeatureParam + kBrowsingTopicsMaxNumberOfApiUsageContextDomainsToStorePerPageLoad; BLINK_COMMON_EXPORT extern const base::FeatureParam kBrowsingTopicsConfigVersion; BLINK_COMMON_EXPORT extern const base::FeatureParam kBrowsingTopicsTaxonomyVersion; +BLINK_COMMON_EXPORT extern const base::Feature + kBrowsingTopicsBypassIPIsPubliclyRoutableCheck; // Control switch for minimizing processing in the WebRTC APM when all audio // tracks are disabled. diff --git a/third_party/blink/public/mojom/BUILD.gn b/third_party/blink/public/mojom/BUILD.gn index 4152fe844de474..83d68e186f14c4 100644 --- a/third_party/blink/public/mojom/BUILD.gn +++ b/third_party/blink/public/mojom/BUILD.gn @@ -31,6 +31,7 @@ mojom("mojom_platform") { "background_sync/background_sync.mojom", "badging/badging.mojom", "browser_interface_broker.mojom", + "browsing_topics/browsing_topics.mojom", "buckets/bucket_manager_host.mojom", "cache_storage/cache_storage.mojom", "choosers/color_chooser.mojom", diff --git a/third_party/blink/public/mojom/browsing_topics/OWNERS b/third_party/blink/public/mojom/browsing_topics/OWNERS new file mode 100644 index 00000000000000..08850f421205f8 --- /dev/null +++ b/third_party/blink/public/mojom/browsing_topics/OWNERS @@ -0,0 +1,2 @@ +per-file *.mojom=set noparent +per-file *.mojom=file://ipc/SECURITY_OWNERS diff --git a/third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom b/third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom new file mode 100644 index 00000000000000..b99c1fdb6d6498 --- /dev/null +++ b/third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom @@ -0,0 +1,40 @@ +// Copyright 2022 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 blink.mojom; + +struct EpochTopic { + // A browsing topic from an epoch, calculated when requested from a context. + // It's always valid, as when there's no topic to give to a context, the + // `EpochTopic` won't be constructed. + int32 topic; + + // The version that identifies the taxonomy and the algorithm used to + // calculate `topic`. This consists of `config_version`, `model_version`, and + // `taxonomy_version`. + string version; + + // Version that identifies the configuration FeatureParams for the Topics API. + string config_version; + + // Version of the model. + string model_version; + + // Version of the taxonomy. + string taxonomy_version; +}; + +// The `BrowsingTopicsDocumentService` provides a method to request the +// browsing topics. It is a per-document interface hosted in the browser +// process. +interface BrowsingTopicsDocumentService { + + // Asynchronously get the browsing topics. Each topic is calculated for a + // particular epoch in the past. Epochs without a valid topic to give to the + // requesting context won't be returned - this could happen if the topics + // calculation for that epoch failed, or if the topics were later cleared, or + // if the candidate topic was filtered for the requesting context and epoch. + // Duplicate `EpochTopic`s are removed and their ordering is random. + GetBrowsingTopics() => (array browsing_topics); +}; diff --git a/third_party/blink/renderer/modules/browsing_topics/browsing_topics_document_supplement.cc b/third_party/blink/renderer/modules/browsing_topics/browsing_topics_document_supplement.cc index 9d938d2ef9ad0d..80f742ea5202bd 100644 --- a/third_party/blink/renderer/modules/browsing_topics/browsing_topics_document_supplement.cc +++ b/third_party/blink/renderer/modules/browsing_topics/browsing_topics_document_supplement.cc @@ -14,6 +14,8 @@ #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" #include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h" #include "third_party/blink/renderer/bindings/modules/v8/v8_browsing_topic.h" +#include "third_party/blink/renderer/core/frame/local_frame.h" +#include "third_party/blink/renderer/core/page/page.h" namespace blink { @@ -45,7 +47,8 @@ ScriptPromise BrowsingTopicsDocumentSupplement::browsingTopics( BrowsingTopicsDocumentSupplement::BrowsingTopicsDocumentSupplement( Document& document) - : Supplement(document) {} + : Supplement(document), + document_host_(document.GetExecutionContext()) {} ScriptPromise BrowsingTopicsDocumentSupplement::GetBrowsingTopics( ScriptState* script_state, @@ -68,6 +71,28 @@ ScriptPromise BrowsingTopicsDocumentSupplement::GetBrowsingTopics( MakeGarbageCollected(script_state); ScriptPromise promise = resolver->Promise(); + // See https://github.com/jkarlin/topics#specific-details for the restrictions + // on the context. + + if (document.GetExecutionContext()->GetSecurityOrigin()->IsOpaque()) { + resolver->Reject(V8ThrowDOMException::CreateOrEmpty( + script_state->GetIsolate(), DOMExceptionCode::kInvalidAccessError, + "document.browsingTopics() is not allowed in an opaque origin " + "context.")); + + return promise; + } + + if (!document.GetFrame()->GetPage()->MainFrame()->IsOutermostMainFrame() || + document.GetFrame()->GetPage()->IsPrerendering()) { + resolver->Reject(V8ThrowDOMException::CreateOrEmpty( + script_state->GetIsolate(), DOMExceptionCode::kInvalidAccessError, + "document.browsingTopics() is only allowed in the primary main frame " + "or in its child iframes.")); + + return promise; + } + if (!document.GetExecutionContext()->IsFeatureEnabled( mojom::blink::PermissionsPolicyFeature::kBrowsingTopics)) { resolver->Reject(V8ThrowDOMException::CreateOrEmpty( @@ -89,11 +114,41 @@ ScriptPromise BrowsingTopicsDocumentSupplement::GetBrowsingTopics( return promise; } - resolver->Resolve(HeapVector>()); + ExecutionContext* execution_context = ExecutionContext::From(script_state); + if (!document_host_.is_bound()) { + execution_context->GetBrowserInterfaceBroker().GetInterface( + document_host_.BindNewPipeAndPassReceiver( + execution_context->GetTaskRunner(TaskType::kMiscPlatformAPI))); + } + + document_host_->GetBrowsingTopics(WTF::Bind( + [](ScriptPromiseResolver* resolver, + BrowsingTopicsDocumentSupplement* supplement, + Vector browsing_topics) { + DCHECK(resolver); + DCHECK(supplement); + + HeapVector> result; + for (const auto& topic : browsing_topics) { + BrowsingTopic* result_topic = BrowsingTopic::Create(); + result_topic->setTopic(topic->topic); + result_topic->setVersion(topic->version); + result_topic->setConfigVersion(topic->config_version); + result_topic->setModelVersion(topic->model_version); + result_topic->setTaxonomyVersion(topic->taxonomy_version); + result.push_back(result_topic); + } + + resolver->Resolve(result); + }, + WrapPersistent(resolver), WrapPersistent(this))); + return promise; } void BrowsingTopicsDocumentSupplement::Trace(Visitor* visitor) const { + visitor->Trace(document_host_); + Supplement::Trace(visitor); } diff --git a/third_party/blink/renderer/modules/browsing_topics/browsing_topics_document_supplement.h b/third_party/blink/renderer/modules/browsing_topics/browsing_topics_document_supplement.h index 52317a168cafa3..e7397369d41237 100644 --- a/third_party/blink/renderer/modules/browsing_topics/browsing_topics_document_supplement.h +++ b/third_party/blink/renderer/modules/browsing_topics/browsing_topics_document_supplement.h @@ -5,6 +5,7 @@ #ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_BROWSING_TOPICS_BROWSING_TOPICS_DOCUMENT_SUPPLEMENT_H_ #define THIRD_PARTY_BLINK_RENDERER_MODULES_BROWSING_TOPICS_BROWSING_TOPICS_DOCUMENT_SUPPLEMENT_H_ +#include "third_party/blink/public/mojom/browsing_topics/browsing_topics.mojom-blink.h" #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/modules/modules_export.h" #include "third_party/blink/renderer/platform/mojo/heap_mojo_remote.h" @@ -36,6 +37,10 @@ class MODULES_EXPORT BrowsingTopicsDocumentSupplement // GC functionality. void Trace(Visitor* visitor) const override; + + private: + // Mojo remote used to query the browsing topics. + HeapMojoRemote document_host_; }; } // namespace blink