blob: 05d47c0f656e488a59e3213eaded0a6466062c10 [file] [log] [blame]
// Copyright 2019 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/externally_installed_web_app_prefs.h"
#include <string>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/metrics/histogram_functions.h"
#include "base/values.h"
#include "chrome/browser/web_applications/externally_installed_prefs_migration_metrics.h"
#include "chrome/browser/web_applications/user_uninstalled_preinstalled_web_app_prefs.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
#include "chrome/browser/web_applications/web_app_prefs_utils.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/common/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "url/gurl.h"
namespace web_app {
namespace {
// The stored preferences look like:
//
// "web_apps": {
// "extension_ids": {
// "https://events.google.com/io2016/?utm_source=web_app_manifest": {
// "extension_id": "mjgafbdfajpigcjmkgmeokfbodbcfijl",
// "install_source": 1,
// "is_placeholder": true,
// },
// "https://www.chromestatus.com/features": {
// "extension_id": "fedbieoalmbobgfjapopkghdmhgncnaa",
// "install_source": 1
// }
// }
// }
//
// From the top, prefs::kWebAppsExtensionIDs is "web_apps.extension_ids".
//
// Two levels in is a dictionary (key/value pairs) whose keys are URLs and
// values are leaf dictionaries. Those leaf dictionaries have keys such as
// kExtensionId and kInstallSource.
// The name "extension_id" comes from when PWAs were only backed by the
// Extension system rather than their own. It cannot be changed now that it
// lives persistently in users' profiles.
constexpr char kExtensionId[] = "extension_id";
constexpr char kInstallSource[] = "install_source";
constexpr char kIsPlaceholder[] = "is_placeholder";
constexpr char kAppIdDeleted[] =
"WebApp.ExternalPrefs.CorruptionFixedRemovedAppId";
constexpr char kInstallUrlsDeleted[] =
"WebApp.ExternalPrefs.CorruptionFixedInstallUrlsDeleted";
// Returns the base::Value in `pref_service` corresponding to our stored dict
// for `app_id`, or `nullptr` if it doesn't exist.
const base::Value::Dict* GetPreferenceValue(const PrefService* pref_service,
const AppId& app_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const base::Value::Dict& urls_to_dicts =
pref_service->GetDict(prefs::kWebAppsExtensionIDs);
// Do a simple O(N) scan for app_id being a value in each dictionary's
// key/value pairs. We expect both N and the number of times
// GetPreferenceValue is called to be relatively small in practice. If they
// turn out to be large, we can write a more sophisticated implementation.
for (const auto it : urls_to_dicts) {
if (!it.second.is_dict()) {
continue;
}
const base::Value::Dict& dict = it.second.GetDict();
const std::string* extension_id = dict.FindString(kExtensionId);
if (extension_id && (*extension_id == app_id)) {
return &it.second.GetDict();
}
}
return nullptr;
}
// Correct any corruption that has occurred due to Lacros processes starting in
// inconsistent ways. https://crbug.com/1359205.
void FixMigrationCorruptionFromLacrosSwitch(PrefService* pref_service,
const WebAppRegistrar& registrar) {
UserUninstalledPreinstalledWebAppPrefs preinstalled_prefs(pref_service);
int install_urls_fixed = 0;
int app_ids_removed = 0;
for (const AppId& app_id : registrar.GetAppIds()) {
if (!registrar.IsInstalledByDefaultManagement(app_id)) {
continue;
}
const WebApp* web_app = registrar.GetAppById(app_id);
DCHECK(web_app);
auto default_config_it = web_app->management_to_external_config_map().find(
WebAppManagement::kDefault);
// If there is no external config or the install urls are empty, just treat
// it as a valid install. The preinstall manager should add to the install
// urls when it synchronizes.
if (default_config_it ==
web_app->management_to_external_config_map().end() ||
default_config_it->second.install_urls.empty()) {
if (preinstalled_prefs.RemoveByAppId(app_id))
++app_ids_removed;
continue;
}
// Remove all install urls that are legitimately installed. If they are all
// removed, then the pref is removed entirely.
for (const GURL& install_url : default_config_it->second.install_urls) {
if (preinstalled_prefs.RemoveByInstallUrl(app_id, install_url))
++install_urls_fixed;
}
}
base::UmaHistogramCounts100(kAppIdDeleted, app_ids_removed);
base::UmaHistogramCounts100(kInstallUrlsDeleted, install_urls_fixed);
}
} // namespace
// static
void ExternallyInstalledWebAppPrefs::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(prefs::kWebAppsExtensionIDs);
}
// static
bool ExternallyInstalledWebAppPrefs::HasAppId(const PrefService* pref_service,
const AppId& app_id) {
return GetPreferenceValue(pref_service, app_id) != nullptr;
}
// static
bool ExternallyInstalledWebAppPrefs::HasAppIdWithInstallSource(
const PrefService* pref_service,
const AppId& app_id,
ExternalInstallSource install_source) {
const base::Value::Dict* dict = GetPreferenceValue(pref_service, app_id);
if (dict == nullptr) {
return false;
}
absl::optional<int> v = dict->FindInt(kInstallSource);
return (v.has_value() && v.value() == static_cast<int>(install_source));
}
// static
base::flat_map<AppId, base::flat_set<GURL>>
ExternallyInstalledWebAppPrefs::BuildAppIdsMap(
const PrefService* pref_service,
ExternalInstallSource install_source) {
const base::Value::Dict& urls_to_dicts =
pref_service->GetDict(prefs::kWebAppsExtensionIDs);
base::flat_map<AppId, base::flat_set<GURL>> ids_to_urls;
for (auto it : urls_to_dicts) {
const base::Value* const v = &it.second;
if (!v->is_dict()) {
continue;
}
const base::Value::Dict& dict = v->GetDict();
const absl::optional<int> install_source_pref =
dict.FindInt(kInstallSource);
if (!install_source_pref ||
(install_source_pref != static_cast<int>(install_source))) {
continue;
}
const std::string* extension_id = dict.FindString(kExtensionId);
if (!extension_id) {
continue;
}
GURL url(it.first);
DCHECK(url.is_valid() && !url.is_empty());
ids_to_urls[*extension_id] = {url};
}
return ids_to_urls;
}
ExternallyInstalledWebAppPrefs::ExternallyInstalledWebAppPrefs(
PrefService* pref_service)
: pref_service_(pref_service) {}
void ExternallyInstalledWebAppPrefs::Insert(
const GURL& url,
const AppId& app_id,
ExternalInstallSource install_source) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::Value dict(base::Value::Type::DICT);
dict.SetKey(kExtensionId, base::Value(app_id));
dict.SetKey(kInstallSource, base::Value(static_cast<int>(install_source)));
ScopedDictPrefUpdate update(pref_service_, prefs::kWebAppsExtensionIDs);
update->Set(url.spec(), std::move(dict));
}
bool ExternallyInstalledWebAppPrefs::Remove(const GURL& url) {
ScopedDictPrefUpdate update(pref_service_, prefs::kWebAppsExtensionIDs);
return update->Remove(url.spec());
}
absl::optional<AppId> ExternallyInstalledWebAppPrefs::LookupAppId(
const GURL& url) const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const base::Value::Dict* dict =
pref_service_->GetDict(prefs::kWebAppsExtensionIDs).FindDict(url.spec());
if (!dict) {
return absl::nullopt;
}
const std::string* extension_id = dict->FindString(kExtensionId);
return extension_id ? absl::make_optional(*extension_id) : absl::nullopt;
}
absl::optional<AppId> ExternallyInstalledWebAppPrefs::LookupPlaceholderAppId(
const GURL& url) const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const base::Value::Dict* entry =
pref_service_->GetDict(prefs::kWebAppsExtensionIDs).FindDict(url.spec());
if (!entry)
return absl::nullopt;
absl::optional<bool> is_placeholder = entry->FindBool(kIsPlaceholder);
if (!is_placeholder.has_value() || !is_placeholder.value())
return absl::nullopt;
return *entry->FindString(kExtensionId);
}
void ExternallyInstalledWebAppPrefs::SetIsPlaceholder(const GURL& url,
bool is_placeholder) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(pref_service_->GetDict(prefs::kWebAppsExtensionIDs).Find(url.spec()));
ScopedDictPrefUpdate update(pref_service_, prefs::kWebAppsExtensionIDs);
base::Value::Dict& map = update.Get();
auto* app_entry = map.FindDict(url.spec());
DCHECK(app_entry);
app_entry->Set(kIsPlaceholder, is_placeholder);
}
bool ExternallyInstalledWebAppPrefs::IsPlaceholderApp(
const AppId& app_id) const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const base::Value::Dict* app_prefs =
GetPreferenceValue(pref_service_, app_id);
if (!app_prefs) {
return false;
}
return app_prefs->FindBool(kIsPlaceholder).value_or(false);
}
// static
ExternallyInstalledWebAppPrefs::ParsedPrefs
ExternallyInstalledWebAppPrefs::ParseExternalPrefsToWebAppData(
PrefService* pref_service) {
const base::Value::Dict& urls_to_dicts =
pref_service->GetDict(prefs::kWebAppsExtensionIDs);
ParsedPrefs ids_to_parsed_data;
for (auto it : urls_to_dicts) {
if (!it.second.is_dict()) {
continue;
}
const base::Value::Dict& dict = it.second.GetDict();
const std::string* app_id = dict.FindString(kExtensionId);
if (!app_id) {
continue;
}
const absl::optional<int> source = dict.FindInt(kInstallSource);
if (!source) {
continue;
}
WebAppManagement::Type source_type = ConvertExternalInstallSourceToSource(
static_cast<ExternalInstallSource>(source.value()));
WebApp::ExternalManagementConfig& config =
ids_to_parsed_data[*app_id][source_type];
config.is_placeholder = dict.FindBool(kIsPlaceholder).value_or(false);
config.install_urls.emplace(GURL(it.first));
}
return ids_to_parsed_data;
}
// static
void ExternallyInstalledWebAppPrefs::MigrateExternalPrefData(
PrefService* pref_service,
WebAppSyncBridge* sync_bridge) {
ExternallyInstalledWebAppPrefs::ParsedPrefs pref_to_app_data =
ParseExternalPrefsToWebAppData(pref_service);
const WebAppRegistrar& registrar = sync_bridge->registrar();
LogDataMetrics(pref_to_app_data.size() != 0,
registrar.AppsExistWithExternalConfigData());
// First migrate data to UserUninstalledPreinstalledWebAppPrefs.
MigrateExternalPrefDataToPreinstalledPrefs(pref_service, &registrar,
pref_to_app_data);
ScopedRegistryUpdate update(sync_bridge);
for (auto it : pref_to_app_data) {
const WebApp* web_app = registrar.GetAppById(it.first);
if (web_app) {
// Sync data across externally installed prefs and web_app DB.
for (auto parsed_info : it.second) {
WebAppManagement::Type& source = parsed_info.first;
if (!web_app->GetSources().test(source))
continue;
const WebApp::ExternalConfigMap& config_map =
web_app->management_to_external_config_map();
auto map_it = config_map.find(source);
// Placeholder migration and metrics logging.
if (map_it != config_map.end() &&
map_it->second.is_placeholder ==
parsed_info.second.is_placeholder) {
LogPlaceholderMigrationState(
PlaceholderMigrationState::kPlaceholderInfoAlreadyInSync);
} else {
WebApp* updated_app = update->UpdateApp(it.first);
updated_app->AddPlaceholderInfoToManagementExternalConfigMap(
source, parsed_info.second.is_placeholder);
LogPlaceholderMigrationState(
PlaceholderMigrationState::kPlaceholderInfoMigrated);
}
// Install URL migration and metrics logging.
for (auto url : parsed_info.second.install_urls) {
DCHECK(url.is_valid());
if (map_it != config_map.end() &&
base::Contains(map_it->second.install_urls, url)) {
LogInstallURLMigrationState(
InstallURLMigrationState::kInstallURLAlreadyInSync);
} else {
WebApp* updated_app = update->UpdateApp(it.first);
updated_app->AddInstallURLToManagementExternalConfigMap(
parsed_info.first, url);
LogInstallURLMigrationState(
InstallURLMigrationState::kInstallURLMigrated);
}
}
}
}
}
FixMigrationCorruptionFromLacrosSwitch(pref_service, registrar);
}
// static
void ExternallyInstalledWebAppPrefs::MigrateExternalPrefDataToPreinstalledPrefs(
PrefService* pref_service,
const WebAppRegistrar* registrar,
const ExternallyInstalledWebAppPrefs::ParsedPrefs& parsed_data) {
UserUninstalledPreinstalledWebAppPrefs preinstalled_prefs(pref_service);
for (auto pair : parsed_data) {
const AppId& app_id = pair.first;
const WebApp::ExternalConfigMap& source_to_config_map = pair.second;
const auto& it = source_to_config_map.find(WebAppManagement::kDefault);
// Migration will happen in the following cases:
// 1. If app_id exists in the external prefs that had source as
// kDefault but the app is no longer installed in the registry or if it
// is no longer preinstalled, that means
// it was preinstalled and then uninstalled by user.
if (!registrar->IsInstalledByDefaultManagement(app_id) &&
it != source_to_config_map.end()) {
if (preinstalled_prefs.AppIdContainsAllUrls(app_id, source_to_config_map,
/*only_default=*/true)) {
LogUserUninstalledPreinstalledAppMigration(
UserUninstalledPreinstalledAppMigrationState::
kPreinstalledAppDataAlreadyInSync);
} else {
preinstalled_prefs.Add(app_id, std::move(it->second.install_urls));
LogUserUninstalledPreinstalledAppMigration(
UserUninstalledPreinstalledAppMigrationState::
kPreinstalledAppDataMigratedByUser);
}
}
// 2. If the value corresponding to the app_id in
// kWasExternalAppUninstalledByUser is true, then it was previously
// a preinstalled app that was user uninstalled. In this case, we migrate
// ALL install URLs for that corresponding app_id, because a preinstalled
// app could have been installed as an app with a different source now.
if (GetBoolWebAppPref(pref_service, app_id,
kWasExternalAppUninstalledByUser)) {
if (preinstalled_prefs.AppIdContainsAllUrls(app_id, source_to_config_map,
/*only_default=*/false)) {
LogUserUninstalledPreinstalledAppMigration(
UserUninstalledPreinstalledAppMigrationState::
kPreinstalledAppDataAlreadyInSync);
} else {
base::flat_set<GURL> urls_to_migrate =
MergeAllUrls(source_to_config_map);
preinstalled_prefs.Add(app_id, std::move(urls_to_migrate));
LogUserUninstalledPreinstalledAppMigration(
UserUninstalledPreinstalledAppMigrationState::
kPreinstalledAppDataMigratedByOldPref);
}
}
}
}
// static
base::flat_set<GURL> ExternallyInstalledWebAppPrefs::MergeAllUrls(
const WebApp::ExternalConfigMap& source_config_map) {
std::vector<GURL> urls;
for (auto it : source_config_map) {
for (const GURL& url : it.second.install_urls) {
DCHECK(url.is_valid());
urls.push_back(url);
}
}
return urls;
}
// static
void ExternallyInstalledWebAppPrefs::LogDataMetrics(
bool data_exists_in_pref,
bool data_exists_in_registrar) {
// Data in registry refers to the data stored in
// management_to_external_config_map per web_app. See
// WebApps::management_to_external_config_map() for more info.
if (!data_exists_in_pref && !data_exists_in_registrar) {
// Case 1: No external apps installed (prefs empty, empty data per web_app
// in the registry).
base::UmaHistogramBoolean(kPrefDataAbsentDBDataAbsent, /*sample=*/true);
} else if (!data_exists_in_pref && data_exists_in_registrar) {
// Case 2: prefs are empty, but data exists in registry.
base::UmaHistogramBoolean(kPrefDataAbsentDBDataPresent, /*sample=*/true);
} else if (data_exists_in_pref && !data_exists_in_registrar) {
// Case 3: prefs contain data, but data does not exist in the registry.
base::UmaHistogramBoolean(kPrefDataPresentDBDataAbsent, /*sample=*/true);
} else {
// Case 4: Data exists in both prefs and in the registry.
base::UmaHistogramBoolean(kPrefDataPresentDBDataPresent, /*sample=*/true);
}
}
} // namespace web_app