| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/first_party_sets/database/first_party_sets_database.h" |
| |
| #include <inttypes.h> |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/sequence_checker.h" |
| #include "base/version.h" |
| #include "content/browser/first_party_sets/first_party_set_parser.h" |
| #include "net/base/schemeful_site.h" |
| #include "net/first_party_sets/first_party_set_entry.h" |
| #include "net/first_party_sets/first_party_sets_cache_filter.h" |
| #include "net/first_party_sets/first_party_sets_context_config.h" |
| #include "net/first_party_sets/global_first_party_sets.h" |
| #include "sql/database.h" |
| #include "sql/meta_table.h" |
| #include "sql/recovery.h" |
| #include "sql/statement.h" |
| #include "sql/transaction.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // Version number of the database. |
| const int kCurrentVersionNumber = 3; |
| |
| // Earliest version which can use a |kCurrentVersionNumber| database |
| // without failing. |
| const int kCompatibleVersionNumber = 2; |
| |
| // Latest version of the database that cannot be upgraded to |
| // |kCurrentVersionNumber| without razing the database. |
| const int kDeprecatedVersionNumber = 1; |
| |
| const char kRunCountKey[] = "run_count"; |
| |
| [[nodiscard]] bool InitSchema(sql::Database& db) { |
| static constexpr char kPublicSetsSql[] = |
| "CREATE TABLE IF NOT EXISTS public_sets(" |
| "version TEXT NOT NULL," |
| "site TEXT NOT NULL," |
| "primary_site TEXT NOT NULL," |
| "site_type INTEGER NOT NULL," |
| "PRIMARY KEY(version,site)" |
| ")WITHOUT ROWID"; |
| if (!db.Execute(kPublicSetsSql)) |
| return false; |
| |
| static constexpr char kBrowserContextSetsVersionSql[] = |
| "CREATE TABLE IF NOT EXISTS browser_context_sets_version(" |
| "browser_context_id TEXT PRIMARY KEY NOT NULL," |
| "public_sets_version TEXT NOT NULL" |
| ")WITHOUT ROWID"; |
| if (!db.Execute(kBrowserContextSetsVersionSql)) |
| return false; |
| |
| static constexpr char kPublicSetsVersionBrowserContextsSql[] = |
| "CREATE INDEX IF NOT EXISTS idx_public_sets_version_browser_contexts " |
| "ON browser_context_sets_version(public_sets_version)"; |
| if (!db.Execute(kPublicSetsVersionBrowserContextsSql)) |
| return false; |
| |
| static constexpr char kBrowserContextSitesToClearSql[] = |
| "CREATE TABLE IF NOT EXISTS browser_context_sites_to_clear(" |
| "browser_context_id TEXT NOT NULL," |
| "site TEXT NOT NULL," |
| "marked_at_run INTEGER NOT NULL," |
| "PRIMARY KEY(browser_context_id,site)" |
| ")WITHOUT ROWID"; |
| if (!db.Execute(kBrowserContextSitesToClearSql)) |
| return false; |
| |
| static constexpr char kMarkedAtRunSitesSql[] = |
| "CREATE INDEX IF NOT EXISTS idx_marked_at_run_sites " |
| "ON browser_context_sites_to_clear(marked_at_run)"; |
| if (!db.Execute(kMarkedAtRunSitesSql)) |
| return false; |
| |
| static constexpr char kBrowserContextsClearedSql[] = |
| "CREATE TABLE IF NOT EXISTS browser_contexts_cleared(" |
| "browser_context_id TEXT PRIMARY KEY NOT NULL," |
| "cleared_at_run INTEGER NOT NULL" |
| ")WITHOUT ROWID"; |
| if (!db.Execute(kBrowserContextsClearedSql)) |
| return false; |
| |
| static constexpr char kClearedAtRunBrowserContextsSql[] = |
| "CREATE INDEX IF NOT EXISTS idx_cleared_at_run_browser_contexts " |
| "ON browser_contexts_cleared(cleared_at_run)"; |
| if (!db.Execute(kClearedAtRunBrowserContextsSql)) |
| return false; |
| |
| static constexpr char kPolicyConfigurationsSql[] = |
| "CREATE TABLE IF NOT EXISTS policy_configurations(" |
| "browser_context_id TEXT NOT NULL," |
| "site TEXT NOT NULL," |
| "primary_site TEXT," // May be NULL if this row represents a deletion. |
| "PRIMARY KEY(browser_context_id,site)" |
| ")WITHOUT ROWID"; |
| if (!db.Execute(kPolicyConfigurationsSql)) |
| return false; |
| |
| static constexpr char kManualSetsSql[] = |
| "CREATE TABLE IF NOT EXISTS manual_sets(" |
| "browser_context_id TEXT NOT NULL," |
| "site TEXT NOT NULL," |
| "primary_site TEXT NOT NULL," |
| "site_type INTEGER NOT NULL," |
| "PRIMARY KEY(browser_context_id,site)" |
| ")WITHOUT ROWID"; |
| if (!db.Execute(kManualSetsSql)) |
| return false; |
| |
| return true; |
| } |
| |
| void RecordInitializationStatus(FirstPartySetsDatabase::InitStatus status) { |
| base::UmaHistogramEnumeration("FirstPartySets.Database.InitStatus", status); |
| } |
| |
| } // namespace |
| |
| FirstPartySetsDatabase::FirstPartySetsDatabase(base::FilePath db_path) |
| : db_path_(std::move(db_path)) { |
| DCHECK(db_path_.IsAbsolute()); |
| } |
| |
| FirstPartySetsDatabase::~FirstPartySetsDatabase() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| bool FirstPartySetsDatabase::PersistSets( |
| const std::string& browser_context_id, |
| const net::GlobalFirstPartySets& sets, |
| const net::FirstPartySetsContextConfig& config) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!LazyInit()) |
| return false; |
| |
| sql::Transaction transaction(db_.get()); |
| if (!transaction.Begin()) |
| return false; |
| |
| // Only persist public sets if the version is valid. |
| if (sets.public_sets_version().IsValid() && |
| !SetPublicSets(browser_context_id, sets)) { |
| return false; |
| } |
| |
| if (!InsertManualSets(browser_context_id, sets.manual_sets())) |
| return false; |
| |
| if (!InsertPolicyConfigurations(browser_context_id, config)) |
| return false; |
| |
| return transaction.Commit(); |
| } |
| |
| bool FirstPartySetsDatabase::SetPublicSets( |
| const std::string& browser_context_id, |
| const net::GlobalFirstPartySets& sets) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK_EQ(db_status_, InitStatus::kSuccess); |
| DCHECK(sets.public_sets_version().IsValid()); |
| |
| const std::string& version = sets.public_sets_version().GetString(); |
| // Checks if the version of the current public sets is referenced by *any* |
| // browser context in the public_sets_version table. If so, that means the |
| // sets already exist in public_sets table and we don't need to write them to |
| // public_sets table again. |
| static constexpr char kCheckSql[] = |
| "SELECT 1 FROM browser_context_sets_version WHERE public_sets_version=?" |
| "LIMIT 1"; |
| sql::Statement check_statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kCheckSql)); |
| check_statement.BindString(0, version); |
| const bool has_matching_version = check_statement.Step(); |
| if (!check_statement.Succeeded()) |
| return false; |
| |
| if (!has_matching_version) { |
| if (!sets.ForEachPublicSetEntry( |
| [&](const net::SchemefulSite& site, |
| const net::FirstPartySetEntry& entry) -> bool { |
| DCHECK(!site.opaque()); |
| DCHECK(!entry.primary().opaque()); |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| static constexpr char kInsertSql[] = |
| "INSERT INTO public_sets(version,site,primary_site,site_type)" |
| "VALUES(?,?,?,?)"; |
| sql::Statement insert_statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kInsertSql)); |
| insert_statement.BindString(0, version); |
| insert_statement.BindString(1, site.Serialize()); |
| insert_statement.BindString(2, entry.primary().Serialize()); |
| insert_statement.BindInt(3, static_cast<int>(entry.site_type())); |
| |
| return insert_statement.Run(); |
| })) { |
| return false; |
| } |
| } |
| |
| // Keeps track of the version used by the given `browser_context_id` in |
| // browser_context_sets_version table. |
| static constexpr char kInsertSql[] = |
| "INSERT OR REPLACE INTO browser_context_sets_version" |
| "(browser_context_id,public_sets_version)VALUES(?,?)"; |
| sql::Statement insert_statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kInsertSql)); |
| insert_statement.BindString(0, browser_context_id); |
| insert_statement.BindString(1, version); |
| |
| if (!insert_statement.Run()) |
| return false; |
| |
| // TODO(shuuran): Garbage collect the public sets no longer used by any |
| // browser_context_id. |
| |
| return true; |
| } |
| |
| bool FirstPartySetsDatabase::InsertSitesToClear( |
| const std::string& browser_context_id, |
| const base::flat_set<net::SchemefulSite>& sites) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!LazyInit()) |
| return false; |
| |
| sql::Transaction transaction(db_.get()); |
| if (!transaction.Begin()) |
| return false; |
| |
| for (const auto& site : sites) { |
| DCHECK(!site.opaque()); |
| static constexpr char kInsertSql[] = |
| // clang-format off |
| "INSERT OR REPLACE INTO browser_context_sites_to_clear" |
| "(browser_context_id,site,marked_at_run)" |
| "VALUES(?,?,?)"; |
| // clang-format on |
| sql::Statement statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kInsertSql)); |
| statement.BindString(0, browser_context_id); |
| statement.BindString(1, site.Serialize()); |
| statement.BindInt64(2, run_count_); |
| |
| if (!statement.Run()) |
| return false; |
| } |
| return transaction.Commit(); |
| } |
| |
| bool FirstPartySetsDatabase::InsertBrowserContextCleared( |
| const std::string& browser_context_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!browser_context_id.empty()); |
| |
| if (!LazyInit()) |
| return false; |
| |
| static constexpr char kInsertBrowserContextsClearedSql[] = |
| // clang-format off |
| "INSERT OR REPLACE INTO browser_contexts_cleared(browser_context_id,cleared_at_run)" |
| "VALUES(?,?)"; |
| // clang-format on |
| sql::Statement statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kInsertBrowserContextsClearedSql)); |
| statement.BindString(0, browser_context_id); |
| statement.BindInt64(1, run_count_); |
| |
| return statement.Run(); |
| } |
| |
| bool FirstPartySetsDatabase::InsertPolicyConfigurations( |
| const std::string& browser_context_id, |
| const net::FirstPartySetsContextConfig& config) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK_EQ(db_status_, InitStatus::kSuccess); |
| |
| static constexpr char kDeleteSql[] = |
| "DELETE FROM policy_configurations WHERE browser_context_id=?"; |
| sql::Statement delete_statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kDeleteSql)); |
| delete_statement.BindString(0, browser_context_id); |
| if (!delete_statement.Run()) |
| return false; |
| |
| return config.ForEachCustomizationEntry( |
| [&](const net::SchemefulSite& site, |
| const absl::optional<net::FirstPartySetEntry>& entry) -> bool { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!site.opaque()); |
| static constexpr char kInsertSql[] = |
| "INSERT INTO " |
| "policy_configurations(browser_context_id,site,primary_site)" |
| "VALUES(?,?,?)"; |
| sql::Statement insert_statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kInsertSql)); |
| insert_statement.BindString(0, browser_context_id); |
| insert_statement.BindString(1, site.Serialize()); |
| if (entry.has_value()) { |
| insert_statement.BindString(2, entry.value().primary().Serialize()); |
| } else { |
| insert_statement.BindNull(2); |
| } |
| return insert_statement.Run(); |
| }); |
| } |
| |
| bool FirstPartySetsDatabase::InsertManualSets( |
| const std::string& browser_context_id, |
| const base::flat_map<net::SchemefulSite, net::FirstPartySetEntry>& |
| manual_sets) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK_EQ(db_status_, InitStatus::kSuccess); |
| |
| static constexpr char kDeleteSql[] = |
| "DELETE FROM manual_sets WHERE browser_context_id=?"; |
| sql::Statement delete_statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kDeleteSql)); |
| delete_statement.BindString(0, browser_context_id); |
| if (!delete_statement.Run()) |
| return false; |
| |
| for (const auto& [site, entry] : manual_sets) { |
| DCHECK(!site.opaque()); |
| static constexpr char kInsertSql[] = |
| "INSERT INTO " |
| "manual_sets(browser_context_id,site,primary_site,site_type)" |
| "VALUES(?,?,?,?)"; |
| sql::Statement insert_statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kInsertSql)); |
| insert_statement.BindString(0, browser_context_id); |
| insert_statement.BindString(1, site.Serialize()); |
| insert_statement.BindString(2, entry.primary().Serialize()); |
| insert_statement.BindInt(3, static_cast<int>(entry.site_type())); |
| |
| if (!insert_statement.Run()) |
| return false; |
| } |
| return true; |
| } |
| |
| net::GlobalFirstPartySets FirstPartySetsDatabase::GetGlobalSets( |
| const std::string& browser_context_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!LazyInit()) |
| return {}; |
| |
| // Query public sets entries. |
| std::vector<std::pair<net::SchemefulSite, net::FirstPartySetEntry>> entries; |
| static constexpr char kVersionSql[] = |
| "SELECT public_sets_version FROM browser_context_sets_version " |
| "WHERE browser_context_id=?"; |
| sql::Statement version_statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kVersionSql)); |
| version_statement.BindString(0, browser_context_id); |
| |
| std::string version; |
| if (version_statement.Step()) { |
| version = version_statement.ColumnString(0); |
| |
| static constexpr char kSelectSql[] = |
| "SELECT site,primary_site,site_type FROM public_sets WHERE version=?"; |
| sql::Statement statement( |
| db_->GetCachedStatement(SQL_FROM_HERE, kSelectSql)); |
| statement.BindString(0, version); |
| |
| while (statement.Step()) { |
| absl::optional<net::SchemefulSite> site = |
| FirstPartySetParser::CanonicalizeRegisteredDomain( |
| statement.ColumnString(0), /*emit_errors=*/false); |
| |
| absl::optional<net::SchemefulSite> primary = |
| FirstPartySetParser::CanonicalizeRegisteredDomain( |
| statement.ColumnString(1), /*emit_errors=*/false); |
| |
| absl::optional<net::SiteType> site_type = |
| net::FirstPartySetEntry::DeserializeSiteType(statement.ColumnInt(2)); |
| |
| // TODO(crbug.com/1314039): Invalid entries should be rare case but |
| // possible. Consider deleting them from DB. |
| if (site.has_value() && primary.has_value() && site_type.has_value()) { |
| entries.emplace_back( |
| std::move(site.value()), |
| net::FirstPartySetEntry(primary.value(), site_type.value(), |
| /*site_index=*/absl::nullopt)); |
| } |
| } |
| if (!statement.Succeeded()) |
| return {}; |
| } |
| if (!version_statement.Succeeded()) |
| return {}; |
| |
| // Aliases are merged with entries inside of the public sets table so it is |
| // sufficient to declare the global sets object with only the entries field. |
| net::GlobalFirstPartySets global_sets(base::Version(version), entries, |
| /*aliases=*/{}); |
| |
| // Query & apply manual set. |
| global_sets.ApplyManuallySpecifiedSet(FetchManualSets(browser_context_id)); |
| |
| return global_sets; |
| } |
| |
| std::pair<std::vector<net::SchemefulSite>, net::FirstPartySetsCacheFilter> |
| FirstPartySetsDatabase::GetSitesToClearFilters( |
| const std::string& browser_context_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!browser_context_id.empty()); |
| if (!LazyInit()) |
| return {}; |
| |
| DCHECK_GT(run_count_, 0); |
| |
| sql::Transaction transaction(db_.get()); |
| if (!transaction.Begin()) |
| return {}; |
| |
| std::vector<net::SchemefulSite> sites_to_clear = |
| FetchSitesToClear(browser_context_id); |
| |
| base::flat_map<net::SchemefulSite, int64_t> all_sites_to_clear = |
| FetchAllSitesToClearFilter(browser_context_id); |
| |
| net::FirstPartySetsCacheFilter cache_filter = |
| all_sites_to_clear.empty() |
| ? net::FirstPartySetsCacheFilter() |
| : net::FirstPartySetsCacheFilter(std::move(all_sites_to_clear), |
| run_count_); |
| |
| if (!transaction.Commit()) |
| return {}; |
| |
| return std::make_pair(std::move(sites_to_clear), std::move(cache_filter)); |
| } |
| |
| std::vector<net::SchemefulSite> FirstPartySetsDatabase::FetchSitesToClear( |
| const std::string& browser_context_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!browser_context_id.empty()); |
| DCHECK_EQ(db_status_, InitStatus::kSuccess); |
| |
| // Gets the sites that were marked to clear but haven't been cleared yet for |
| // the given `browser_context_id`. Use 0 as the default |
| // `browser_contexts_cleared.cleared_at_run` value if the `browser_context_id` |
| // does not exist in the browser_contexts_cleared table. |
| std::vector<net::SchemefulSite> results; |
| static constexpr char kSelectSql[] = |
| // clang-format off |
| "SELECT p.site FROM browser_context_sites_to_clear p " |
| "LEFT JOIN browser_contexts_cleared c ON p.browser_context_id=c.browser_context_id " |
| "WHERE p.marked_at_run>COALESCE(c.cleared_at_run,0)" |
| "AND p.browser_context_id=?"; |
| // clang-format on |
| |
| sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSelectSql)); |
| statement.BindString(0, browser_context_id); |
| |
| while (statement.Step()) { |
| absl::optional<net::SchemefulSite> site = |
| FirstPartySetParser::CanonicalizeRegisteredDomain( |
| statement.ColumnString(0), /*emit_errors=*/false); |
| // TODO(crbug/1314039): Invalid sites should be rare case but possible. |
| // Consider deleting them from DB. |
| if (site.has_value()) { |
| results.push_back(std::move(site.value())); |
| } |
| } |
| |
| if (!statement.Succeeded()) |
| return {}; |
| |
| return results; |
| } |
| |
| base::flat_map<net::SchemefulSite, int64_t> |
| FirstPartySetsDatabase::FetchAllSitesToClearFilter( |
| const std::string& browser_context_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK_EQ(db_status_, InitStatus::kSuccess); |
| |
| std::vector<std::pair<net::SchemefulSite, int64_t>> results; |
| static constexpr char kSelectSql[] = |
| // clang-format off |
| "SELECT site,marked_at_run FROM browser_context_sites_to_clear " |
| "WHERE browser_context_id=?"; |
| // clang-format on |
| |
| sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSelectSql)); |
| statement.BindString(0, browser_context_id); |
| |
| while (statement.Step()) { |
| absl::optional<net::SchemefulSite> site = |
| FirstPartySetParser::CanonicalizeRegisteredDomain( |
| statement.ColumnString(0), /*emit_errors=*/false); |
| // TODO(crbug/1314039): Invalid sites should be rare case but possible. |
| // Consider deleting them from DB. |
| if (site.has_value()) { |
| results.emplace_back(std::move(site.value()), statement.ColumnInt(1)); |
| } |
| } |
| |
| if (!statement.Succeeded()) |
| return {}; |
| |
| return results; |
| } |
| |
| net::FirstPartySetsContextConfig |
| FirstPartySetsDatabase::FetchPolicyConfigurations( |
| const std::string& browser_context_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!LazyInit()) |
| return {}; |
| |
| std::vector< |
| std::pair<net::SchemefulSite, absl::optional<net::FirstPartySetEntry>>> |
| results; |
| static constexpr char kSelectSql[] = |
| // clang-format off |
| "SELECT site,primary_site FROM policy_configurations " |
| "WHERE browser_context_id=?"; |
| // clang-format on |
| sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSelectSql)); |
| statement.BindString(0, browser_context_id); |
| |
| while (statement.Step()) { |
| absl::optional<net::SchemefulSite> site = |
| FirstPartySetParser::CanonicalizeRegisteredDomain( |
| statement.ColumnString(0), /*emit_errors=*/false); |
| |
| absl::optional<net::SchemefulSite> maybe_primary_site; |
| if (std::string primary_site = statement.ColumnString(1); |
| !primary_site.empty()) { |
| maybe_primary_site = FirstPartySetParser::CanonicalizeRegisteredDomain( |
| primary_site, /*emit_errors=*/false); |
| } |
| |
| // TODO(crbug/1314039): Invalid sites should be rare case but possible. |
| // Consider deleting them from DB. |
| if (site.has_value()) { |
| results.emplace_back( |
| std::move(site.value()), |
| maybe_primary_site.has_value() |
| ? absl::make_optional(net::FirstPartySetEntry( |
| maybe_primary_site.value(), |
| // TODO(https://crbug.com/1219656): May change to use the |
| // real site_type and site_index in the future, depending on |
| // the design details. Use kAssociated as default site type |
| // and null site index for now. |
| net::SiteType::kAssociated, absl::nullopt)) |
| : absl::nullopt); |
| } |
| } |
| if (!statement.Succeeded()) |
| return {}; |
| |
| return net::FirstPartySetsContextConfig(std::move(results)); |
| } |
| |
| bool FirstPartySetsDatabase::HasEntryInBrowserContextsClearedForTesting( |
| const std::string& browser_context_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!browser_context_id.empty()); |
| |
| if (!LazyInit()) |
| return {}; |
| |
| static constexpr char kSelectSql[] = |
| // clang-format off |
| "SELECT 1 FROM browser_contexts_cleared " |
| "WHERE browser_context_id=? LIMIT 1"; |
| // clang-format on |
| |
| sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSelectSql)); |
| statement.BindString(0, browser_context_id); |
| |
| return statement.Step() && statement.Succeeded(); |
| } |
| |
| base::flat_map<net::SchemefulSite, net::FirstPartySetEntry> |
| FirstPartySetsDatabase::FetchManualSets(const std::string& browser_context_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!LazyInit()) |
| return {}; |
| |
| std::vector<std::pair<net::SchemefulSite, net::FirstPartySetEntry>> results; |
| static constexpr char kSelectSql[] = |
| // clang-format off |
| "SELECT site,primary_site,site_type FROM manual_sets " |
| "WHERE browser_context_id=?"; |
| // clang-format on |
| sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSelectSql)); |
| statement.BindString(0, browser_context_id); |
| |
| while (statement.Step()) { |
| absl::optional<net::SchemefulSite> site = |
| FirstPartySetParser::CanonicalizeRegisteredDomain( |
| statement.ColumnString(0), /*emit_errors=*/false); |
| |
| absl::optional<net::SchemefulSite> primary = |
| FirstPartySetParser::CanonicalizeRegisteredDomain( |
| statement.ColumnString(1), /*emit_errors=*/false); |
| |
| absl::optional<net::SiteType> site_type = |
| net::FirstPartySetEntry::DeserializeSiteType(statement.ColumnInt(2)); |
| |
| // TODO(crbug.com/1314039): Invalid entries should be rare case but |
| // possible. Consider deleting them from DB. |
| if (site.has_value() && primary.has_value() && site_type.has_value()) { |
| results.emplace_back( |
| std::move(site.value()), |
| net::FirstPartySetEntry(primary.value(), site_type.value(), |
| /*site_index=*/absl::nullopt)); |
| } |
| } |
| |
| if (!statement.Succeeded()) |
| return {}; |
| |
| return results; |
| } |
| |
| bool FirstPartySetsDatabase::LazyInit() { |
| // Early return in case of previous failure, to prevent an unbounded |
| // number of re-attempts. |
| if (db_status_ != InitStatus::kUnattempted) |
| return db_status_ == InitStatus::kSuccess; |
| |
| DCHECK_EQ(db_.get(), nullptr); |
| db_ = std::make_unique<sql::Database>(sql::DatabaseOptions{ |
| .exclusive_locking = true, .page_size = 4096, .cache_size = 32}); |
| db_->set_histogram_tag("FirstPartySets"); |
| // base::Unretained is safe here because this FirstPartySetsDatabase owns |
| // the sql::Database instance that stores and uses the callback. So, |
| // `this` is guaranteed to outlive the callback. |
| db_->set_error_callback(base::BindRepeating( |
| &FirstPartySetsDatabase::DatabaseErrorCallback, base::Unretained(this))); |
| db_status_ = InitializeTables(); |
| |
| if (db_status_ != InitStatus::kSuccess) { |
| db_.reset(); |
| meta_table_.Reset(); |
| } else { |
| IncreaseRunCount(); |
| } |
| |
| RecordInitializationStatus(db_status_); |
| return db_status_ == InitStatus::kSuccess; |
| } |
| |
| bool FirstPartySetsDatabase::OpenDatabase() { |
| DCHECK(db_); |
| if (db_->is_open() || db_->Open(db_path_)) { |
| db_->Preload(); |
| return true; |
| } |
| return false; |
| } |
| |
| void FirstPartySetsDatabase::DatabaseErrorCallback(int extended_error, |
| sql::Statement* stmt) { |
| DCHECK(db_); |
| // Attempt to recover a corrupt database. |
| if (sql::Recovery::ShouldRecover(extended_error)) { |
| // Prevent reentrant calls. |
| db_->reset_error_callback(); |
| |
| // After this call, the |db_| handle is poisoned so that future calls will |
| // return errors until the handle is re-opened. |
| sql::Recovery::RecoverDatabaseWithMetaVersion(db_.get(), db_path_); |
| |
| // The DLOG(FATAL) below is intended to draw immediate attention to errors |
| // in newly-written code. Database corruption is generally a result of OS or |
| // hardware issues, not coding errors at the client level, so displaying the |
| // error would probably lead to confusion. The ignored call signals the |
| // test-expectation framework that the error was handled. |
| std::ignore = sql::Database::IsExpectedSqliteError(extended_error); |
| return; |
| } |
| |
| // The default handling is to assert on debug and to ignore on release. |
| if (!sql::Database::IsExpectedSqliteError(extended_error)) |
| DLOG(FATAL) << db_->GetErrorMessage(); |
| |
| // Consider the database closed if we did not attempt to recover so we did not |
| // produce further errors. |
| db_status_ = InitStatus::kError; |
| } |
| |
| FirstPartySetsDatabase::InitStatus FirstPartySetsDatabase::InitializeTables() { |
| if (!OpenDatabase()) |
| return InitStatus::kError; |
| |
| // Database should now be open. |
| DCHECK(db_->is_open()); |
| |
| // Razes the DB if the version is deprecated or too new to get the feature |
| // working. |
| // |
| // TODO(crbug.com/1372445): Re-enable track DB init status kTooNew and kTooOld |
| // after the bug is resolved and migration is implemented. |
| DCHECK_LT(kDeprecatedVersionNumber, kCurrentVersionNumber); |
| sql::MetaTable::RazeIfIncompatible( |
| db_.get(), /*lowest_supported_version=*/kDeprecatedVersionNumber + 1, |
| kCurrentVersionNumber); |
| |
| // db could have been razed due to version being deprecated or too new. |
| bool db_empty = !sql::MetaTable::DoesTableExist(db_.get()); |
| |
| // Scope initialization in a transaction so we can't be partially initialized. |
| sql::Transaction transaction(db_.get()); |
| if (!transaction.Begin()) { |
| LOG(WARNING) << "First-Party Sets database begin initialization failed."; |
| db_->RazeAndClose(); |
| return InitStatus::kError; |
| } |
| |
| if (!meta_table_.Init(db_.get(), kCurrentVersionNumber, |
| kCompatibleVersionNumber)) { |
| return InitStatus::kError; |
| } |
| |
| // Create the tables if db not already exists. |
| if (db_empty) { |
| if (!InitSchema(*db_)) |
| return InitStatus::kError; |
| } else { |
| if (!UpgradeSchema()) |
| return InitStatus::kError; |
| } |
| |
| if (!transaction.Commit()) { |
| LOG(WARNING) << "First-Party Sets database initialization commit failed."; |
| return InitStatus::kError; |
| } |
| |
| return InitStatus::kSuccess; |
| } |
| |
| bool FirstPartySetsDatabase::UpgradeSchema() { |
| if (meta_table_.GetVersionNumber() == 2) { |
| if (!MigrateToVersion3()) |
| return false; |
| } |
| // Add similar if () blocks for new versions here. |
| |
| return true; |
| } |
| |
| bool FirstPartySetsDatabase::MigrateToVersion3() { |
| DCHECK(db_->HasActiveTransactions()); |
| // Rename the policy_modifications table with policy_configurations. |
| static constexpr char kRenamePolicyConfigurationsTableSql[] = |
| "ALTER TABLE policy_modifications RENAME TO policy_configurations"; |
| if (!db_->Execute(kRenamePolicyConfigurationsTableSql)) |
| return false; |
| |
| meta_table_.SetVersionNumber(3); |
| return true; |
| } |
| |
| void FirstPartySetsDatabase::IncreaseRunCount() { |
| DCHECK_EQ(db_status_, InitStatus::kSuccess); |
| // 0 is the default value, `run_count_` should only be set once. |
| DCHECK_EQ(run_count_, 0); |
| |
| int64_t count = 0; |
| // `count` should be positive if the value exists in the meta table. Consider |
| // db data is corrupted and delete db file if that's not the case. |
| if (meta_table_.GetValue(kRunCountKey, &count) && count <= 0) { |
| db_status_ = InitStatus::kCorrupted; |
| // TODO(crbug/1316090): Need to resolve how the restarted `run_count_` could |
| // affect cache clearing. |
| if (!Destroy()) { |
| LOG(ERROR) << "First-Party Sets database destruction failed."; |
| } |
| return; |
| } |
| |
| run_count_ = count + 1; |
| // TODO(crbug/1314039): Figure out how to handle run_count update failure. |
| if (!meta_table_.SetValue(kRunCountKey, run_count_)) { |
| LOG(ERROR) << "First-Party Sets database updating run_count failed."; |
| } |
| } |
| |
| bool FirstPartySetsDatabase::Destroy() { |
| // Reset the value. |
| run_count_ = 0; |
| |
| if (db_ && db_->is_open() && !db_->RazeAndClose()) |
| return false; |
| |
| // The file already doesn't exist. |
| if (db_path_.empty()) |
| return true; |
| |
| return base::DeleteFile(db_path_); |
| } |
| |
| } // namespace content |