| // Copyright 2019 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 "chrome/browser/android/usage_stats/usage_stats_database.h" |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/strings/safe_sprintf.h" |
| #include "base/strings/strcat.h" |
| #include "base/task/post_task.h" |
| #include "chrome/browser/android/usage_stats/website_event.pb.h" |
| #include "components/leveldb_proto/content/proto_database_provider_factory.h" |
| #include "components/leveldb_proto/public/proto_database_provider.h" |
| |
| namespace usage_stats { |
| |
| const char kNamespace[] = "usage_stats"; |
| const char kTypePrefix[] = "usage_stats"; |
| |
| const char kKeySeparator[] = "_"; |
| |
| const char kWebsiteEventPrefix[] = "website_event"; |
| const char kSuspensionPrefix[] = "suspension"; |
| const char kTokenMappingPrefix[] = "token_mapping"; |
| |
| const int kUnixTimeDigits = 11; |
| // Formats an integer with a minimum width of 11, right-justified, and |
| // zero-filled (example: 1548353315 => 01548353315). |
| const char kUnixTimeFormat[] = "%011d"; |
| |
| const int kFqdnPosition = base::size(kWebsiteEventPrefix) + kUnixTimeDigits + 1; |
| |
| UsageStatsDatabase::UsageStatsDatabase(Profile* profile) |
| : weak_ptr_factory_(this) { |
| leveldb_proto::ProtoDatabaseProvider* db_provider = |
| leveldb_proto::ProtoDatabaseProviderFactory::GetInstance() |
| ->GetForBrowserContext(profile); |
| |
| base::FilePath usage_stats_dir = profile->GetPath().Append(kNamespace); |
| |
| // TODO(crbug/921133): Switch back to separate dbs for each message type when |
| // possible. |
| proto_db_ = db_provider->GetDB<UsageStat>( |
| kNamespace, kTypePrefix, usage_stats_dir, |
| base::CreateSequencedTaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT})); |
| } |
| |
| UsageStatsDatabase::UsageStatsDatabase( |
| std::unique_ptr<leveldb_proto::ProtoDatabase<UsageStat>> proto_db) |
| : proto_db_(std::move(proto_db)), weak_ptr_factory_(this) {} |
| |
| UsageStatsDatabase::~UsageStatsDatabase() = default; |
| |
| namespace { |
| |
| bool DoesNotContainFilter(const base::flat_set<std::string>& set, |
| const std::string& key) { |
| return !set.contains(key); |
| } |
| |
| bool KeyContainsDomainFilter(const base::flat_set<std::string>& domains, |
| const std::string& key) { |
| return domains.contains(key.substr(kFqdnPosition)); |
| } |
| |
| UsageStatsDatabase::Error ToError(bool isSuccess) { |
| return isSuccess ? UsageStatsDatabase::Error::kNoError |
| : UsageStatsDatabase::Error::kUnknownError; |
| } |
| |
| std::string CreateWebsiteEventKey(int64_t seconds, const std::string& fqdn) { |
| // Zero-pad the Unix time in seconds since epoch. Allows ascending timestamps |
| // to sort lexicographically, supporting efficient range queries by key. |
| char unixTime[kUnixTimeDigits + 1]; |
| ssize_t printed = |
| base::strings::SafeSPrintf(unixTime, kUnixTimeFormat, seconds); |
| DCHECK(printed == kUnixTimeDigits); |
| |
| // Create the key from the prefix, time, and fqdn (example: |
| // website_event_01548276551_foo.com). |
| return base::StrCat( |
| {kWebsiteEventPrefix, kKeySeparator, unixTime, kKeySeparator, fqdn}); |
| } |
| |
| } // namespace |
| |
| void UsageStatsDatabase::GetAllEvents(EventsCallback callback) { |
| // Load all UsageStats with the website events prefix. |
| proto_db_->LoadEntriesWithFilter( |
| leveldb_proto::KeyFilter(), leveldb::ReadOptions(), kWebsiteEventPrefix, |
| base::BindOnce(&UsageStatsDatabase::OnLoadEntriesForGetAllEvents, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void UsageStatsDatabase::QueryEventsInRange(int64_t start, |
| int64_t end, |
| EventsCallback callback) { |
| // Load all UsageStats with the website events prefix, where the timestamp is |
| // in the specified range. Function accepts a half-open range [start, end) as |
| // input, but the database operates on fully-closed ranges. Because the |
| // timestamps are represented by integers, [start, end) is equivalent to |
| // [start, end - 1]. |
| proto_db_->LoadKeysAndEntriesInRange( |
| CreateWebsiteEventKey(start, ""), CreateWebsiteEventKey(end - 1, ""), |
| base::BindOnce(&UsageStatsDatabase::OnLoadEntriesForQueryEventsInRange, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void UsageStatsDatabase::AddEvents(std::vector<WebsiteEvent> events, |
| StatusCallback callback) { |
| auto entries = std::make_unique< |
| leveldb_proto::ProtoDatabase<UsageStat>::KeyEntryVector>(); |
| entries->reserve(events.size()); |
| |
| for (WebsiteEvent event : events) { |
| std::string key = |
| CreateWebsiteEventKey(event.timestamp().seconds(), event.fqdn()); |
| |
| UsageStat value; |
| WebsiteEvent* website_event = value.mutable_website_event(); |
| *website_event = event; |
| |
| entries->emplace_back(key, value); |
| } |
| |
| // Add all entries created from input vector. |
| proto_db_->UpdateEntries( |
| std::move(entries), std::make_unique<std::vector<std::string>>(), |
| base::BindOnce(&UsageStatsDatabase::OnUpdateEntries, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void UsageStatsDatabase::DeleteAllEvents(StatusCallback callback) { |
| // Remove all entries with the website event prefix. |
| proto_db_->UpdateEntriesWithRemoveFilter( |
| std::make_unique< |
| leveldb_proto::ProtoDatabase<UsageStat>::KeyEntryVector>(), |
| base::BindRepeating([](const std::string& key) { return true; }), |
| kWebsiteEventPrefix, |
| base::BindOnce(&UsageStatsDatabase::OnUpdateEntries, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| void UsageStatsDatabase::DeleteEventsInRange(int64_t start, |
| int64_t end, |
| StatusCallback callback) { |
| // TODO(crbug/927655): If/when leveldb_proto adds an UpdateEntriesInRange |
| // function, we should consolidate these two proto_db_ calls into a single |
| // call. |
| |
| // Load all keys with the website events prefix, where the timestamp is |
| // in the specified range, passing a callback to delete those entries. |
| // Function accepts a half-open range [start, end) as input, but the database |
| // operates on fully-closed ranges. Because the timestamps are represented by |
| // integers, [start, end) is equivalent to [start, end - 1]. |
| proto_db_->LoadKeysAndEntriesInRange( |
| CreateWebsiteEventKey(start, ""), CreateWebsiteEventKey(end - 1, ""), |
| base::BindOnce(&UsageStatsDatabase::OnLoadEntriesForDeleteEventsInRange, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void UsageStatsDatabase::DeleteEventsWithMatchingDomains( |
| base::flat_set<std::string> domains, |
| StatusCallback callback) { |
| // Remove all events with domains in the given set. |
| proto_db_->UpdateEntriesWithRemoveFilter( |
| std::make_unique< |
| leveldb_proto::ProtoDatabase<UsageStat>::KeyEntryVector>(), |
| base::BindRepeating(&KeyContainsDomainFilter, std::move(domains)), |
| base::BindOnce(&UsageStatsDatabase::OnUpdateEntries, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void UsageStatsDatabase::GetAllSuspensions(SuspensionsCallback callback) { |
| // Load all UsageStats with the suspension prefix. |
| proto_db_->LoadEntriesWithFilter( |
| leveldb_proto::KeyFilter(), leveldb::ReadOptions(), kSuspensionPrefix, |
| base::BindOnce(&UsageStatsDatabase::OnLoadEntriesForGetAllSuspensions, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void UsageStatsDatabase::SetSuspensions(base::flat_set<std::string> domains, |
| StatusCallback callback) { |
| std::vector<std::string> keys; |
| keys.reserve(domains.size()); |
| |
| auto entries = std::make_unique< |
| leveldb_proto::ProtoDatabase<UsageStat>::KeyEntryVector>(); |
| |
| for (std::string domain : domains) { |
| // Prepend prefix to form key. |
| std::string key = base::StrCat({kSuspensionPrefix, kKeySeparator, domain}); |
| |
| keys.emplace_back(key); |
| |
| UsageStat value; |
| Suspension* suspension = value.mutable_suspension(); |
| suspension->set_fqdn(domain); |
| |
| entries->emplace_back(key, value); |
| } |
| |
| auto key_set = base::flat_set<std::string>(keys); |
| |
| // Add all entries created from domain set, remove all entries not in the set. |
| proto_db_->UpdateEntriesWithRemoveFilter( |
| std::move(entries), |
| base::BindRepeating(&DoesNotContainFilter, std::move(key_set)), |
| base::BindOnce(&UsageStatsDatabase::OnUpdateEntries, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void UsageStatsDatabase::GetAllTokenMappings(TokenMappingsCallback callback) { |
| // Load all UsageStats with the token mapping prefix. |
| proto_db_->LoadEntriesWithFilter( |
| leveldb_proto::KeyFilter(), leveldb::ReadOptions(), kTokenMappingPrefix, |
| base::BindOnce(&UsageStatsDatabase::OnLoadEntriesForGetAllTokenMappings, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void UsageStatsDatabase::SetTokenMappings(TokenMap mappings, |
| StatusCallback callback) { |
| std::vector<std::string> keys; |
| keys.reserve(mappings.size()); |
| |
| auto entries = std::make_unique< |
| leveldb_proto::ProtoDatabase<UsageStat>::KeyEntryVector>(); |
| |
| for (const auto& mapping : mappings) { |
| // Prepend prefix to token to form key. |
| std::string key = |
| base::StrCat({kTokenMappingPrefix, kKeySeparator, mapping.first}); |
| |
| keys.emplace_back(key); |
| |
| UsageStat value; |
| TokenMapping* token_mapping = value.mutable_token_mapping(); |
| token_mapping->set_token(mapping.first); |
| token_mapping->set_fqdn(mapping.second); |
| |
| entries->emplace_back(key, value); |
| } |
| |
| auto key_set = base::flat_set<std::string>(keys); |
| |
| // Add all entries created from map, remove all entries not in the map. |
| proto_db_->UpdateEntriesWithRemoveFilter( |
| std::move(entries), |
| base::BindRepeating(&DoesNotContainFilter, std::move(key_set)), |
| base::BindOnce(&UsageStatsDatabase::OnUpdateEntries, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void UsageStatsDatabase::OnUpdateEntries(StatusCallback callback, |
| bool isSuccess) { |
| std::move(callback).Run(ToError(isSuccess)); |
| } |
| |
| void UsageStatsDatabase::OnLoadEntriesForGetAllEvents( |
| EventsCallback callback, |
| bool isSuccess, |
| std::unique_ptr<std::vector<UsageStat>> stats) { |
| std::vector<WebsiteEvent> results; |
| |
| if (stats) { |
| results.reserve(stats->size()); |
| for (UsageStat stat : *stats) { |
| results.emplace_back(stat.website_event()); |
| } |
| } |
| |
| std::move(callback).Run(ToError(isSuccess), std::move(results)); |
| } |
| |
| void UsageStatsDatabase::OnLoadEntriesForQueryEventsInRange( |
| EventsCallback callback, |
| bool isSuccess, |
| std::unique_ptr<UsageStatMap> stat_map) { |
| std::vector<WebsiteEvent> results; |
| |
| if (stat_map) { |
| results.reserve(stat_map->size()); |
| for (const auto& key_stat : *stat_map) { |
| results.emplace_back(key_stat.second.website_event()); |
| } |
| } |
| |
| std::move(callback).Run(ToError(isSuccess), std::move(results)); |
| } |
| |
| void UsageStatsDatabase::OnLoadEntriesForDeleteEventsInRange( |
| StatusCallback callback, |
| bool isSuccess, |
| std::unique_ptr<UsageStatMap> stat_map) { |
| if (isSuccess && stat_map) { |
| // Collect keys found in range to be deleted. |
| auto keys_to_delete = std::make_unique<std::vector<std::string>>(); |
| keys_to_delete->reserve(stat_map->size()); |
| |
| for (const auto& key_stat : *stat_map) { |
| keys_to_delete->emplace_back(key_stat.first); |
| } |
| |
| // Remove all entries found in range. |
| proto_db_->UpdateEntries( |
| std::make_unique< |
| leveldb_proto::ProtoDatabase<UsageStat>::KeyEntryVector>(), |
| std::move(keys_to_delete), |
| base::BindOnce(&UsageStatsDatabase::OnUpdateEntries, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } else { |
| std::move(callback).Run(ToError(isSuccess)); |
| } |
| } |
| |
| void UsageStatsDatabase::OnLoadEntriesForGetAllSuspensions( |
| SuspensionsCallback callback, |
| bool isSuccess, |
| std::unique_ptr<std::vector<UsageStat>> stats) { |
| std::vector<std::string> results; |
| |
| if (stats) { |
| results.reserve(stats->size()); |
| for (UsageStat stat : *stats) { |
| results.emplace_back(stat.suspension().fqdn()); |
| } |
| } |
| |
| std::move(callback).Run(ToError(isSuccess), std::move(results)); |
| } |
| |
| void UsageStatsDatabase::OnLoadEntriesForGetAllTokenMappings( |
| TokenMappingsCallback callback, |
| bool isSuccess, |
| std::unique_ptr<std::vector<UsageStat>> stats) { |
| TokenMap results; |
| |
| if (stats) { |
| for (UsageStat stat : *stats) { |
| TokenMapping mapping = stat.token_mapping(); |
| results.emplace(mapping.token(), mapping.fqdn()); |
| } |
| } |
| |
| std::move(callback).Run(ToError(isSuccess), std::move(results)); |
| } |
| |
| } // namespace usage_stats |