blob: 4d102e68165c8ab332ca7f51271d0a3a844bfdc5 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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/external_web_app_manager.h"
#include <iterator>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/json/json_file_value_serializer.h"
#include "base/json/json_reader.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/path_service.h"
#include "base/stl_util.h"
#include "base/strings/strcat.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/apps/user_type_filter.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/components/external_app_install_features.h"
#include "chrome/browser/web_applications/components/externally_installed_web_app_prefs.h"
#include "chrome/browser/web_applications/components/pending_app_manager.h"
#include "chrome/browser/web_applications/components/web_app_constants.h"
#include "chrome/browser/web_applications/components/web_app_install_utils.h"
#include "chrome/browser/web_applications/extension_status_utils.h"
#include "chrome/browser/web_applications/external_web_app_utils.h"
#include "chrome/browser/web_applications/preinstalled_web_apps/preinstalled_web_apps.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chromeos/constants/chromeos_switches.h"
#include "components/arc/arc_util.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
namespace web_app {
namespace {
#if BUILDFLAG(IS_CHROMEOS_ASH)
// The sub-directory of the extensions directory in which to scan for external
// web apps (as opposed to external extensions or external ARC apps).
const base::FilePath::CharType kWebAppsSubDirectory[] =
FILE_PATH_LITERAL("web_apps");
#endif
bool g_skip_startup_for_testing_ = false;
bool g_bypass_offline_manifest_requirement_for_testing_ = false;
const base::FilePath* g_config_dir_for_testing = nullptr;
const std::vector<base::Value>* g_configs_for_testing = nullptr;
const FileUtilsWrapper* g_file_utils_for_testing = nullptr;
struct LoadedConfig {
base::Value contents;
base::FilePath file;
};
struct LoadedConfigs {
std::vector<LoadedConfig> configs;
std::vector<std::string> errors;
};
LoadedConfigs LoadConfigsBlocking(const base::FilePath& config_dir) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::MAY_BLOCK);
LoadedConfigs result;
base::FilePath::StringType extension(FILE_PATH_LITERAL(".json"));
base::FileEnumerator json_files(config_dir,
false, // Recursive.
base::FileEnumerator::FILES);
for (base::FilePath file = json_files.Next(); !file.empty();
file = json_files.Next()) {
if (!file.MatchesExtension(extension)) {
continue;
}
JSONFileValueDeserializer deserializer(file);
std::string error_msg;
std::unique_ptr<base::Value> app_config =
deserializer.Deserialize(nullptr, &error_msg);
if (!app_config) {
result.errors.push_back(base::StrCat(
{file.AsUTF8Unsafe(), " was not valid JSON: ", error_msg}));
VLOG(1) << result.errors.back();
continue;
}
result.configs.push_back(
{.contents = std::move(*app_config), .file = file});
}
return result;
}
struct ParsedConfigs {
std::vector<ExternalInstallOptions> options_list;
std::vector<std::string> errors;
};
ParsedConfigs ParseConfigsBlocking(const base::FilePath& config_dir,
LoadedConfigs loaded_configs) {
ParsedConfigs result;
result.errors = std::move(loaded_configs.errors);
auto file_utils = g_file_utils_for_testing
? g_file_utils_for_testing->Clone()
: std::make_unique<FileUtilsWrapper>();
for (const LoadedConfig& loaded_config : loaded_configs.configs) {
OptionsOrError parse_result = ParseConfig(
*file_utils, config_dir, loaded_config.file, loaded_config.contents);
if (ExternalInstallOptions* options =
absl::get_if<ExternalInstallOptions>(&parse_result)) {
result.options_list.push_back(std::move(*options));
} else {
result.errors.push_back(std::move(absl::get<std::string>(parse_result)));
VLOG(1) << result.errors.back();
}
}
return result;
}
base::Optional<std::string> GetDisableReason(
const ExternalInstallOptions& options,
Profile* profile,
bool is_new_user,
const std::string& user_type) {
// Remove if not applicable to current user type.
DCHECK_GT(options.user_type_allowlist.size(), 0u);
if (!base::Contains(options.user_type_allowlist, user_type)) {
return options.install_url.spec() + " disabled for user type: " + user_type;
}
// Remove if gated on a disabled feature.
if (options.gate_on_feature &&
!IsExternalAppInstallFeatureEnabled(*options.gate_on_feature)) {
return options.install_url.spec() +
" disabled because feature is disabled: " + *options.gate_on_feature;
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Remove if ARC is supported and app should be disabled.
if (options.disable_if_arc_supported && arc::IsArcAvailable()) {
return options.install_url.spec() + " disabled because ARC is available.";
}
// Remove if device is tablet and app should be disabled.
if (options.disable_if_tablet_form_factor &&
chromeos::switches::IsTabletFormFactor()) {
return options.install_url.spec() + " disabled because device is tablet.";
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
// Remove if only for new users, user isn't new and app was not
// installed previously.
if (options.only_for_new_users && !is_new_user) {
bool was_previously_installed =
ExternallyInstalledWebAppPrefs(profile->GetPrefs())
.LookupAppId(options.install_url)
.has_value();
if (!was_previously_installed) {
return options.install_url.spec() +
" disabled because user was not new when config was added.";
}
}
// Remove if any apps to replace are blocked by admin policy.
for (const AppId& app_id : options.uninstall_and_replace) {
if (extensions::IsExtensionBlockedByPolicy(profile, app_id)) {
return options.install_url.spec() +
" disabled due to admin policy blocking replacement "
"Extension.";
}
}
// Keep if any apps to replace are installed.
for (const AppId& app_id : options.uninstall_and_replace) {
if (extensions::IsExtensionInstalled(profile, app_id)) {
return base::nullopt;
}
}
// Remove if any apps to replace were previously uninstalled.
for (const AppId& app_id : options.uninstall_and_replace) {
if (extensions::IsExternalExtensionUninstalled(profile, app_id)) {
return options.install_url.spec() +
" disabled because apps to replace were uninstalled.";
}
}
return base::nullopt;
}
} // namespace
const char* ExternalWebAppManager::kHistogramEnabledCount =
"WebApp.Preinstalled.EnabledCount";
const char* ExternalWebAppManager::kHistogramDisabledCount =
"WebApp.Preinstalled.DisabledCount";
const char* ExternalWebAppManager::kHistogramConfigErrorCount =
"WebApp.Preinstalled.ConfigErrorCount";
const char* ExternalWebAppManager::kHistogramInstallResult =
"Webapp.InstallResult.Default";
const char* ExternalWebAppManager::kHistogramUninstallAndReplaceCount =
"WebApp.Preinstalled.UninstallAndReplaceCount";
void ExternalWebAppManager::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterStringPref(prefs::kWebAppsLastPreinstallSynchronizeVersion,
"");
}
void ExternalWebAppManager::SkipStartupForTesting() {
g_skip_startup_for_testing_ = true;
}
void ExternalWebAppManager::BypassOfflineManifestRequirementForTesting() {
g_bypass_offline_manifest_requirement_for_testing_ = true;
}
void ExternalWebAppManager::SetConfigDirForTesting(
const base::FilePath* config_dir) {
g_config_dir_for_testing = config_dir;
}
void ExternalWebAppManager::SetConfigsForTesting(
const std::vector<base::Value>* configs) {
g_configs_for_testing = configs;
}
void ExternalWebAppManager::SetFileUtilsForTesting(
const FileUtilsWrapper* file_utils) {
g_file_utils_for_testing = file_utils;
}
ExternalWebAppManager::ExternalWebAppManager(Profile* profile)
: profile_(profile) {
if (base::FeatureList::IsEnabled(features::kRecordWebAppDebugInfo)) {
debug_info_ = std::make_unique<DebugInfo>();
}
}
ExternalWebAppManager::~ExternalWebAppManager() = default;
void ExternalWebAppManager::SetSubsystems(
PendingAppManager* pending_app_manager) {
pending_app_manager_ = pending_app_manager;
}
void ExternalWebAppManager::Start() {
if (!g_skip_startup_for_testing_) {
LoadAndSynchronize(
base::BindOnce(&ExternalWebAppManager::OnStartUpTaskCompleted,
weak_ptr_factory_.GetWeakPtr()));
}
}
void ExternalWebAppManager::LoadForTesting(ConsumeInstallOptions callback) {
Load(std::move(callback));
}
void ExternalWebAppManager::LoadAndSynchronizeForTesting(
SynchronizeCallback callback) {
LoadAndSynchronize(std::move(callback));
}
void ExternalWebAppManager::LoadAndSynchronize(SynchronizeCallback callback) {
Load(base::BindOnce(&ExternalWebAppManager::Synchronize,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void ExternalWebAppManager::Load(ConsumeInstallOptions callback) {
if (!base::FeatureList::IsEnabled(features::kDefaultWebAppInstallation)) {
std::move(callback).Run({});
return;
}
LoadConfigs(base::BindOnce(
&ExternalWebAppManager::ParseConfigs, weak_ptr_factory_.GetWeakPtr(),
base::BindOnce(&ExternalWebAppManager::PostProcessConfigs,
weak_ptr_factory_.GetWeakPtr(), std::move(callback))));
}
void ExternalWebAppManager::LoadConfigs(ConsumeLoadedConfigs callback) {
if (g_configs_for_testing) {
LoadedConfigs loaded_configs;
for (const base::Value& config : *g_configs_for_testing) {
loaded_configs.configs.push_back(
{.contents = config.Clone(),
.file = base::FilePath(FILE_PATH_LITERAL("test.json"))});
}
std::move(callback).Run(std::move(loaded_configs));
return;
}
base::FilePath config_dir = GetConfigDir();
if (config_dir.empty()) {
std::move(callback).Run({});
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(&LoadConfigsBlocking, config_dir), std::move(callback));
}
void ExternalWebAppManager::ParseConfigs(ConsumeParsedConfigs callback,
LoadedConfigs loaded_configs) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(&ParseConfigsBlocking, GetConfigDir(),
std::move(loaded_configs)),
std::move(callback));
}
void ExternalWebAppManager::PostProcessConfigs(ConsumeInstallOptions callback,
ParsedConfigs parsed_configs) {
// Add hard coded configs.
for (ExternalInstallOptions& options : GetPreinstalledWebApps())
parsed_configs.options_list.push_back(std::move(options));
// Set common install options.
for (ExternalInstallOptions& options : parsed_configs.options_list) {
ALLOW_UNUSED_LOCAL(options);
DCHECK_EQ(options.install_source, ExternalInstallSource::kExternalDefault);
#if !BUILDFLAG(IS_CHROMEOS_ASH)
if (!g_bypass_offline_manifest_requirement_for_testing_) {
// Non-Chrome OS platforms are not permitted to fetch the web app install
// URLs during start up.
DCHECK(options.app_info_factory);
options.only_use_app_info_factory = true;
}
// Preinstalled web apps should not have OS shortcuts of any kind outside of
// Chrome OS.
options.add_to_applications_menu = false;
options.add_to_search = false;
options.add_to_management = false;
options.add_to_desktop = false;
options.add_to_quick_launch_bar = false;
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
bool is_new_user = IsNewUser();
std::string user_type = apps::DetermineUserType(profile_);
size_t disabled_count = 0;
base::EraseIf(
parsed_configs.options_list, [&](const ExternalInstallOptions& options) {
base::Optional<std::string> disable_reason =
GetDisableReason(options, profile_, is_new_user, user_type);
if (disable_reason) {
VLOG(1) << *disable_reason;
++disabled_count;
if (debug_info_) {
debug_info_->disabled_configs.emplace_back(
std::move(options), std::move(*disable_reason));
}
return true;
}
return false;
});
if (debug_info_) {
debug_info_->parse_errors = parsed_configs.errors;
debug_info_->enabled_configs = parsed_configs.options_list;
}
UMA_HISTOGRAM_COUNTS_100(kHistogramEnabledCount,
parsed_configs.options_list.size());
UMA_HISTOGRAM_COUNTS_100(kHistogramDisabledCount, disabled_count);
UMA_HISTOGRAM_COUNTS_100(kHistogramConfigErrorCount,
parsed_configs.errors.size());
std::move(callback).Run(parsed_configs.options_list);
}
void ExternalWebAppManager::Synchronize(
PendingAppManager::SynchronizeCallback callback,
std::vector<ExternalInstallOptions> desired_apps_install_options) {
DCHECK(pending_app_manager_);
pending_app_manager_->SynchronizeInstalledApps(
std::move(desired_apps_install_options),
ExternalInstallSource::kExternalDefault,
base::BindOnce(&ExternalWebAppManager::OnExternalWebAppsSynchronized,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void ExternalWebAppManager::OnExternalWebAppsSynchronized(
PendingAppManager::SynchronizeCallback callback,
std::map<GURL, PendingAppManager::InstallResult> install_results,
std::map<GURL, bool> uninstall_results) {
// Note that we are storing the Chrome version instead of a "has synchronised"
// bool in order to do version update specific logic in the future.
profile_->GetPrefs()->SetString(
prefs::kWebAppsLastPreinstallSynchronizeVersion,
version_info::GetMajorVersionNumber());
size_t uninstall_and_replace_count = 0;
for (const auto& url_and_result : install_results) {
UMA_HISTOGRAM_ENUMERATION(kHistogramInstallResult,
url_and_result.second.code);
if (url_and_result.second.did_uninstall_and_replace)
++uninstall_and_replace_count;
}
UMA_HISTOGRAM_COUNTS_100(kHistogramUninstallAndReplaceCount,
uninstall_and_replace_count);
if (callback) {
std::move(callback).Run(std::move(install_results),
std::move(uninstall_results));
}
}
void ExternalWebAppManager::OnStartUpTaskCompleted(
std::map<GURL, PendingAppManager::InstallResult> install_results,
std::map<GURL, bool> uninstall_results) {
if (debug_info_) {
debug_info_->is_start_up_task_complete = true;
debug_info_->install_results = std::move(install_results);
debug_info_->uninstall_results = std::move(uninstall_results);
}
}
base::FilePath ExternalWebAppManager::GetConfigDir() {
base::FilePath dir;
#if BUILDFLAG(IS_CHROMEOS_ASH)
// As of mid 2018, only Chrome OS has default/external web apps, and
// chrome::DIR_STANDALONE_EXTERNAL_EXTENSIONS is only defined for OS_LINUX,
// which includes OS_CHROMEOS.
if (chromeos::ProfileHelper::IsPrimaryProfile(profile_)) {
if (g_config_dir_for_testing) {
dir = *g_config_dir_for_testing;
} else {
// For manual testing, you can change s/STANDALONE/USER/, as writing to
// "$HOME/.config/chromium/test-user/.config/chromium/External
// Extensions/web_apps" does not require root ACLs, unlike
// "/usr/share/chromium/extensions/web_apps".
if (!base::PathService::Get(chrome::DIR_STANDALONE_EXTERNAL_EXTENSIONS,
&dir)) {
LOG(ERROR) << "base::PathService::Get failed";
} else {
dir = dir.Append(kWebAppsSubDirectory);
}
}
}
#endif
return dir;
}
bool ExternalWebAppManager::IsNewUser() {
PrefService* prefs = profile_->GetPrefs();
std::string last_version =
prefs->GetString(prefs::kWebAppsLastPreinstallSynchronizeVersion);
if (!last_version.empty())
return false;
// It's not enough to check whether the last_version string has been set
// because users have been around before this pref was introduced (M88). We
// distinguish those users via the presence of any
// ExternallyInstalledWebAppPrefs which would have been set by past default
// app installs. Remove this after a few Chrome versions have passed.
return ExternallyInstalledWebAppPrefs(prefs).HasNoApps();
}
ExternalWebAppManager::DebugInfo::DebugInfo() = default;
ExternalWebAppManager::DebugInfo::~DebugInfo() = default;
} // namespace web_app