blob: b487783301a0559f634be1e405700ee64da1ddd6 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/affiliations/core/browser/affiliation_database.h"
#include <stdint.h>
#include <memory>
#include <set>
#include <vector>
#include "base/containers/flat_set.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/parameter_pack.h"
#include "build/build_config.h"
#include "components/affiliations/core/browser/sql_table_builder.h"
#include "components/affiliations/core/browser/affiliation_utils.h"
#include "sql/database.h"
#include "sql/error_delegate_util.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"
namespace affiliations {
namespace {
// The current version number of the affiliation database schema.
const int kVersion = 7;
// The oldest version of the schema such that a legacy Chrome client using that
// version can still read/write the current database.
const int kCompatibleVersion = 1;
// Struct to hold table builder for "eq_classes", "eq_class_members",
// and "eq_class_groups" tables.
struct SQLTableBuilders {
std::vector<SQLTableBuilder*> AsVector() const {
// It's important to keep builders in this order as tables are migrated one
// by one.
return {eq_classes, eq_class_members, eq_class_groups, psl_extensions};
}
raw_ptr<SQLTableBuilder> eq_classes;
raw_ptr<SQLTableBuilder> eq_class_members;
raw_ptr<SQLTableBuilder> eq_class_groups;
raw_ptr<SQLTableBuilder> psl_extensions;
};
// Seals the version of the given builders. This is method should be always used
// to seal versions of all builder to make sure all builders are at the same
// version.
void SealVersion(SQLTableBuilders builders, unsigned expected_version) {
unsigned eq_classes_version = builders.eq_classes->SealVersion();
DCHECK_EQ(expected_version, eq_classes_version);
unsigned eq_class_members_version = builders.eq_class_members->SealVersion();
DCHECK_EQ(expected_version, eq_class_members_version);
unsigned eq_class_groups_version = builders.eq_class_groups->SealVersion();
DCHECK_EQ(expected_version, eq_class_groups_version);
unsigned eq_psl_extensions_version = builders.psl_extensions->SealVersion();
DCHECK_EQ(expected_version, eq_psl_extensions_version);
}
// Initializes the passed in table builders and defines the structure of the
// tables.
void InitializeTableBuilders(SQLTableBuilders builders) {
// Version 0 and 1 of the affiliation database.
builders.eq_classes->AddPrimaryKeyColumn("id");
builders.eq_classes->AddColumn("last_update_time", "INTEGER");
builders.eq_class_members->AddPrimaryKeyColumn("id");
builders.eq_class_members->AddColumnToUniqueKey("facet_uri",
"LONGVARCHAR NOT NULL");
builders.eq_class_members->AddColumn(
"set_id", "INTEGER NOT NULL REFERENCES eq_classes(id) ON DELETE CASCADE");
// An index on eq_class_members.facet_uri is automatically created due to the
// UNIQUE constraint, however, we must create one on eq_class_members.set_id
// manually (to prevent linear scan when joining).
builders.eq_class_members->AddIndex("index_on_eq_class_members_set_id",
{"set_id"});
SealVersion(builders, /*expected_version=*/0u);
SealVersion(builders, /*expected_version=*/1u);
// Version 2 of the affiliation database.
builders.eq_class_members->AddColumn("facet_display_name", "VARCHAR");
builders.eq_class_members->AddColumn("facet_icon_url", "VARCHAR");
SealVersion(builders, /*expected_version=*/2u);
// Version 3 of the affiliation database.
builders.eq_class_groups->AddPrimaryKeyColumn("id");
builders.eq_class_groups->AddColumn("facet_uri", "LONGVARCHAR NOT NULL");
builders.eq_class_groups->AddColumn(
"set_id", "INTEGER NOT NULL REFERENCES eq_classes(id) ON DELETE CASCADE");
builders.eq_classes->AddColumn("group_display_name", "VARCHAR");
builders.eq_classes->AddColumn("group_icon_url", "VARCHAR");
SealVersion(builders, /*expected_version=*/3u);
// Version 4 of the affiliation database.
builders.eq_class_groups->AddColumn("main_domain", "VARCHAR");
SealVersion(builders, /*expected_version=*/4u);
builders.psl_extensions->AddColumnToUniqueKey("domain", "VARCHAR NOT NULL");
SealVersion(builders, /*expected_version=*/5u);
// Add index on eq_class_groups.facet_uri and eq_class_groups.set_id
// manually (to prevent linear scan when joining).
builders.eq_class_groups->AddIndex("index_on_eq_groups_url_index",
{"facet_uri"});
builders.eq_class_groups->AddIndex("index_on_eq_groups_set_id_index",
{"set_id"});
SealVersion(builders, /*expected_version=*/6u);
builders.eq_class_members->AddColumn("change_password_url", "VARCHAR");
builders.eq_class_groups->AddColumn("change_password_url", "VARCHAR");
SealVersion(builders, /*expected_version=*/7u);
}
// Migrates from a given version or creates table depending if table exists or
// not.
bool EnsureCurrentVersion(sql::Database* db,
unsigned version,
SQLTableBuilder* builder) {
if (db->DoesTableExist(builder->TableName())) {
return builder->MigrateFrom(version, db);
} else {
return builder->CreateTable(db);
}
}
} // namespace
AffiliationDatabase::AffiliationDatabase() = default;
AffiliationDatabase::~AffiliationDatabase() = default;
bool AffiliationDatabase::Init(const base::FilePath& path) {
sql_connection_ =
std::make_unique<sql::Database>(sql::Database::Tag("Affiliation"));
sql_connection_->set_error_callback(base::BindRepeating(
&AffiliationDatabase::SQLErrorCallback, base::Unretained(this)));
if (!sql_connection_->Open(path))
return false;
if (!sql_connection_->Execute("PRAGMA foreign_keys=1")) {
sql_connection_->Poison();
return false;
}
sql::MetaTable metatable;
if (!metatable.Init(sql_connection_.get(), kVersion, kCompatibleVersion)) {
sql_connection_->Poison();
return false;
}
if (metatable.GetCompatibleVersionNumber() > kVersion) {
LOG(WARNING) << "AffiliationDatabase is too new.";
sql_connection_->Poison();
return false;
}
SQLTableBuilder eq_classes_builder("eq_classes");
SQLTableBuilder eq_class_members_builder("eq_class_members");
SQLTableBuilder eq_class_groups_builder("eq_class_groups");
SQLTableBuilder psl_extensions_builder("psl_extensions");
SQLTableBuilders builders = {&eq_classes_builder, &eq_class_members_builder,
&eq_class_groups_builder,
&psl_extensions_builder};
InitializeTableBuilders(builders);
int version = metatable.GetVersionNumber();
for (SQLTableBuilder* builder : builders.AsVector()) {
if (!EnsureCurrentVersion(sql_connection_.get(), version, builder)) {
LOG(WARNING) << "Failed to set up " << builder->TableName() << " table.";
sql_connection_->Poison();
return false;
}
}
if (version < kVersion) {
if (!metatable.SetVersionNumber(kVersion)) {
return false;
}
}
std::optional<int64_t> db_size = base::GetFileSize(path);
if (db_size.has_value()) {
base::UmaHistogramMemoryKB(
"PasswordManager.AffiliationDatabase.DatabaseSize",
db_size.value() / 1024);
}
return true;
}
bool AffiliationDatabase::GetAffiliationsAndBrandingForFacetURI(
const FacetURI& facet_uri,
AffiliatedFacetsWithUpdateTime* result) const {
DCHECK(result);
result->facets.clear();
sql::Statement statement(sql_connection_->GetCachedStatement(
SQL_FROM_HERE,
"SELECT m2.facet_uri, m2.facet_display_name, m2.facet_icon_url, "
"m2.change_password_url,"
" c.last_update_time "
"FROM eq_class_members m1, eq_class_members m2, eq_classes c "
"WHERE m1.facet_uri = ? AND m1.set_id = m2.set_id AND m1.set_id = c.id"));
statement.BindString(0, facet_uri.canonical_spec());
while (statement.Step()) {
result->facets.emplace_back(
FacetURI::FromCanonicalSpec(statement.ColumnString(0)),
FacetBrandingInfo{
statement.ColumnString(1),
GURL(statement.ColumnStringView(2)),
},
GURL(statement.ColumnString(3)));
result->last_update_time = statement.ColumnTime(4);
}
return !result->facets.empty();
}
void AffiliationDatabase::GetAllAffiliationsAndBranding(
std::vector<AffiliatedFacetsWithUpdateTime>* results) const {
DCHECK(results);
results->clear();
sql::Statement statement(sql_connection_->GetCachedStatement(
SQL_FROM_HERE,
"SELECT m.facet_uri, m.facet_display_name, m.facet_icon_url, "
"m.change_password_url,"
" c.last_update_time, c.id "
"FROM eq_class_members m, eq_classes c "
"WHERE m.set_id = c.id "
"ORDER BY c.id"));
int64_t last_eq_class_id = 0;
while (statement.Step()) {
int64_t eq_class_id = statement.ColumnInt64(5);
if (results->empty() || eq_class_id != last_eq_class_id) {
results->push_back(AffiliatedFacetsWithUpdateTime());
last_eq_class_id = eq_class_id;
}
results->back().facets.emplace_back(
FacetURI::FromCanonicalSpec(statement.ColumnString(0)),
FacetBrandingInfo{
statement.ColumnString(1),
GURL(statement.ColumnStringView(2)),
},
GURL(statement.ColumnString(3)));
results->back().last_update_time =
base::Time::FromInternalValue(statement.ColumnInt64(4));
}
}
std::vector<GroupedFacets> AffiliationDatabase::GetAllGroups() const {
std::vector<GroupedFacets> results;
sql::Statement statement(sql_connection_->GetCachedStatement(
SQL_FROM_HERE,
"SELECT g.facet_uri, g.main_domain, g.change_password_url, c.id, "
"c.group_display_name, "
"c.group_icon_url "
"FROM eq_class_groups g, eq_classes c "
"WHERE g.set_id = c.id "
"ORDER BY c.id"));
int64_t last_eq_class_id = 0;
while (statement.Step()) {
int64_t eq_class_id = statement.ColumnInt64(3);
if (results.empty() || eq_class_id != last_eq_class_id) {
GroupedFacets group;
group.branding_info = FacetBrandingInfo{
statement.ColumnString(4),
GURL(statement.ColumnStringView(5)),
};
results.push_back(std::move(group));
last_eq_class_id = eq_class_id;
}
results.back().facets.emplace_back(
FacetURI::FromCanonicalSpec(statement.ColumnString(0)),
FacetBrandingInfo(), GURL(statement.ColumnString(2)),
statement.ColumnString(1));
}
return results;
}
GroupedFacets AffiliationDatabase::GetGroup(const FacetURI& facet_uri) const {
sql::Statement statement(sql_connection_->GetCachedStatement(
SQL_FROM_HERE,
"SELECT m2.facet_uri, m2.main_domain, m2.change_password_url, "
"c.group_display_name, "
"c.group_icon_url, c.id "
"FROM eq_class_groups m1, eq_class_groups m2, eq_classes c "
"WHERE m1.facet_uri = ? AND m1.set_id = m2.set_id AND m1.set_id = c.id "
"ORDER BY c.id"));
statement.BindString(0, facet_uri.potentially_invalid_spec());
GroupedFacets result;
if (!statement.Step()) {
// No such |facet_uri| in the database, return group with requested facet.
result.facets.emplace_back(facet_uri);
return result;
}
int64_t group_id = statement.ColumnInt64(5);
// Add branding info for a group as it's the same for all steps.
result.branding_info.name = statement.ColumnString(3);
result.branding_info.icon_url = GURL(statement.ColumnStringView(4));
result.facets.emplace_back(
FacetURI::FromCanonicalSpec(statement.ColumnString(0)),
FacetBrandingInfo(), GURL(statement.ColumnString(2)),
statement.ColumnString(1));
while (statement.Step()) {
// Return only the first group from the response, as other groups are exact
// duplicates.
if (group_id != statement.ColumnInt64(5)) {
break;
}
result.facets.emplace_back(
FacetURI::FromCanonicalSpec(statement.ColumnString(0)),
FacetBrandingInfo(), GURL(statement.ColumnString(2)),
statement.ColumnString(1));
}
return result;
}
std::vector<std::string> AffiliationDatabase::GetPSLExtensions() const {
std::vector<std::string> result;
sql::Statement statement(sql_connection_->GetCachedStatement(
SQL_FROM_HERE, "SELECT domain FROM psl_extensions"));
while (statement.Step()) {
result.push_back(statement.ColumnString(0));
}
return result;
}
void AffiliationDatabase::DeleteAffiliationsAndBrandingForFacetURI(
const FacetURI& facet_uri) {
sql::Transaction transaction(sql_connection_.get());
if (!transaction.Begin())
return;
sql::Statement statement_lookup(sql_connection_->GetCachedStatement(
SQL_FROM_HERE,
"SELECT m.set_id FROM eq_class_members m "
"WHERE m.facet_uri = ?"));
statement_lookup.BindString(0, facet_uri.canonical_spec());
// No such |facet_uri|, nothing to do.
if (!statement_lookup.Step())
return;
int64_t eq_class_id = statement_lookup.ColumnInt64(0);
// Children will get deleted due to 'ON DELETE CASCADE'.
sql::Statement statement_parent(sql_connection_->GetCachedStatement(
SQL_FROM_HERE, "DELETE FROM eq_classes WHERE eq_classes.id = ?"));
statement_parent.BindInt64(0, eq_class_id);
if (!statement_parent.Run())
return;
transaction.Commit();
}
AffiliationDatabase::StoreAffiliationResult AffiliationDatabase::Store(
const AffiliatedFacetsWithUpdateTime& affiliated_facets,
const GroupedFacets& group) {
DCHECK(!affiliated_facets.facets.empty());
sql::Statement statement_parent(sql_connection_->GetCachedStatement(
SQL_FROM_HERE,
"INSERT INTO eq_classes(last_update_time, group_display_name, "
"group_icon_url) VALUES (?, ?, ?)"));
sql::Statement statement_child(sql_connection_->GetCachedStatement(
SQL_FROM_HERE,
"INSERT INTO "
"eq_class_members(facet_uri, facet_display_name, facet_icon_url, "
"change_password_url, set_id) "
"VALUES (?, ?, ?, ?, ?)"));
sql::Statement statement_groups(sql_connection_->GetCachedStatement(
SQL_FROM_HERE,
"INSERT INTO eq_class_groups(facet_uri, main_domain, "
"change_password_url, set_id) "
"VALUES (?, ?, ?, ?)"));
sql::Transaction transaction(sql_connection_.get());
if (!transaction.Begin()) {
return StoreAffiliationResult::kFailedToStartTransaction;
}
statement_parent.BindTime(0, affiliated_facets.last_update_time);
statement_parent.BindString(1, group.branding_info.name);
statement_parent.BindString(
2, group.branding_info.icon_url.possibly_invalid_spec());
if (!statement_parent.Run()) {
return StoreAffiliationResult::kFailedToAddSet;
}
int64_t eq_class_id = sql_connection_->GetLastInsertRowId();
for (const Facet& facet : affiliated_facets.facets) {
statement_child.Reset(true);
statement_child.BindString(0, facet.uri.canonical_spec());
statement_child.BindString(1, facet.branding_info.name);
statement_child.BindString(
2, facet.branding_info.icon_url.possibly_invalid_spec());
statement_child.BindString(
3, facet.change_password_url.possibly_invalid_spec());
statement_child.BindInt64(4, eq_class_id);
if (!statement_child.Run()) {
return StoreAffiliationResult::kFailedToAddAffiliation;
}
}
for (const Facet& facet : group.facets) {
statement_groups.Reset(true);
statement_groups.BindString(0, facet.uri.canonical_spec());
statement_groups.BindString(1, facet.main_domain);
statement_groups.BindString(
2, facet.change_password_url.possibly_invalid_spec());
statement_groups.BindInt64(3, eq_class_id);
if (!statement_groups.Run()) {
return StoreAffiliationResult::kFailedToAddGroup;
}
}
if (!transaction.Commit()) {
return StoreAffiliationResult::kFailedToCloseTransaction;
}
return StoreAffiliationResult::kSuccess;
}
void AffiliationDatabase::StoreAndRemoveConflicting(
const AffiliatedFacetsWithUpdateTime& affiliation,
const GroupedFacets& group,
std::vector<AffiliatedFacetsWithUpdateTime>* removed_affiliations) {
DCHECK(!affiliation.facets.empty());
DCHECK(removed_affiliations);
removed_affiliations->clear();
sql::Transaction transaction(sql_connection_.get());
if (!transaction.Begin())
return;
for (const Facet& facet : affiliation.facets) {
AffiliatedFacetsWithUpdateTime old_affiliation;
if (GetAffiliationsAndBrandingForFacetURI(facet.uri, &old_affiliation)) {
if (!AreEquivalenceClassesEqual(old_affiliation.facets,
affiliation.facets)) {
removed_affiliations->push_back(old_affiliation);
}
DeleteAffiliationsAndBrandingForFacetURI(facet.uri);
}
}
StoreAffiliationResult result = Store(affiliation, group);
UMA_HISTOGRAM_ENUMERATION("PasswordManager.AffiliationDatabase.StoreResult",
result);
transaction.Commit();
}
void AffiliationDatabase::RemoveMissingFacetURI(
std::vector<FacetURI> facet_uris) {
sql::Transaction transaction(sql_connection_.get());
if (!transaction.Begin())
return;
auto current_facets = base::MakeFlatSet<std::string>(
facet_uris, {}, &FacetURI::potentially_invalid_spec);
std::set<int64_t> all_ids, found_ids;
sql::Statement statement(sql_connection_->GetUniqueStatement(
"SELECT m.facet_uri, m.set_id FROM eq_class_members m"));
// For every facet in the database check if it exists in |current_facets|.
while (statement.Step()) {
std::string facet_uri = statement.ColumnString(0);
int64_t eq_class_id = statement.ColumnInt64(1);
all_ids.insert(eq_class_id);
if (current_facets.contains(facet_uri)) {
found_ids.insert(eq_class_id);
}
}
// Remove any equivalence class which aren't represented in |current_facets|.
for (const auto id : all_ids) {
if (found_ids.find(id) == found_ids.end()) {
sql::Statement statement_parent(sql_connection_->GetCachedStatement(
SQL_FROM_HERE, "DELETE FROM eq_classes WHERE eq_classes.id = ?"));
statement_parent.BindInt64(0, id);
statement_parent.Run();
}
}
transaction.Commit();
}
// static
void AffiliationDatabase::Delete(const base::FilePath& path) {
bool success = sql::Database::Delete(path);
DCHECK(success);
}
int AffiliationDatabase::GetDatabaseVersionForTesting() {
sql::MetaTable metatable;
// The second and third parameters to |MetaTable::Init| are ignored, given
// that a metatable already exists. Hence they are not influencing the version
// of the underlying database.
DCHECK(sql::MetaTable::DoesTableExist(sql_connection_.get()));
bool ok = metatable.Init(sql_connection_.get(), 1, 1);
DCHECK(ok);
return metatable.GetVersionNumber();
}
void AffiliationDatabase::UpdatePslExtensions(
const std::vector<std::string>& domains) {
DCHECK(!domains.empty());
sql::Statement clear_table_statement(
sql_connection_->GetUniqueStatement("DELETE FROM psl_extensions"));
sql::Statement statement(sql_connection_->GetUniqueStatement(
"INSERT INTO psl_extensions(domain) VALUES (?)"));
sql::Transaction transaction(sql_connection_.get());
if (!transaction.Begin()) {
return;
}
// Clear all the records first.
if (!clear_table_statement.Run()) {
return;
}
for (const auto& domain : domains) {
statement.Reset(true);
statement.BindString(0, domain);
if (!statement.Run()) {
return;
}
}
transaction.Commit();
}
void AffiliationDatabase::SQLErrorCallback(int error,
sql::Statement* statement) {
sql::UmaHistogramSqliteResult("PasswordManager.AffiliationDatabase.Error",
error);
if (sql::IsErrorCatastrophic(error) && sql_connection_->is_open()) {
// Normally this will poison the database, causing any subsequent operations
// to silently fail without any side effects. However, if RazeAndPoison() is
// called from the error callback in response to an error raised from within
// sql::Database::Open, opening the now-razed database will be retried.
sql_connection_->RazeAndPoison();
}
}
} // namespace affiliations