diff --git a/app/brave_settings_strings.grdp b/app/brave_settings_strings.grdp index 51832bfa38c..4d7cc2d8c18 100644 --- a/app/brave_settings_strings.grdp +++ b/app/brave_settings_strings.grdp @@ -776,6 +776,37 @@ Use Google services for push messaging + + + Local history retention + + + How long Brave stores your browsing history locally + + + 1 day + + + 1 week + + + 1 month + + + 3 months + + + 6 months + + + 1 year + + + 5 years + + + Forever + Automatically send diagnostic reports diff --git a/browser/brave_profile_prefs.cc b/browser/brave_profile_prefs.cc index 173e0bbd826..a83e74a7212 100644 --- a/browser/brave_profile_prefs.cc +++ b/browser/brave_profile_prefs.cc @@ -65,6 +65,7 @@ #include "components/content_settings/core/common/pref_names.h" #include "components/embedder_support/pref_names.h" #include "components/gcm_driver/gcm_buildflags.h" +#include "components/history/core/common/pref_names.h" #include "components/ntp_tiles/tile_type.h" #include "components/omnibox/browser/omnibox_prefs.h" #include "components/password_manager/core/common/password_manager_pref_names.h" @@ -438,6 +439,7 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) { registry->RegisterBooleanPref(kShieldsStatsBadgeVisible, true); registry->RegisterBooleanPref(kGoogleLoginControlType, true); + registry->RegisterIntegerPref(prefs::kBraveHistoryRetentionDays, 90); registry->RegisterBooleanPref( query_filter::kTrackingQueryParametersFilteringEnabled, true); registry->RegisterBooleanPref( diff --git a/browser/extensions/BUILD.gn b/browser/extensions/BUILD.gn index c15d9e250a5..5df7fc9a3b4 100644 --- a/browser/extensions/BUILD.gn +++ b/browser/extensions/BUILD.gn @@ -130,6 +130,7 @@ source_set("extensions") { "//chrome/common", "//components/browsing_data/core", "//components/gcm_driver:gcm_buildflags", + "//components/history/core/common", "//components/prefs", "//components/update_client:network_impl", "//components/user_prefs", diff --git a/browser/extensions/api/settings_private/brave_prefs_util.cc b/browser/extensions/api/settings_private/brave_prefs_util.cc index 3f2df16cfbc..4a8ab6e5038 100644 --- a/browser/extensions/api/settings_private/brave_prefs_util.cc +++ b/browser/extensions/api/settings_private/brave_prefs_util.cc @@ -33,6 +33,7 @@ #include "components/bookmarks/common/bookmark_pref_names.h" #include "components/browsing_data/core/pref_names.h" #include "components/gcm_driver/gcm_buildflags.h" +#include "components/history/core/common/pref_names.h" #include "components/omnibox/browser/omnibox_prefs.h" #include "extensions/buildflags/buildflags.h" @@ -317,6 +318,8 @@ const PrefsUtil::TypedPrefMap& BravePrefsUtil::GetAllowlistedKeys() { #endif (*s_brave_allowlist)[prefs::kWebRTCIPHandlingPolicy] = settings_api::PrefType::kString; + (*s_brave_allowlist)[prefs::kBraveHistoryRetentionDays] = + settings_api::PrefType::kNumber; // Request OTR feature (*s_brave_allowlist)[request_otr::kRequestOTRActionOption] = settings_api::PrefType::kNumber; diff --git a/browser/resources/settings/brave_privacy_page/brave_personalization_options.html b/browser/resources/settings/brave_privacy_page/brave_personalization_options.html index 84c5283ab06..e5e35fddee5 100644 --- a/browser/resources/settings/brave_privacy_page/brave_personalization_options.html +++ b/browser/resources/settings/brave_privacy_page/brave_personalization_options.html @@ -28,6 +28,18 @@ +
+
+
$i18n{historyRetentionLabel}
+
+ $i18n{historyRetentionSubLabel} +
+
+ + +
InitHistoryRetentionPref( \ + Profile::FromBrowserContext(context)->GetPrefs()); + +#include + +#undef BRAVE_BUILD_HISTORY_SERVICE diff --git a/chromium_src/components/history/core/browser/expire_history_backend.cc b/chromium_src/components/history/core/browser/expire_history_backend.cc new file mode 100644 index 00000000000..c343905a5ef --- /dev/null +++ b/chromium_src/components/history/core/browser/expire_history_backend.cc @@ -0,0 +1,42 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#define BRAVE_EXPIRE_HISTORY_BACKEND_START_EXPIRING_OLD_STUFF \ + work_queue_ = {}; \ + weak_factory_.InvalidateWeakPtrs(); \ + if (expiration_threshold_.is_max()) { \ + return; \ + } + +#include + +namespace history { + +void ExpireHistoryBackend::UpdateExpirationThreshold( + base::TimeDelta threshold) { + // StartExpiringOldStuff() schedules a self-perpetuating cleanup loop, so pref + // changes must update the running loop instead of starting another one. If + // the new value is "forever", cancel pending expiration work. If the previous + // value was "forever", the loop is no longer running and must be started + // once. Otherwise the existing loop will pick up the new threshold on its + // next pass. + if (threshold.is_max()) { + expiration_threshold_ = threshold; + work_queue_ = {}; + weak_factory_.InvalidateWeakPtrs(); + return; + } + + if (expiration_threshold_.is_max()) { + StartExpiringOldStuff(threshold); + return; + } + + expiration_threshold_ = threshold; +} + +} // namespace history + +#undef BRAVE_EXPIRE_HISTORY_BACKEND_START_EXPIRING_OLD_STUFF diff --git a/chromium_src/components/history/core/browser/expire_history_backend.h b/chromium_src/components/history/core/browser/expire_history_backend.h new file mode 100644 index 00000000000..273e1f3b48e --- /dev/null +++ b/chromium_src/components/history/core/browser/expire_history_backend.h @@ -0,0 +1,17 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_CHROMIUM_SRC_COMPONENTS_HISTORY_CORE_BROWSER_EXPIRE_HISTORY_BACKEND_H_ +#define BRAVE_CHROMIUM_SRC_COMPONENTS_HISTORY_CORE_BROWSER_EXPIRE_HISTORY_BACKEND_H_ + +#define ClearOldOnDemandFaviconsIfPossible(...) \ + ClearOldOnDemandFaviconsIfPossible(__VA_ARGS__); \ + void UpdateExpirationThreshold(base::TimeDelta threshold) + +#include // IWYU pragma: export + +#undef ClearOldOnDemandFaviconsIfPossible + +#endif // BRAVE_CHROMIUM_SRC_COMPONENTS_HISTORY_CORE_BROWSER_EXPIRE_HISTORY_BACKEND_H_ diff --git a/chromium_src/components/history/core/browser/history_backend.cc b/chromium_src/components/history/core/browser/history_backend.cc index 007ac2c3be8..ae5bc35f630 100644 --- a/chromium_src/components/history/core/browser/history_backend.cc +++ b/chromium_src/components/history/core/browser/history_backend.cc @@ -12,4 +12,8 @@ HistoryCountResult HistoryBackend::GetKnownToSyncCount() { return {db_ && db_->GetKnownToSyncCount(&count), count}; } +void HistoryBackend::UpdateExpirationThreshold(base::TimeDelta threshold) { + expirer_.UpdateExpirationThreshold(threshold); +} + } // namespace history diff --git a/chromium_src/components/history/core/browser/history_backend.h b/chromium_src/components/history/core/browser/history_backend.h index 73a2f0f18d6..1ea17370398 100644 --- a/chromium_src/components/history/core/browser/history_backend.h +++ b/chromium_src/components/history/core/browser/history_backend.h @@ -6,8 +6,11 @@ #ifndef BRAVE_CHROMIUM_SRC_COMPONENTS_HISTORY_CORE_BROWSER_HISTORY_BACKEND_H_ #define BRAVE_CHROMIUM_SRC_COMPONENTS_HISTORY_CORE_BROWSER_HISTORY_BACKEND_H_ -#define GetHistoryCount \ - GetKnownToSyncCount(); \ +#include "base/time/time.h" + +#define GetHistoryCount \ + GetKnownToSyncCount(); \ + void UpdateExpirationThreshold(base::TimeDelta threshold); \ HistoryCountResult GetHistoryCount #include // IWYU pragma: export diff --git a/chromium_src/components/history/core/browser/history_service.cc b/chromium_src/components/history/core/browser/history_service.cc index 103452563e4..c46e0130a0b 100644 --- a/chromium_src/components/history/core/browser/history_service.cc +++ b/chromium_src/components/history/core/browser/history_service.cc @@ -3,10 +3,29 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at https://mozilla.org/MPL/2.0/. */ +#include "components/history/core/browser/history_service.h" + +#include "components/history/core/common/pref_names.h" +#include "components/prefs/pref_service.h" + #include namespace history { +namespace { + +constexpr int kDefaultHistoryRetentionDays = 90; +constexpr int kKeepHistoryForever = -1; + +base::TimeDelta HistoryRetentionDaysToTimeDelta(int days) { + if (days == kKeepHistoryForever) { + return base::TimeDelta::Max(); + } + return base::Days(days > 0 ? days : kDefaultHistoryRetentionDays); +} + +} // namespace + void HistoryService::GetKnownToSyncCount( base::OnceCallback callback) { backend_task_runner_->PostTaskAndReplyWithResult( @@ -15,4 +34,22 @@ void HistoryService::GetKnownToSyncCount( std::move(callback)); } +void HistoryService::InitHistoryRetentionPref(PrefService* prefs) { + DCHECK(prefs); + + history_retention_days_.Init( + prefs::kBraveHistoryRetentionDays, prefs, + base::BindRepeating(&HistoryService::OnHistoryRetentionDaysChanged, + base::Unretained(this))); + OnHistoryRetentionDaysChanged(); +} + +void HistoryService::OnHistoryRetentionDaysChanged() { + const base::TimeDelta threshold = + HistoryRetentionDaysToTimeDelta(history_retention_days_.GetValue()); + backend_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&HistoryBackend::UpdateExpirationThreshold, + history_backend_, threshold)); +} + } // namespace history diff --git a/chromium_src/components/history/core/browser/history_service.h b/chromium_src/components/history/core/browser/history_service.h index da5fa540afa..55482820309 100644 --- a/chromium_src/components/history/core/browser/history_service.h +++ b/chromium_src/components/history/core/browser/history_service.h @@ -6,6 +6,10 @@ #ifndef BRAVE_CHROMIUM_SRC_COMPONENTS_HISTORY_CORE_BROWSER_HISTORY_SERVICE_H_ #define BRAVE_CHROMIUM_SRC_COMPONENTS_HISTORY_CORE_BROWSER_HISTORY_SERVICE_H_ +#include "components/prefs/pref_member.h" + +class PrefService; + class BraveHistoryURLProviderTest; class BraveHistoryQuickProviderTest; @@ -18,6 +22,13 @@ class BraveHistoryQuickProviderTest; #define AddRelatedSearchesForVisit \ GetKnownToSyncCount( \ base::OnceCallback callback); \ + void InitHistoryRetentionPref(PrefService* prefs); \ + void OnHistoryRetentionDaysChanged(); \ + \ + private: \ + IntegerPrefMember history_retention_days_; \ + \ + public: \ void AddRelatedSearchesForVisit #include // IWYU pragma: export diff --git a/chromium_src/components/history/core/common/pref_names.h b/chromium_src/components/history/core/common/pref_names.h new file mode 100644 index 00000000000..5bf20bed7b0 --- /dev/null +++ b/chromium_src/components/history/core/common/pref_names.h @@ -0,0 +1,19 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_CHROMIUM_SRC_COMPONENTS_HISTORY_CORE_COMMON_PREF_NAMES_H_ +#define BRAVE_CHROMIUM_SRC_COMPONENTS_HISTORY_CORE_COMMON_PREF_NAMES_H_ + +#include // IWYU pragma: export + +namespace prefs { + +// Number of days to retain browsing history. -1 keeps history forever. +inline constexpr char kBraveHistoryRetentionDays[] = + "brave.history.retention_days"; + +} // namespace prefs + +#endif // BRAVE_CHROMIUM_SRC_COMPONENTS_HISTORY_CORE_COMMON_PREF_NAMES_H_ diff --git a/components/history/core/browser/BUILD.gn b/components/history/core/browser/BUILD.gn index 5f83b7d3262..77783d6858a 100644 --- a/components/history/core/browser/BUILD.gn +++ b/components/history/core/browser/BUILD.gn @@ -6,12 +6,19 @@ source_set("unit_tests") { testonly = true sources = [ + "//brave/components/history/core/browser/brave_expire_history_backend_unittest.cc", "//brave/components/history/core/browser/brave_visit_database_unittest.cc", ] deps = [ "//base", + "//base/test:test_support", "//components/history/core/browser", + "//components/history/core/common", + "//components/history/core/test", + "//components/prefs:test_support", "//sql:test_support", "//testing/gtest", + "//ui/base", + "//url", ] } diff --git a/components/history/core/browser/DEPS b/components/history/core/browser/DEPS index 292839e12a7..9cf8f8f1f90 100644 --- a/components/history/core/browser/DEPS +++ b/components/history/core/browser/DEPS @@ -1,5 +1,6 @@ include_rules = [ "+components/history", + "+components/prefs", "+components/sync", "+sql/test", ] diff --git a/components/history/core/browser/brave_expire_history_backend_unittest.cc b/components/history/core/browser/brave_expire_history_backend_unittest.cc new file mode 100644 index 00000000000..f1c4be9ab33 --- /dev/null +++ b/components/history/core/browser/brave_expire_history_backend_unittest.cc @@ -0,0 +1,217 @@ +/* Copyright (c) 2026 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include +#include +#include + +#include "base/files/file_path.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "base/task/cancelable_task_tracker.h" +#include "base/test/task_environment.h" +#include "base/test/test_future.h" +#include "base/time/time.h" +#include "components/history/core/browser/expire_history_backend.h" +#include "components/history/core/browser/history_backend_notifier.h" +#include "components/history/core/browser/history_constants.h" +#include "components/history/core/browser/history_database.h" +#include "components/history/core/browser/history_service.h" +#include "components/history/core/browser/history_types.h" +#include "components/history/core/common/pref_names.h" +#include "components/history/core/test/history_service_test_util.h" +#include "components/history/core/test/test_history_database.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/testing_pref_service.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/page_transition_types.h" +#include "url/gurl.h" + +namespace history { + +namespace { + +constexpr char kExpiredUrl[] = "https://expired.example/"; +constexpr char kCurrentUrl[] = "https://current.example/"; +constexpr char kForeverUrl[] = "https://forever.example/"; + +bool QueryURL(HistoryService* history_service, + const GURL& url, + base::CancelableTaskTracker* tracker) { + base::test::TestFuture future; + history_service->QueryURL(url, future.GetCallback(), tracker); + return future.Get().success; +} + +} // namespace + +class BraveExpireHistoryBackendTest : public testing::Test, + public HistoryBackendNotifier { + public: + BraveExpireHistoryBackendTest() + : expirer_(this, + /*backend_client=*/nullptr, + task_environment_.GetMainThreadTaskRunner()) {} + + protected: + base::test::TaskEnvironment task_environment_{ + base::test::TaskEnvironment::TimeSource::MOCK_TIME}; + ExpireHistoryBackend expirer_; + + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + + main_db_ = std::make_unique(); + ASSERT_EQ(sql::INIT_OK, + main_db_->Init(temp_dir_.GetPath().Append(kHistoryFilename))); + expirer_.SetDatabases(main_db_.get(), /*favicon_db=*/nullptr); + } + + void TearDown() override { + expirer_.SetDatabases(nullptr, nullptr); + main_db_.reset(); + } + + URLID AddPageWithVisit(const GURL& url, base::Time visit_time) { + URLRow url_row(url); + url_row.set_last_visit(visit_time); + url_row.set_visit_count(1); + const URLID url_id = main_db_->AddURL(url_row); + + VisitRow visit_row; + visit_row.url_id = url_id; + visit_row.visit_time = visit_time; + visit_row.transition = ui::PAGE_TRANSITION_TYPED; + visit_row.source = SOURCE_BROWSED; + EXPECT_TRUE(main_db_->AddVisit(&visit_row)); + return url_id; + } + + bool HasURL(URLID url_id) { + URLRow url_row; + return main_db_->GetURLRow(url_id, &url_row); + } + + void RunExpirationTimer() { + task_environment_.FastForwardBy(base::Seconds(31)); + } + + protected: + // HistoryBackendNotifier: + void NotifyFaviconsChanged(const std::set& page_urls, + const GURL& icon_url) override {} + void NotifyURLVisited(VisitedURLInfo visited_url_info) override {} + void NotifyURLsModified(const URLRows& changed_urls, + bool is_from_expiration) override {} + void NotifyDeletions(DeletionInfo deletion_info) override {} + void NotifyVisitUpdated(const VisitRow& visit, + VisitUpdateReason reason) override {} + void NotifyVisitsDeleted(const std::vector& visits) override {} + + private: + base::ScopedTempDir temp_dir_; + std::unique_ptr main_db_; +}; + +TEST_F(BraveExpireHistoryBackendTest, ExpiresOldVisitsForPositiveThreshold) { + const URLID expired_url_id = + AddPageWithVisit(GURL(kExpiredUrl), base::Time::Now() - base::Days(2)); + const URLID current_url_id = + AddPageWithVisit(GURL(kCurrentUrl), base::Time::Now()); + + expirer_.StartExpiringOldStuff(base::Days(1)); + RunExpirationTimer(); + + EXPECT_FALSE(HasURL(expired_url_id)); + EXPECT_TRUE(HasURL(current_url_id)); +} + +TEST_F(BraveExpireHistoryBackendTest, ForeverCancelsPendingExpiration) { + const URLID expired_url_id = + AddPageWithVisit(GURL(kExpiredUrl), base::Time::Now() - base::Days(2)); + + expirer_.StartExpiringOldStuff(base::Days(1)); + expirer_.StartExpiringOldStuff(base::TimeDelta::Max()); + RunExpirationTimer(); + + EXPECT_TRUE(HasURL(expired_url_id)); +} + +class BraveHistoryRetentionPrefTest : public testing::Test { + protected: + void SetUp() override { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + prefs_.registry()->RegisterIntegerPref(prefs::kBraveHistoryRetentionDays, + 90); + history_service_ = CreateHistoryService(temp_dir_.GetPath(), + /*create_db=*/true); + ASSERT_TRUE(history_service_); + history_service_->InitHistoryRetentionPref(&prefs_); + } + + void TearDown() override { + if (history_service_) { + base::RunLoop run_loop; + history_service_->SetOnBackendDestroyTask(run_loop.QuitClosure()); + history_service_.reset(); + run_loop.Run(); + } + } + + void AddPageWithVisit(const GURL& url, base::Time visit_time) { + history_service_->AddPageWithDetails(url, std::u16string(), + /*visit_count=*/1, + /*typed_count=*/0, visit_time, + /*hidden=*/false, SOURCE_BROWSED); + BlockUntilHistoryProcessesPendingRequests(history_service_.get()); + } + + void SetRetentionDays(int days) { + prefs_.SetInteger(prefs::kBraveHistoryRetentionDays, days); + BlockUntilHistoryProcessesPendingRequests(history_service_.get()); + } + + void RunExpirationTimer() { + task_environment_.FastForwardBy(base::Seconds(31)); + BlockUntilHistoryProcessesPendingRequests(history_service_.get()); + } + + bool HasURL(const GURL& url) { + return QueryURL(history_service_.get(), url, &tracker_); + } + + private: + base::test::TaskEnvironment task_environment_{ + base::test::TaskEnvironment::TimeSource::MOCK_TIME}; + base::ScopedTempDir temp_dir_; + TestingPrefServiceSimple prefs_; + std::unique_ptr history_service_; + base::CancelableTaskTracker tracker_; +}; + +TEST_F(BraveHistoryRetentionPrefTest, PrefChangeExpiresOldVisits) { + const GURL expired_url(kExpiredUrl); + const GURL current_url(kCurrentUrl); + AddPageWithVisit(expired_url, base::Time::Now() - base::Days(2)); + AddPageWithVisit(current_url, base::Time::Now()); + + SetRetentionDays(1); + RunExpirationTimer(); + + EXPECT_FALSE(HasURL(expired_url)); + EXPECT_TRUE(HasURL(current_url)); +} + +TEST_F(BraveHistoryRetentionPrefTest, ForeverPrefKeepsOldVisits) { + const GURL forever_url(kForeverUrl); + AddPageWithVisit(forever_url, base::Time::Now() - base::Days(2)); + + SetRetentionDays(-1); + RunExpirationTimer(); + + EXPECT_TRUE(HasURL(forever_url)); +} + +} // namespace history diff --git a/patches/chrome-browser-history-history_service_factory.cc.patch b/patches/chrome-browser-history-history_service_factory.cc.patch new file mode 100644 index 00000000000..4976028852b --- /dev/null +++ b/patches/chrome-browser-history-history_service_factory.cc.patch @@ -0,0 +1,11 @@ +diff --git a/chrome/browser/history/history_service_factory.cc b/chrome/browser/history/history_service_factory.cc +index f51a9c7b49118cddda8b75d38804c3e9875fef43..b17c0b2f00b86dd07cd46d2a127f75dcf759057f 100644 +--- a/chrome/browser/history/history_service_factory.cc ++++ b/chrome/browser/history/history_service_factory.cc +@@ -30,5 +30,6 @@ std::unique_ptr BuildHistoryService( + context->GetPath(), chrome::GetChannel()))) { + return nullptr; + } ++ BRAVE_BUILD_HISTORY_SERVICE + return history_service; + } diff --git a/patches/components-history-core-browser-expire_history_backend.cc.patch b/patches/components-history-core-browser-expire_history_backend.cc.patch new file mode 100644 index 00000000000..f53b1c31291 --- /dev/null +++ b/patches/components-history-core-browser-expire_history_backend.cc.patch @@ -0,0 +1,9 @@ +diff --git a/components/history/core/browser/expire_history_backend.cc b/components/history/core/browser/expire_history_backend.cc +index 193014683e670facf7192eccf392c800cb2be879..2351d061567bfe09d5a0421fcc6d0d7bc08e4287 100644 +--- a/components/history/core/browser/expire_history_backend.cc ++++ b/components/history/core/browser/expire_history_backend.cc +@@ -397,3 +397,4 @@ void ExpireHistoryBackend::StartExpiringOldStuff( + // Remove all readers, just in case this was method was called before. + readers_.clear(); ++ BRAVE_EXPIRE_HISTORY_BACKEND_START_EXPIRING_OLD_STUFF + // For now, we explicitly add all known readers. If we come up with more