browsing_topics_state.cc 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. // Copyright 2022 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/browsing_topics/browsing_topics_state.h"
  5. #include "base/base64.h"
  6. #include "base/files/file_path.h"
  7. #include "base/files/file_util.h"
  8. #include "base/json/json_file_value_serializer.h"
  9. #include "base/json/json_string_value_serializer.h"
  10. #include "base/json/values_util.h"
  11. #include "base/metrics/histogram_functions.h"
  12. #include "base/task/task_runner_util.h"
  13. #include "base/task/task_traits.h"
  14. #include "base/task/thread_pool.h"
  15. #include "components/browsing_topics/util.h"
  16. #include "third_party/blink/public/common/features.h"
  17. namespace browsing_topics {
  18. namespace {
  19. // How often the file is saved at most.
  20. const base::TimeDelta kSaveDelay = base::Milliseconds(2500);
  21. const char kEpochsNameKey[] = "epochs";
  22. const char kNextScheduledCalculationTimeNameKey[] =
  23. "next_scheduled_calculation_time";
  24. const char kHexEncodedHmacKeyNameKey[] = "hex_encoded_hmac_key";
  25. const char kConfigVersionNameKey[] = "config_version";
  26. std::unique_ptr<BrowsingTopicsState::LoadResult> LoadFileOnBackendTaskRunner(
  27. const base::FilePath& file_path) {
  28. bool file_exists = base::PathExists(file_path);
  29. if (!file_exists) {
  30. return std::make_unique<BrowsingTopicsState::LoadResult>(
  31. /*file_exists=*/false, nullptr);
  32. }
  33. JSONFileValueDeserializer deserializer(file_path);
  34. std::unique_ptr<base::Value> value = deserializer.Deserialize(
  35. /*error_code=*/nullptr,
  36. /*error_message=*/nullptr);
  37. return std::make_unique<BrowsingTopicsState::LoadResult>(/*file_exists=*/true,
  38. std::move(value));
  39. }
  40. } // namespace
  41. BrowsingTopicsState::LoadResult::LoadResult(bool file_exists,
  42. std::unique_ptr<base::Value> value)
  43. : file_exists(file_exists), value(std::move(value)) {}
  44. BrowsingTopicsState::LoadResult::~LoadResult() = default;
  45. BrowsingTopicsState::BrowsingTopicsState(const base::FilePath& profile_path,
  46. base::OnceClosure loaded_callback)
  47. : backend_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
  48. {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
  49. base::TaskShutdownBehavior::BLOCK_SHUTDOWN})),
  50. writer_(profile_path.Append(FILE_PATH_LITERAL("BrowsingTopicsState")),
  51. backend_task_runner_,
  52. kSaveDelay,
  53. /*histogram_suffix=*/"BrowsingTopicsState") {
  54. base::PostTaskAndReplyWithResult(
  55. backend_task_runner_.get(), FROM_HERE,
  56. base::BindOnce(&LoadFileOnBackendTaskRunner, writer_.path()),
  57. base::BindOnce(&BrowsingTopicsState::DidLoadFile,
  58. weak_ptr_factory_.GetWeakPtr(),
  59. std::move(loaded_callback)));
  60. }
  61. BrowsingTopicsState::~BrowsingTopicsState() {
  62. if (writer_.HasPendingWrite())
  63. writer_.DoScheduledWrite();
  64. }
  65. void BrowsingTopicsState::ClearAllTopics() {
  66. DCHECK(loaded_);
  67. if (!epochs_.empty()) {
  68. epochs_.clear();
  69. ScheduleSave();
  70. }
  71. }
  72. void BrowsingTopicsState::ClearOneEpoch(size_t epoch_index) {
  73. DCHECK(loaded_);
  74. epochs_[epoch_index].ClearTopics();
  75. ScheduleSave();
  76. }
  77. void BrowsingTopicsState::ClearTopic(Topic topic, int taxonomy_version) {
  78. for (EpochTopics& epoch : epochs_) {
  79. // TODO(crbug.com/1310951): this Chrome version only supports a single
  80. // taxonomy version. When we start writing taxonomy conversion code, we may
  81. // revisit this constraint.
  82. DCHECK_EQ(epoch.taxonomy_version(), taxonomy_version);
  83. epoch.ClearTopic(topic);
  84. }
  85. ScheduleSave();
  86. }
  87. void BrowsingTopicsState::ClearContextDomain(
  88. const HashedDomain& hashed_context_domain) {
  89. for (EpochTopics& epoch : epochs_) {
  90. epoch.ClearContextDomain(hashed_context_domain);
  91. }
  92. ScheduleSave();
  93. }
  94. void BrowsingTopicsState::AddEpoch(EpochTopics epoch_topics) {
  95. DCHECK(loaded_);
  96. epochs_.push_back(std::move(epoch_topics));
  97. // Remove the epoch data that is no longer useful.
  98. if (epochs_.size() >
  99. static_cast<size_t>(
  100. blink::features::kBrowsingTopicsNumberOfEpochsToExpose.Get()) +
  101. 1) {
  102. epochs_.pop_front();
  103. }
  104. ScheduleSave();
  105. }
  106. void BrowsingTopicsState::UpdateNextScheduledCalculationTime() {
  107. DCHECK(loaded_);
  108. next_scheduled_calculation_time_ =
  109. base::Time::Now() +
  110. blink::features::kBrowsingTopicsTimePeriodPerEpoch.Get();
  111. ScheduleSave();
  112. }
  113. std::vector<const EpochTopics*> BrowsingTopicsState::EpochsForSite(
  114. const std::string& top_domain) const {
  115. DCHECK(loaded_);
  116. const size_t kNumberOfEpochsToExpose = static_cast<size_t>(
  117. blink::features::kBrowsingTopicsNumberOfEpochsToExpose.Get());
  118. DCHECK_GT(kNumberOfEpochsToExpose, 0u);
  119. // Derive a per-user per-site time delta in the range of
  120. // [0, kBrowsingTopicsTimePeriodPerEpoch). The latest epoch will be switched
  121. // to use when the current time is within `site_sticky_time_delta` apart from
  122. // the `next_scheduled_calculation_time_`. This way, each site will see a
  123. // different epoch switch time.
  124. base::TimeDelta site_sticky_time_delta =
  125. CalculateSiteStickyTimeDelta(top_domain);
  126. size_t end_epoch_index = 0;
  127. if (base::Time::Now() + site_sticky_time_delta <
  128. next_scheduled_calculation_time_) {
  129. if (epochs_.size() < 2)
  130. return {};
  131. end_epoch_index = epochs_.size() - 2;
  132. } else {
  133. if (epochs_.empty())
  134. return {};
  135. end_epoch_index = epochs_.size() - 1;
  136. }
  137. size_t start_epoch_index = (end_epoch_index + 1 >= kNumberOfEpochsToExpose)
  138. ? end_epoch_index + 1 - kNumberOfEpochsToExpose
  139. : 0;
  140. std::vector<const EpochTopics*> result;
  141. for (size_t i = start_epoch_index; i <= end_epoch_index; ++i) {
  142. result.emplace_back(&epochs_[i]);
  143. }
  144. return result;
  145. }
  146. bool BrowsingTopicsState::HasScheduledSaveForTesting() const {
  147. return writer_.HasPendingWrite();
  148. }
  149. base::TimeDelta BrowsingTopicsState::CalculateSiteStickyTimeDelta(
  150. const std::string& top_domain) const {
  151. uint64_t epoch_switch_time_decision_hash =
  152. HashTopDomainForEpochSwitchTimeDecision(hmac_key_, top_domain);
  153. return base::Seconds(
  154. epoch_switch_time_decision_hash %
  155. blink::features::kBrowsingTopicsTimePeriodPerEpoch.Get().InSeconds());
  156. }
  157. base::ImportantFileWriter::BackgroundDataProducerCallback
  158. BrowsingTopicsState::GetSerializedDataProducerForBackgroundSequence() {
  159. DCHECK(loaded_);
  160. return base::BindOnce(
  161. [](base::Value value, std::string* output) {
  162. // This runs on the background sequence.
  163. JSONStringValueSerializer serializer(output);
  164. serializer.set_pretty_print(true);
  165. return serializer.Serialize(value);
  166. },
  167. base::Value(ToDictValue()));
  168. }
  169. base::Value::Dict BrowsingTopicsState::ToDictValue() const {
  170. DCHECK(loaded_);
  171. base::Value::List epochs_list;
  172. for (const EpochTopics& epoch : epochs_) {
  173. epochs_list.Append(epoch.ToDictValue());
  174. }
  175. base::Value::Dict result_dict;
  176. result_dict.Set(kEpochsNameKey, std::move(epochs_list));
  177. result_dict.Set(kNextScheduledCalculationTimeNameKey,
  178. base::TimeToValue(next_scheduled_calculation_time_));
  179. std::string hex_encoded_hmac_key = base::HexEncode(hmac_key_);
  180. result_dict.Set(kHexEncodedHmacKeyNameKey, base::HexEncode(hmac_key_));
  181. result_dict.Set(kConfigVersionNameKey,
  182. blink::features::kBrowsingTopicsConfigVersion.Get());
  183. return result_dict;
  184. }
  185. void BrowsingTopicsState::ScheduleSave() {
  186. DCHECK(loaded_);
  187. writer_.ScheduleWriteWithBackgroundDataSerializer(this);
  188. }
  189. void BrowsingTopicsState::DidLoadFile(base::OnceClosure loaded_callback,
  190. std::unique_ptr<LoadResult> load_result) {
  191. DCHECK(load_result);
  192. DCHECK(!loaded_);
  193. bool success = false;
  194. bool should_save_state_to_file = false;
  195. if (!load_result->file_exists) {
  196. // If this is the first time loading, generate a `hmac_key_`, and save it.
  197. // This ensures we only generate the key once per profile, as data derived
  198. // from the key may be subsequently stored elsewhere.
  199. hmac_key_ = GenerateRandomHmacKey();
  200. success = true;
  201. should_save_state_to_file = true;
  202. } else if (!load_result->value) {
  203. // If a file read error was encountered, or if the JSON deserialization
  204. // failed in general, empty the file.
  205. should_save_state_to_file = true;
  206. } else {
  207. // JSON deserialization succeeded in general. Parse the value to individual
  208. // fields.
  209. ParseResult parse_result = ParseValue(*(load_result->value));
  210. success = parse_result.success;
  211. should_save_state_to_file = parse_result.should_save_state_to_file;
  212. }
  213. base::UmaHistogramBoolean(
  214. "BrowsingTopics.BrowsingTopicsState.LoadFinishStatus", success);
  215. loaded_ = true;
  216. if (should_save_state_to_file)
  217. ScheduleSave();
  218. std::move(loaded_callback).Run();
  219. }
  220. BrowsingTopicsState::ParseResult BrowsingTopicsState::ParseValue(
  221. const base::Value& value) {
  222. DCHECK(!loaded_);
  223. const base::Value::Dict* dict_value = value.GetIfDict();
  224. if (!dict_value)
  225. return ParseResult{.success = false, .should_save_state_to_file = true};
  226. const std::string* hex_encoded_hmac_key =
  227. dict_value->FindString(kHexEncodedHmacKeyNameKey);
  228. if (!hex_encoded_hmac_key)
  229. return ParseResult{.success = false, .should_save_state_to_file = true};
  230. if (!base::HexStringToSpan(*hex_encoded_hmac_key, hmac_key_)) {
  231. // `HexStringToSpan` may partially fill the `hmac_key_` up until the
  232. // failure. Reset it to empty.
  233. hmac_key_.fill(0);
  234. return ParseResult{.success = false, .should_save_state_to_file = true};
  235. }
  236. absl::optional<int> config_version_in_storage =
  237. dict_value->FindInt(kConfigVersionNameKey);
  238. if (!config_version_in_storage)
  239. return ParseResult{.success = false, .should_save_state_to_file = true};
  240. // If the config is has been updated, start with a fresh `epoch_`.
  241. if (*config_version_in_storage !=
  242. blink::features::kBrowsingTopicsConfigVersion.Get()) {
  243. return ParseResult{.success = true, .should_save_state_to_file = true};
  244. }
  245. const base::Value::List* epochs_value = dict_value->FindList(kEpochsNameKey);
  246. if (!epochs_value)
  247. return ParseResult{.success = false, .should_save_state_to_file = true};
  248. for (const base::Value& epoch_value : *epochs_value) {
  249. const base::Value::Dict* epoch_dict_value = epoch_value.GetIfDict();
  250. if (!epoch_dict_value)
  251. return ParseResult{.success = false, .should_save_state_to_file = true};
  252. epochs_.push_back(EpochTopics::FromDictValue(*epoch_dict_value));
  253. }
  254. const base::Value* next_scheduled_calculation_time_value =
  255. dict_value->Find(kNextScheduledCalculationTimeNameKey);
  256. if (!next_scheduled_calculation_time_value)
  257. return ParseResult{.success = false, .should_save_state_to_file = true};
  258. next_scheduled_calculation_time_ =
  259. base::ValueToTime(next_scheduled_calculation_time_value).value();
  260. return ParseResult{.success = true, .should_save_state_to_file = false};
  261. }
  262. } // namespace browsing_topics