| // 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 |