blob: 66ef097a64b42b4e99ac7864df702a8e7031ad2f [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/web_applications/web_app_database.h"
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "chrome/browser/web_applications/proto/web_app.pb.h"
#include "chrome/browser/web_applications/proto/web_app_launch_handler.pb.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_database_factory.h"
#include "chrome/browser/web_applications/web_app_database_serialization.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_proto_utils.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/chrome_features.h"
#include "components/sync/base/data_type.h"
#include "components/sync/model/data_type_store.h"
#include "components/sync/model/metadata_batch.h"
#include "components/sync/model/metadata_change_list.h"
#include "components/sync/model/model_error.h"
#include "components/sync/protocol/web_app_specifics.pb.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/common/web_app_id.h"
#include "url/gurl.h"
namespace web_app {
WebAppDatabase::WebAppDatabase(AbstractWebAppDatabaseFactory* database_factory,
ReportErrorCallback error_callback)
: database_factory_(database_factory),
error_callback_(std::move(error_callback)) {
DCHECK(database_factory_);
}
WebAppDatabase::~WebAppDatabase() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void WebAppDatabase::OpenDatabase(RegistryOpenedCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!store_);
syncer::OnceDataTypeStoreFactory store_factory =
database_factory_->GetStoreFactory();
std::move(store_factory)
.Run(syncer::WEB_APPS,
base::BindOnce(&WebAppDatabase::OnDatabaseOpened,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebAppDatabase::Write(
const RegistryUpdateData& update_data,
std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
CompletionCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(opened_);
std::unique_ptr<syncer::DataTypeStore::WriteBatch> write_batch =
store_->CreateWriteBatch();
// |update_data| can be empty here but we should write |metadata_change_list|
// anyway.
write_batch->TakeMetadataChangesFrom(std::move(metadata_change_list));
for (const std::unique_ptr<WebApp>& web_app : update_data.apps_to_create) {
auto proto = WebAppToProto(*web_app);
write_batch->WriteData(web_app->app_id(), proto->SerializeAsString());
}
for (const std::unique_ptr<WebApp>& web_app : update_data.apps_to_update) {
auto proto = WebAppToProto(*web_app);
write_batch->WriteData(web_app->app_id(), proto->SerializeAsString());
}
for (const webapps::AppId& app_id : update_data.apps_to_delete) {
write_batch->DeleteData(app_id);
}
store_->CommitWriteBatch(
std::move(write_batch),
base::BindOnce(&WebAppDatabase::OnDataWritten,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
// static
int WebAppDatabase::GetCurrentDatabaseVersion() {
return 3;
}
WebAppDatabase::ProtobufState::ProtobufState() = default;
WebAppDatabase::ProtobufState::~ProtobufState() = default;
WebAppDatabase::ProtobufState::ProtobufState(ProtobufState&&) = default;
WebAppDatabase::ProtobufState& WebAppDatabase::ProtobufState::operator=(
ProtobufState&&) = default;
WebAppDatabase::ProtobufState WebAppDatabase::ParseProtobufs(
const syncer::DataTypeStore::RecordList& data_records) const {
ProtobufState state;
for (const syncer::DataTypeStore::Record& record : data_records) {
if (record.id == kDatabaseMetadataKey) {
bool success = state.metadata.ParseFromString(record.value);
if (!success) {
DLOG(ERROR)
<< "WebApps LevelDB parse error: can't parse metadata proto.";
// TODO: Consider logging a histogram
}
continue;
}
proto::WebApp app_proto;
bool success = app_proto.ParseFromString(record.value);
if (!success) {
DLOG(ERROR) << "WebApps LevelDB parse error: can't parse app proto.";
// TODO: Consider logging a histogram
}
state.apps.emplace(record.id, std::move(app_proto));
}
return state;
}
void WebAppDatabase::MigrateDatabase(ProtobufState& state) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Migration should happen when we have gotten a `store_`, but haven't
// finished opening the database yet.
CHECK(store_);
CHECK(!opened_);
bool did_change_metadata = false;
std::set<webapps::AppId> changed_apps;
// Upgrade from version 0 to version 1. This migrates the kSync source to
// a combination of kSync and kUserInstalled.
if (state.metadata.version() < 1 && GetCurrentDatabaseVersion() >= 1) {
MigrateInstallSourceAddUserInstalled(state, changed_apps);
base::UmaHistogramSparse("WebApp.Database.VersionUpgradedTo", 1);
state.metadata.set_version(1);
did_change_metadata = true;
}
// Upgrade from version 1 to version 2.
if (state.metadata.version() < 2 && GetCurrentDatabaseVersion() >= 2) {
MigrateShortcutAppsToDiyApps(state, changed_apps);
MigrateDefaultDisplayModeToPlatformDisplayMode(state, changed_apps);
MigratePartiallyInstalledAppsToCorrectState(state, changed_apps);
base::UmaHistogramSparse("WebApp.Database.VersionUpgradedTo", 2);
state.metadata.set_version(2);
did_change_metadata = true;
}
// Upgrade from version 2 to version 3.
if (state.metadata.version() < 3 && GetCurrentDatabaseVersion() >= 3) {
MigrateDeprecatedLaunchHandlerToClientMode(state, changed_apps);
MigrateScopeToRemoveRefAndQuery(state, changed_apps);
MigrateToRelativeManifestIdNoFragment(state, changed_apps);
base::UmaHistogramSparse("WebApp.Database.VersionUpgradedTo", 3);
state.metadata.set_version(3);
did_change_metadata = true;
}
CHECK_EQ(state.metadata.version(), GetCurrentDatabaseVersion());
if (did_change_metadata || !changed_apps.empty()) {
std::unique_ptr<syncer::DataTypeStore::WriteBatch> write_batch =
store_->CreateWriteBatch();
if (did_change_metadata) {
write_batch->WriteData(std::string(kDatabaseMetadataKey),
state.metadata.SerializeAsString());
}
for (const auto& app_id : changed_apps) {
CHECK(state.apps.contains(app_id));
write_batch->WriteData(app_id, state.apps[app_id].SerializeAsString());
}
store_->CommitWriteBatch(
std::move(write_batch),
base::BindOnce(&WebAppDatabase::OnDataWritten,
weak_ptr_factory_.GetWeakPtr(), base::DoNothing()));
}
}
void WebAppDatabase::MigrateInstallSourceAddUserInstalled(
ProtobufState& state,
std::set<webapps::AppId>& changed_apps) {
// Migrating from version 0 to version 1.
CHECK_LT(state.metadata.version(), 1);
const bool is_syncing_apps = database_factory_->IsSyncingApps();
int apps_migrated_count = 0;
for (auto& [app_id, app_proto] : state.apps) {
if (!app_proto.sources().sync()) {
continue;
}
bool changed = false;
if (!app_proto.sources().user_installed()) {
app_proto.mutable_sources()->set_user_installed(true);
changed = true;
}
if (!is_syncing_apps) {
app_proto.mutable_sources()->set_sync(false);
changed = true;
}
if (changed) {
changed_apps.insert(app_id);
apps_migrated_count++;
}
}
base::UmaHistogramCounts1000(
"WebApp.Migrations.InstallSourceAddUserInstalled", apps_migrated_count);
}
void WebAppDatabase::MigrateShortcutAppsToDiyApps(
WebAppDatabase::ProtobufState& state,
std::set<webapps::AppId>& changed_apps) {
// Migrating from version 1 to version 2.
CHECK_LT(state.metadata.version(), 2);
int shortcut_to_diy_apps = 0;
for (auto& [app_id, app_proto] : state.apps) {
bool is_shortcut =
!app_proto.has_scope() || app_proto.scope().empty() ||
(app_proto.has_latest_install_source() &&
app_proto.latest_install_source() ==
static_cast<uint32_t>(
webapps::WebappInstallSource::MENU_CREATE_SHORTCUT));
if (!is_shortcut) {
continue;
}
changed_apps.insert(app_id);
app_proto.set_is_diy_app(true);
app_proto.set_was_shortcut_app(true);
shortcut_to_diy_apps++;
if (app_proto.has_scope() && !app_proto.scope().empty() &&
GURL(app_proto.scope()).is_valid()) {
continue;
}
// Populate the scope if it was empty or invalid.
if (!app_proto.has_sync_data() || !app_proto.sync_data().has_start_url()) {
DLOG(ERROR) << "Missing sync data or start_url for shortcut app "
<< app_id;
continue;
}
GURL start_url(app_proto.sync_data().start_url());
if (!start_url.is_valid()) {
// Cannot recover scope, mark for potential cleanup later if needed.
DLOG(ERROR) << "Invalid start_url for shortcut app " << app_id << ":"
<< start_url.possibly_invalid_spec();
continue;
}
app_proto.set_scope(start_url.GetWithoutFilename().spec());
}
base::UmaHistogramCounts1000("WebApp.Migrations.ShortcutAppsToDiy2",
shortcut_to_diy_apps);
}
void WebAppDatabase::MigrateDefaultDisplayModeToPlatformDisplayMode(
WebAppDatabase::ProtobufState& state,
std::set<webapps::AppId>& changed_apps) {
// Migrating from version 1 to version 2.
CHECK_LT(state.metadata.version(), 2);
int apps_migrated_count = 0;
for (auto& [app_id, app_proto] : state.apps) {
if (!app_proto.has_sync_data()) {
// Cannot migrate without sync data.
continue;
}
sync_pb::WebAppSpecifics* sync_data = app_proto.mutable_sync_data();
if (!HasCurrentPlatformUserDisplayMode(*sync_data)) {
sync_pb::WebAppSpecifics_UserDisplayMode udm =
ResolvePlatformSpecificUserDisplayMode(*sync_data);
SetPlatformSpecificUserDisplayMode(udm, sync_data);
changed_apps.insert(app_id);
apps_migrated_count++;
}
}
base::UmaHistogramCounts1000("WebApp.Migrations.DefaultDisplayModeToPlatform",
apps_migrated_count);
}
// Corrects the install_state for apps that claim OS integration but lack the
// necessary OS integration state data.
void WebAppDatabase::MigratePartiallyInstalledAppsToCorrectState(
WebAppDatabase::ProtobufState& state,
std::set<webapps::AppId>& changed_apps) {
// Migrating from version 1 to version 2.
CHECK_LT(state.metadata.version(), 2);
int install_state_fixed_count = 0;
for (auto& [app_id, app_proto] : state.apps) {
if (app_proto.install_state() !=
proto::InstallState::INSTALLED_WITH_OS_INTEGRATION) {
continue;
}
// Check if any OS integration state exists. A simple check for shortcut
// presence is sufficient as a proxy for any OS integration.
if (app_proto.has_current_os_integration_states() &&
app_proto.current_os_integration_states().has_shortcut()) {
continue;
}
app_proto.set_install_state(
proto::InstallState::INSTALLED_WITHOUT_OS_INTEGRATION);
changed_apps.insert(app_id);
install_state_fixed_count++;
}
base::UmaHistogramCounts1000(
"WebApp.Migrations.PartiallyInstalledAppsToCorrectState",
install_state_fixed_count);
}
void WebAppDatabase::MigrateDeprecatedLaunchHandlerToClientMode(
ProtobufState& state,
std::set<webapps::AppId>& changed_apps) {
// Migrating from version 2 to version 3.
CHECK_LT(state.metadata.version(), 3);
int apps_migrated_count = 0;
for (auto& [app_id, app_proto] : state.apps) {
if (!app_proto.has_launch_handler()) {
continue;
}
bool changed = false;
proto::LaunchHandler* launch_handler = app_proto.mutable_launch_handler();
// If client_mode is unspecified, try migrating from deprecated fields.
if (launch_handler->client_mode() ==
proto::LaunchHandler::CLIENT_MODE_UNSPECIFIED) {
proto::LaunchHandler::ClientMode migrated_client_mode =
proto::LaunchHandler::CLIENT_MODE_UNSPECIFIED;
switch (launch_handler->route_to()) {
case proto::LaunchHandler_DeprecatedRouteTo_UNSPECIFIED_ROUTE:
break;
case proto::LaunchHandler_DeprecatedRouteTo_AUTO_ROUTE:
migrated_client_mode = proto::LaunchHandler::CLIENT_MODE_AUTO;
break;
case proto::LaunchHandler_DeprecatedRouteTo_NEW_CLIENT:
migrated_client_mode = proto::LaunchHandler::CLIENT_MODE_NAVIGATE_NEW;
break;
case proto::LaunchHandler_DeprecatedRouteTo_EXISTING_CLIENT:
if (launch_handler->navigate_existing_client() ==
proto::LaunchHandler_DeprecatedNavigateExistingClient_NEVER) {
migrated_client_mode =
proto::LaunchHandler::CLIENT_MODE_FOCUS_EXISTING;
} else {
migrated_client_mode =
proto::LaunchHandler::CLIENT_MODE_NAVIGATE_EXISTING;
}
break;
case proto::LaunchHandler_DeprecatedRouteTo_EXISTING_CLIENT_NAVIGATE:
migrated_client_mode =
proto::LaunchHandler::CLIENT_MODE_NAVIGATE_EXISTING;
break;
case proto::LaunchHandler_DeprecatedRouteTo_EXISTING_CLIENT_RETAIN:
migrated_client_mode =
proto::LaunchHandler::CLIENT_MODE_FOCUS_EXISTING;
break;
}
launch_handler->set_client_mode(migrated_client_mode);
changed = true;
} else if (launch_handler->client_mode() ==
proto::LaunchHandler::CLIENT_MODE_AUTO) {
// If client_mode is set to auto, and client_mode_valid_and_specified is
// explicitly false, treat client_mode as unspecified.
if (launch_handler->has_client_mode_valid_and_specified() &&
!launch_handler->client_mode_valid_and_specified()) {
launch_handler->set_client_mode(
proto::LaunchHandler::CLIENT_MODE_UNSPECIFIED);
changed = true;
}
}
// Clear deprecated fields if they exist.
if (launch_handler->has_route_to()) {
launch_handler->clear_route_to();
changed = true;
}
if (launch_handler->has_navigate_existing_client()) {
launch_handler->clear_navigate_existing_client();
changed = true;
}
if (launch_handler->has_client_mode_valid_and_specified()) {
launch_handler->clear_client_mode_valid_and_specified();
changed = true;
}
if (changed) {
changed_apps.insert(app_id);
apps_migrated_count++;
}
}
base::UmaHistogramCounts1000(
"WebApp.Migrations.DeprecatedLaunchHandlerToClientMode",
apps_migrated_count);
}
void WebAppDatabase::MigrateScopeToRemoveRefAndQuery(
ProtobufState& state,
std::set<webapps::AppId>& changed_apps) {
// Migrating from version 2 to version 3.
CHECK_LT(state.metadata.version(), 3);
int apps_migrated_count = 0;
for (auto& [app_id, app_proto] : state.apps) {
if (!app_proto.has_scope()) {
continue;
}
GURL scope(app_proto.scope());
if (!scope.is_valid()) {
continue;
}
if (scope.has_query() || scope.has_ref()) {
GURL::Replacements replacements;
replacements.ClearQuery();
replacements.ClearRef();
GURL clean_scope = scope.ReplaceComponents(replacements);
app_proto.set_scope(clean_scope.spec());
changed_apps.insert(app_id);
apps_migrated_count++;
}
}
base::UmaHistogramCounts1000("WebApp.Migrations.ScopeRefQueryRemoved",
apps_migrated_count);
}
void WebAppDatabase::MigrateToRelativeManifestIdNoFragment(
ProtobufState& state,
std::set<webapps::AppId>& changed_apps) {
// Migrating from version 2 to version 3.
CHECK_LT(state.metadata.version(), 3);
int apps_migrated_count = 0;
int fragment_removed_count = 0;
for (auto& [app_id, app_proto] : state.apps) {
if (!app_proto.has_sync_data()) {
continue;
}
sync_pb::WebAppSpecifics* sync_data = app_proto.mutable_sync_data();
if (!sync_data->has_start_url()) {
continue;
}
GURL start_url(sync_data->start_url());
if (!start_url.is_valid()) {
continue;
}
// Calculate the expected manifest_id and relative path without fragment.
webapps::ManifestId expected_manifest_id;
if (sync_data->has_relative_manifest_id()) {
expected_manifest_id =
GenerateManifestId(sync_data->relative_manifest_id(), start_url);
} else {
expected_manifest_id = GenerateManifestIdFromStartUrlOnly(start_url);
}
if (!expected_manifest_id.is_valid()) {
continue;
}
std::string expected_relative_path =
RelativeManifestIdPath(expected_manifest_id);
bool changed = false;
if (!sync_data->has_relative_manifest_id()) {
// Populate if missing.
sync_data->set_relative_manifest_id(expected_relative_path);
changed = true;
} else if (sync_data->relative_manifest_id() != expected_relative_path) {
// Correct if different (e.g., had a fragment).
sync_data->set_relative_manifest_id(expected_relative_path);
changed = true;
fragment_removed_count++;
}
if (changed) {
changed_apps.insert(app_id);
apps_migrated_count++;
}
}
base::UmaHistogramCounts1000(
"WebApp.Migrations.RelativeManifestIdFragmentRemoved",
fragment_removed_count);
base::UmaHistogramCounts1000(
"WebApp.Migrations.RelativeManifestIdPopulatedOrFixed",
apps_migrated_count);
}
void WebAppDatabase::OnDatabaseOpened(
RegistryOpenedCallback callback,
const std::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::DataTypeStore> store) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error) {
error_callback_.Run(*error);
DLOG(ERROR) << "WebApps LevelDB opening error: " << error->ToString();
return;
}
store_ = std::move(store);
store_->ReadAllDataAndMetadata(
base::BindOnce(&WebAppDatabase::OnAllDataAndMetadataRead,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebAppDatabase::OnAllDataAndMetadataRead(
RegistryOpenedCallback callback,
const std::optional<syncer::ModelError>& error,
std::unique_ptr<syncer::DataTypeStore::RecordList> data_records,
std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
TRACE_EVENT0("ui", "WebAppDatabase::OnAllMetadataRead");
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error) {
error_callback_.Run(*error);
DLOG(ERROR) << "WebApps LevelDB read error: " << error->ToString();
return;
}
ProtobufState state = ParseProtobufs(*data_records);
MigrateDatabase(state);
Registry registry;
for (const auto& [app_id, app_proto] : state.apps) {
std::unique_ptr<WebApp> web_app = ParseWebAppProto(app_proto);
base::UmaHistogramBoolean("WebApp.Database.ValidProto", web_app != nullptr);
if (!web_app) {
continue;
}
// Record whether the derived app_id matches the database key.
bool mismatch = (web_app->app_id() != app_id);
base::UmaHistogramBoolean("WebApp.Database.AppIdMatch", !mismatch);
if (mismatch) {
DLOG(ERROR) << "WebApps LevelDB error: app_id doesn't match storage key "
<< app_id << " vs " << web_app->app_id() << ", from "
<< web_app->manifest_id();
continue;
}
registry.emplace(app_id, std::move(web_app));
}
opened_ = true;
// This should be a tail call: a callback code may indirectly call |this|
// methods, like WebAppDatabase::Write()
std::move(callback).Run(std::move(registry), std::move(metadata_batch));
}
void WebAppDatabase::OnDataWritten(
CompletionCallback callback,
const std::optional<syncer::ModelError>& error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error) {
error_callback_.Run(*error);
DLOG(ERROR) << "WebApps LevelDB write error: " << error->ToString();
}
std::move(callback).Run(!error);
}
} // namespace web_app