blob: ea430a23d758418b8dc108cb7b29404fdc1df11c [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/policy/web_app_policy_manager.h"
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "base/check_deref.h"
#include "base/containers/contains.h"
#include "base/containers/extend.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/containers/map_util.h"
#include "base/containers/to_vector.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/functional/concurrent_closures.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/syslog_logging.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/external_install_options.h"
#include "chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_external_install_options.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/os_integration/os_integration_sub_manager.h"
#include "chrome/browser/web_applications/policy/pre_redirection_url_observer.h"
#include "chrome/browser/web_applications/policy/web_app_policy_constants.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_install_utils.h"
#include "chrome/browser/web_applications/web_app_management_type.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/crx_file/id_util.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
#include "third_party/blink/public/common/manifest/manifest.h"
#include "url/url_constants.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/web_app_id_constants.h"
#include "ash/edusumer/graduation_utils.h"
#include "ash/webui/system_apps/public/system_web_app_type.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/policy/profile_policy_connector.h"
#include "chrome/browser/policy/system_features_disable_list_policy_handler.h"
#include "chrome/browser/web_applications/policy/app_service_web_app_policy.h"
#include "chrome/browser/web_applications/web_app_system_web_app_delegate_map_utils.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chromeos/ash/components/file_manager/app_id.h"
#include "chromeos/ash/components/policy/system_features_disable_list/system_features_disable_list_policy_utils.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/policy/core/common/system_features_disable_list_constants.h"
#include "components/user_manager/user_manager.h"
#endif
namespace {
bool IconInfosContainIconURL(const std::vector<apps::IconInfo>& icon_infos,
const GURL& url) {
for (const apps::IconInfo& info : icon_infos) {
if (info.url.EqualsIgnoringRef(url))
return true;
}
return false;
}
// Policy installed apps are only allowed on:
// 1. ChromeOS guest sessions (current only on Ash).
// 2. All Chrome profiles apart from incognito/guest profiles.
bool AreForceInstalledAppsAllowed(Profile* profile) {
bool allowed = web_app::AreWebAppsUserInstallable(profile);
#if BUILDFLAG(IS_CHROMEOS)
allowed = allowed || user_manager::UserManager::Get()->IsLoggedInAsGuest() ||
user_manager::UserManager::Get()->IsLoggedInAsManagedGuestSession();
#endif
return allowed;
}
bool IsForceUnregistrationPolicyEnabled() {
return base::FeatureList::IsEnabled(
web_app::kDesktopPWAsForceUnregisterOSIntegration);
}
#if BUILDFLAG(IS_CHROMEOS)
inline constexpr std::string_view kDisabled = "disabled";
// Note that this mapping lists only selected Preinstalled Web Apps
// actively used in policies and is not meant to be exhaustive.
// These app Id constants need to be kept in sync with java/com/
// google/chrome/cros/policyconverter/ChromePolicySettingsProcessor.java
// LINT.IfChange
constexpr auto kPreinstalledWebAppsMapping =
base::MakeFixedFlatMap<std::string_view, std::string_view>(
{{"cursive", ash::kCursiveAppId}, {"canvas", ash::kCanvasAppId}});
// LINT.ThenChange(//depot/google3/java/com/google/chrome/cros/policyconverter/ChromePolicySettingsProcessor.java)
#endif // BUILDFLAG(IS_CHROMEOS)
std::optional<base::flat_map<std::string_view, std::string_view>>&
GetPreinstalledWebAppsMappingForTesting() {
static base::NoDestructor<
std::optional<base::flat_map<std::string_view, std::string_view>>>
preinstalled_web_apps_mapping_for_testing;
return *preinstalled_web_apps_mapping_for_testing;
}
} // namespace
namespace web_app {
BASE_FEATURE(kDesktopPWAsForceUnregisterOSIntegration,
"DesktopPWAsForceUnregisterOSIntegration",
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
base::FEATURE_ENABLED_BY_DEFAULT
#else
base::FEATURE_DISABLED_BY_DEFAULT
#endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
);
const char WebAppPolicyManager::kInstallResultHistogramName[];
WebAppPolicyManager::WebAppPolicyManager(Profile* profile)
: profile_(profile), pref_service_(profile_->GetPrefs()) {}
WebAppPolicyManager::~WebAppPolicyManager() = default;
#if BUILDFLAG(IS_CHROMEOS)
void WebAppPolicyManager::SetSystemWebAppDelegateMap(
const ash::SystemWebAppDelegateMap* system_web_apps_delegate_map) {
system_web_apps_delegate_map_ = system_web_apps_delegate_map;
}
#endif
void WebAppPolicyManager::SetProvider(base::PassKey<WebAppProvider>,
WebAppProvider& provider) {
provider_ = &provider;
}
void WebAppPolicyManager::Start(
base::OnceClosure policy_settings_and_force_installs_applied) {
DCHECK(policy_settings_and_force_installs_applied_.is_null());
policy_settings_and_force_installs_applied_ =
std::move(policy_settings_and_force_installs_applied);
content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT})
->PostTask(FROM_HERE,
base::BindOnce(
&WebAppPolicyManager::InitChangeRegistrarAndRefreshPolicy,
weak_ptr_factory_.GetWeakPtr()));
}
void WebAppPolicyManager::Shutdown() {
weak_ptr_factory_.InvalidateWeakPtrs();
}
void WebAppPolicyManager::ReinstallPlaceholderAppIfNecessary(
const GURL& url,
ExternallyManagedAppManager::OnceInstallCallback on_complete) {
const base::Value::List& web_apps =
pref_service_->GetList(prefs::kWebAppInstallForceList);
const auto& web_apps_list = web_apps;
const auto it = std::ranges::find(
web_apps_list, url.spec(), [](const base::Value& entry) {
return CHECK_DEREF(entry.GetDict().FindString(kUrlKey));
});
bool is_placeholder_url =
provider_->registrar_unsafe()
.LookupPlaceholderAppId(url, WebAppManagement::kPolicy)
.has_value();
if (it == web_apps_list.end() || !is_placeholder_url) {
std::move(on_complete)
.Run(url, ExternallyManagedAppManager::InstallResult(
webapps::InstallResultCode::kFailedPlaceholderUninstall));
return;
}
std::optional<ExternalInstallOptions> install_options =
ParseInstallPolicyEntry(it->GetDict());
// The install_url must have been invalid for install policy parsing to return
// a `std::nullopt`.
if (!install_options.has_value()) {
std::move(on_complete)
.Run(url, ExternallyManagedAppManager::InstallResult(
webapps::InstallResultCode::kInstallURLInvalid));
return;
}
// No need to install a placeholder because there should be one already.
install_options->placeholder_resolution_behavior =
PlaceholderResolutionBehavior::kWaitForAppWindowsClosed;
// If the app is not a placeholder app, ExternallyManagedAppManager will
// ignore the request.
provider_->externally_managed_app_manager().InstallNow(
std::move(*install_options), std::move(on_complete));
}
// static
void WebAppPolicyManager::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterListPref(prefs::kWebAppInstallForceList);
registry->RegisterListPref(prefs::kWebAppSettings);
}
// static
bool WebAppPolicyManager::IsChromeAppPolicyId(std::string_view policy_id) {
return crx_file::id_util::IdIsValid(policy_id);
}
// static
bool WebAppPolicyManager::IsWebAppPolicyId(std::string_view policy_id) {
return GURL{policy_id}.is_valid();
}
// static
std::optional<std::string_view>
WebAppPolicyManager::GetPolicyIdForPreinstalledWebApp(std::string_view app_id) {
if (const auto& test_mapping = GetPreinstalledWebAppsMappingForTesting()) {
for (const auto& [policy_id, mapped_app_id] : *test_mapping) {
if (mapped_app_id == app_id) {
return policy_id;
}
}
return {};
}
#if BUILDFLAG(IS_CHROMEOS)
for (const auto& [policy_id, mapped_app_id] : kPreinstalledWebAppsMapping) {
if (mapped_app_id == app_id) {
return policy_id;
}
}
#endif // BUILDFLAG(IS_CHROMEOS)
return {};
}
// static
void WebAppPolicyManager::SetPreinstalledWebAppsMappingForTesting( // IN-TEST
std::optional<base::flat_map<std::string_view, std::string_view>>
preinstalled_web_apps_mapping_for_testing) {
GetPreinstalledWebAppsMappingForTesting() = // IN-TEST
std::move(preinstalled_web_apps_mapping_for_testing); // IN-TEST
}
// static
bool WebAppPolicyManager::IsPreinstalledWebAppPolicyId(
std::string_view policy_id) {
if (auto& mapping = GetPreinstalledWebAppsMappingForTesting()) { // IN-TEST
return base::Contains(*mapping, policy_id);
}
#if BUILDFLAG(IS_CHROMEOS)
return base::Contains(kPreinstalledWebAppsMapping, policy_id);
#else
return false;
#endif // BUILDFLAG(IS_CHROMEOS)
}
// static
bool WebAppPolicyManager::IsIsolatedWebAppPolicyId(std::string_view policy_id) {
return web_package::SignedWebBundleId::Create(policy_id).has_value();
}
// static
std::vector<std::string> WebAppPolicyManager::GetPolicyIds(
Profile* profile,
const WebApp& web_app) {
const auto& app_id = web_app.app_id();
WebAppRegistrar& web_app_registrar =
WebAppProvider::GetForWebApps(profile)->registrar_unsafe();
if (web_app_registrar.IsIsolated(app_id) &&
web_app_registrar.IsInstalledByPolicy(app_id)) {
// This is an IWA - and thus, web_bundle_id == policy_id == URL hostname
return {web_app.start_url().host()};
}
std::vector<std::string> policy_ids;
if (std::optional<std::string_view> preinstalled_web_app_policy_id =
GetPolicyIdForPreinstalledWebApp(app_id)) {
policy_ids.emplace_back(*preinstalled_web_app_policy_id);
}
#if BUILDFLAG(IS_CHROMEOS)
const auto& swa_data = web_app.client_data().system_web_app_data;
if (swa_data) {
const ash::SystemWebAppType swa_type = swa_data->system_app_type;
const std::optional<std::string_view> swa_policy_id =
GetPolicyIdForSystemWebAppType(swa_type);
if (swa_policy_id) {
policy_ids.emplace_back(*swa_policy_id);
}
// File Manager SWA uses File Manager Extension's ID for policy.
if (swa_type == ash::SystemWebAppType::FILE_MANAGER) {
policy_ids.push_back(file_manager::kFileManagerAppId);
}
}
#endif // BUIDLFLAG(IS_CHROMEOS)
for (const auto& [source, external_config] :
web_app.management_to_external_config_map()) {
if (!external_config.additional_policy_ids.empty()) {
base::Extend(policy_ids, external_config.additional_policy_ids);
}
}
if (!web_app_registrar.HasExternalAppWithInstallSource(
app_id, ExternalInstallSource::kExternalPolicy)) {
return policy_ids;
}
base::flat_map<webapps::AppId, base::flat_set<GURL>> installed_apps =
web_app_registrar.GetExternallyInstalledApps(
ExternalInstallSource::kExternalPolicy);
if (auto* install_urls = base::FindOrNull(installed_apps, app_id)) {
DCHECK(!install_urls->empty());
base::Extend(policy_ids, base::ToVector(*install_urls, &GURL::spec));
}
return policy_ids;
}
void WebAppPolicyManager::InitChangeRegistrarAndRefreshPolicy() {
pref_change_registrar_.Init(pref_service_);
pref_change_registrar_.Add(
prefs::kWebAppInstallForceList,
base::BindRepeating(&WebAppPolicyManager::RefreshPolicyInstalledApps,
weak_ptr_factory_.GetWeakPtr(),
/*allow_close_and_relaunch=*/false));
pref_change_registrar_.Add(
prefs::kWebAppSettings,
base::BindRepeating(&WebAppPolicyManager::RefreshPolicySettings,
weak_ptr_factory_.GetWeakPtr()));
RefreshPolicySettings();
#if BUILDFLAG(IS_CHROMEOS)
RefreshPolicyInstalledApps(
/*allow_close_and_relaunch=*/base::FeatureList::IsEnabled(
features::kForcedAppRelaunchOnPlaceholderUpdate));
pref_change_registrar_.Add(
prefs::kDefaultHandlersForFileExtensions,
base::BindRepeating(
&WebAppPolicyManager::SynchronizeOsWithPolicyDefinedFileHandlers,
weak_ptr_factory_.GetWeakPtr()));
#else
RefreshPolicyInstalledApps(/*allow_close_and_relaunch=*/false);
#endif
ObserveDisabledSystemFeaturesPolicy();
}
void WebAppPolicyManager::OnDisableListPolicyChanged() {
#if BUILDFLAG(IS_CHROMEOS)
PopulateDisabledWebAppsIdsLists();
std::vector<webapps::AppId> app_ids =
provider_->registrar_unsafe().GetAppIds();
WebAppProvider* provider = WebAppProvider::GetForLocalAppsUnchecked(profile_);
for (const auto& id : app_ids) {
const bool is_disabled = base::Contains(disabled_web_apps_, id);
provider->scheduler().SetAppIsDisabled(id, is_disabled, base::DoNothing());
}
#endif // BUILDFLAG(IS_CHROMEOS)
}
void WebAppPolicyManager::OnSyncPolicySettingsCommandsComplete() {
provider_->registrar_unsafe().NotifyWebAppSettingsPolicyChanged();
if (refresh_policy_settings_completed_) {
std::move(refresh_policy_settings_completed_).Run();
}
}
#if BUILDFLAG(IS_CHROMEOS)
const absl::flat_hash_set<ash::SystemWebAppType>&
WebAppPolicyManager::GetDisabledSystemWebApps() const {
return disabled_system_apps_;
}
bool WebAppPolicyManager::IsDisabledAppsModeHidden(
std::optional<ash::SystemWebAppType> system_app_type) const {
if (system_app_type.has_value() &&
base::Contains(disabled_system_apps_not_hidden_,
system_app_type.value())) {
return false;
}
PrefService* const local_state = g_browser_process->local_state();
if (!local_state) { // Sometimes it's not available in tests.
return false;
}
return policy::IsDisabledAppsModeHidden(*local_state);
}
#endif // BUILDFLAG(IS_CHROMEOS)
bool WebAppPolicyManager::IsWebAppInDisabledList(
const webapps::AppId& app_id) const {
return base::Contains(disabled_web_apps_, app_id);
}
void WebAppPolicyManager::RefreshPolicyInstalledApps(
bool allow_close_and_relaunch) {
#if !BUILDFLAG(IS_CHROMEOS)
CHECK(!allow_close_and_relaunch);
#endif // !BUILDFLAG(IS_CHROMEOS)
if (!AreForceInstalledAppsAllowed(profile_)) {
OnWebAppForceInstallPolicyParsed();
return;
}
// If this is called again while in progress, we will run it again once the
// |SynchronizeInstalledApps| call is finished.
if (is_refreshing_) {
needs_refresh_ = true;
return;
}
is_refreshing_ = true;
needs_refresh_ = false;
custom_manifest_values_by_url_.clear();
const base::Value::List& web_apps =
pref_service_->GetList(prefs::kWebAppInstallForceList);
std::vector<ExternalInstallOptions> install_options_list;
// No need to validate the types or values of the policy members because we
// are using a SimpleSchemaValidatingPolicyHandler which should validate them
// for us.
for (const base::Value& entry : web_apps) {
std::optional<ExternalInstallOptions> install_options =
ParseInstallPolicyEntry(entry.GetDict());
if (!install_options.has_value()) {
continue;
}
install_options->install_placeholder = true;
// When the policy gets refreshed, we should try to reinstall placeholder
// apps but only if they are not being used. In the non-placeholder case, we
// will not reinstall and there is no need to wait for windows being closed.
// Note: an exception to this rule is described in
// go/preventclose-waitforwindowsclosed.
CHECK(install_options->install_url.is_valid());
install_options->placeholder_resolution_behavior =
provider_->registrar_unsafe()
.LookupPlaceholderAppId(install_options->install_url,
WebAppManagement::kPolicy)
.has_value()
? (allow_close_and_relaunch
? PlaceholderResolutionBehavior::kCloseAndRelaunch
: PlaceholderResolutionBehavior::kWaitForAppWindowsClosed)
: PlaceholderResolutionBehavior::kClose;
std::optional<webapps::AppId> app_id =
provider_->registrar_unsafe().LookupExternalAppId(
install_options->install_url);
if (app_id) {
// If the override name has changed, reinstall:
if (install_options->override_name &&
install_options->override_name.value() !=
provider_->registrar_unsafe().GetAppShortName(app_id.value())) {
install_options->force_reinstall = true;
}
// If the override icon has changed, reinstall:
if (install_options->override_icon_url &&
!IconInfosContainIconURL(
provider_->registrar_unsafe().GetAppIconInfos(app_id.value()),
install_options->override_icon_url.value())) {
install_options->force_reinstall = true;
}
}
install_options_list.push_back(std::move(*install_options));
}
provider_->externally_managed_app_manager().SynchronizeInstalledApps(
std::move(install_options_list), ExternalInstallSource::kExternalPolicy,
base::BindOnce(&WebAppPolicyManager::OnAppsSynchronized,
weak_ptr_factory_.GetWeakPtr()));
}
void WebAppPolicyManager::ParsePolicySettings() {
// No need to validate the types or values of the policy members because we
// are using a WebAppSettingsPolicyHandler which should validate them for us.
const base::Value::List& web_apps_list =
pref_service_->GetList(prefs::kWebAppSettings);
settings_by_url_.clear();
default_settings_ = WebAppPolicyManager::WebAppSetting();
// Read default policy, if provided.
const auto it =
std::ranges::find(web_apps_list, kWildcard, [](const base::Value& entry) {
return CHECK_DEREF(entry.GetDict().FindString(kManifestId));
});
if (it != web_apps_list.end() && it->is_dict()) {
if (!default_settings_.Parse(it->GetDict(), true)) {
SYSLOG(WARNING) << "Malformed default web app management setting.";
default_settings_ = WebAppPolicyManager::WebAppSetting();
}
}
// Read policy for individual web apps
for (const auto& iter : web_apps_list) {
const auto& dict = iter.GetDict();
const std::string* web_app_id_str = dict.FindString(kManifestId);
if (*web_app_id_str == kWildcard)
continue;
GURL url = GURL(*web_app_id_str);
if (!url.is_valid()) {
LOG(WARNING) << "Invalid URL: " << *web_app_id_str;
continue;
}
WebAppPolicyManager::WebAppSetting by_url(default_settings_);
if (by_url.Parse(dict, /*for_default_settings=*/false)) {
settings_by_url_[url.spec()] = by_url;
} else {
LOG(WARNING) << "Malformed web app settings for " << url;
}
}
}
void WebAppPolicyManager::RefreshPolicySettings() {
ParsePolicySettings();
ApplyPolicySettings();
}
void WebAppPolicyManager::SynchronizeOsWithPolicyDefinedFileHandlers() {
provider_->scheduler().SynchronizeOsIntegrationForAllApps(
WebAppFilter::InstalledInChrome(), base::DoNothing());
}
void WebAppPolicyManager::ApplyPolicySettings() {
// The number of closures are 2, since we want to wait for 2 things to
// complete:
// 1. Applying Run on OS login settings policy.
// 2. Applying force unregistration settings policy.
// If for any reason the same app_id is being used for both Run on OS
// login and force unregistration, it is still safe, since both functions
// invoke commands, so the Run on OS login will always be scheduled before the
// force unregistration, and execution will be synchronous.
base::ConcurrentClosures concurrent;
ApplyRunOnOsLoginPolicySettings(concurrent.CreateClosure());
ApplyForceOSUnregistrationPolicySettings(concurrent.CreateClosure());
std::move(concurrent)
.Done(base::BindOnce(
&WebAppPolicyManager::OnSyncPolicySettingsCommandsComplete,
weak_ptr_factory_.GetWeakPtr()));
}
void WebAppPolicyManager::ApplyRunOnOsLoginPolicySettings(
base::OnceClosure policy_settings_applied_callback) {
base::ConcurrentClosures concurrent;
WebAppProvider* provider = WebAppProvider::GetForLocalAppsUnchecked(profile_);
for (const webapps::AppId& app_id :
provider_->registrar_unsafe().GetAppIds()) {
provider->scheduler().SyncRunOnOsLoginMode(app_id,
concurrent.CreateClosure());
}
std::move(concurrent).Done(std::move(policy_settings_applied_callback));
}
void WebAppPolicyManager::ApplyForceOSUnregistrationPolicySettings(
base::OnceClosure policy_settings_applied_callback) {
if (!IsForceUnregistrationPolicyEnabled()) {
std::move(policy_settings_applied_callback).Run();
return;
}
base::ConcurrentClosures concurrent;
SynchronizeOsOptions options;
options.force_unregister_os_integration = true;
for (const auto& [manifest_string, setting] : settings_by_url_) {
const GURL manifest_id = GURL(manifest_string);
if (!manifest_id.is_valid()) {
continue;
}
const webapps::AppId& app_id =
web_app::GenerateAppIdFromManifestId(manifest_id);
if (provider_->registrar_unsafe().GetInstallState(app_id) !=
proto::INSTALLED_WITH_OS_INTEGRATION) {
continue;
}
if (setting.force_unregister_os_integration) {
provider_->scheduler().SynchronizeOsIntegration(
app_id, concurrent.CreateClosure(), options);
}
}
std::move(concurrent).Done(std::move(policy_settings_applied_callback));
}
std::optional<ExternalInstallOptions>
WebAppPolicyManager::ParseInstallPolicyEntry(const base::Value::Dict& entry) {
const std::string* install_url = entry.FindString(kUrlKey);
// url is a required field and is validated by
// SimpleSchemaValidatingPolicyHandler. It is guaranteed to exist.
const GURL install_gurl(CHECK_DEREF(install_url));
const std::string* default_launch_container =
entry.FindString(kDefaultLaunchContainerKey);
const std::optional<bool> create_desktop_shortcut =
entry.FindBool(kCreateDesktopShortcutKey);
const std::string* fallback_app_name = entry.FindString(kFallbackAppNameKey);
const base::Value::List* uninstall_and_replace =
entry.FindList(kUninstallAndReplaceKey);
const std::optional<bool> install_as_diy = entry.FindBool(kInstallAsShortcut);
DCHECK(!default_launch_container ||
(*default_launch_container == kDefaultLaunchContainerWindowValue) ||
(*default_launch_container == kDefaultLaunchContainerTabValue));
if (!install_gurl.is_valid()) {
LOG(WARNING) << "Policy-installed web app has invalid URL " << *install_url;
return std::nullopt;
}
mojom::UserDisplayMode user_display_mode;
if (!default_launch_container) {
user_display_mode = mojom::UserDisplayMode::kBrowser;
} else if (*default_launch_container == kDefaultLaunchContainerTabValue) {
user_display_mode = mojom::UserDisplayMode::kBrowser;
} else {
user_display_mode = mojom::UserDisplayMode::kStandalone;
}
ExternalInstallOptions install_options{
install_gurl, user_display_mode, ExternalInstallSource::kExternalPolicy};
// TODO(dmurph): Store expected os integration state in the database so
// this doesn't re-apply when we already have it done.
// https://crbug.com/1295044
install_options.add_to_applications_menu = true;
install_options.add_to_desktop = create_desktop_shortcut.value_or(false);
// Pinning apps to the ChromeOS shelf is done through the PinnedLauncherApps
// policy.
install_options.add_to_quick_launch_bar = false;
// Allow administrators to override the name of the placeholder app, as well
// as the permanent name for Web Apps without a manifest.
if (fallback_app_name) {
install_options.fallback_app_name = *fallback_app_name;
}
// Used by default Chrome app policy migration to force install web apps and
// uninstall the old Chrome app equivalents.
if (uninstall_and_replace) {
for (const base::Value& item : *uninstall_and_replace) {
if (item.is_string()) {
install_options.uninstall_and_replace.push_back(item.GetString());
}
}
}
// Shortcut apps no longer exist in the web applications system and are
// treated as DIY apps now.
install_options.install_as_diy = install_as_diy.value_or(false);
const std::string* custom_name = entry.FindString(kCustomNameKey);
if (custom_name) {
install_options.override_name = *custom_name;
if (install_gurl.is_valid())
custom_manifest_values_by_url_[install_gurl].SetName(*custom_name);
}
const base::Value::Dict* custom_icon = entry.FindDict(kCustomIconKey);
if (custom_icon && custom_icon) {
const std::string* icon_url = custom_icon->FindString(kCustomIconURLKey);
if (icon_url) {
GURL icon_gurl = GURL(*icon_url);
if (icon_gurl.SchemeIs(url::kHttpsScheme)) {
install_options.override_icon_url = icon_gurl;
if (install_gurl.is_valid())
custom_manifest_values_by_url_[install_gurl].SetIcon(icon_gurl);
} else {
LOG(WARNING) << "Policy-installed web app " << *install_url
<< " has non-https custom icon URL " << *icon_url
<< ", ignoring custom icon.";
}
}
}
return install_options;
}
RunOnOsLoginPolicy WebAppPolicyManager::GetUrlRunOnOsLoginPolicy(
const webapps::AppId& app_id) const {
return GetUrlRunOnOsLoginPolicyByManifestId(
provider_->registrar_unsafe().GetComputedManifestId(app_id).spec());
}
RunOnOsLoginPolicy WebAppPolicyManager::GetUrlRunOnOsLoginPolicyByManifestId(
const std::string& manifest_id) const {
auto it = settings_by_url_.find(manifest_id);
if (it != settings_by_url_.end())
return it->second.run_on_os_login_policy;
return default_settings_.run_on_os_login_policy;
}
void WebAppPolicyManager::SetOnAppsSynchronizedCompletedCallbackForTesting(
base::OnceClosure callback) {
on_apps_synchronized_for_testing_ = std::move(callback);
}
void WebAppPolicyManager::SetRefreshPolicySettingsCompletedCallbackForTesting(
base::OnceClosure callback) {
refresh_policy_settings_completed_ = std::move(callback);
}
void WebAppPolicyManager::RefreshPolicySettingsForTesting() {
RefreshPolicySettings();
}
void WebAppPolicyManager::OverrideManifest(
const GURL& custom_values_key,
blink::mojom::ManifestPtr& manifest) const {
const CustomManifestValues& custom_values =
custom_manifest_values_by_url_.at(custom_values_key);
if (custom_values.name) {
manifest->name = custom_values.name.value();
}
if (custom_values.icons) {
manifest->icons = custom_values.icons.value();
}
}
void WebAppPolicyManager::MaybeOverrideManifest(
content::RenderFrameHost* frame_host,
blink::mojom::ManifestPtr& manifest) const {
// This doesn't override the manifest properly on a non primary page since it
// checks the url from PreRedirectionURLObserver that works only on a primary
// page.
if (!frame_host->IsInPrimaryMainFrame())
return;
if (!manifest)
return;
// For policy-installed apps there are two ways for getting to the manifest:
// via the policy install URL, or via the manifest-specified identity
// of an already installed app. Websites without a manifest will use the
// policy-installed URL as start_url, so they are covered by the first case.
// Second case first:
if (manifest->id.is_valid()) {
const webapps::AppId& app_id = GenerateAppIdFromManifestId(manifest->id);
// List of policy-installed apps and their install URLs:
base::flat_map<webapps::AppId, base::flat_set<GURL>> policy_installed_apps =
provider_->registrar_unsafe().GetExternallyInstalledApps(
ExternalInstallSource::kExternalPolicy);
if (base::Contains(policy_installed_apps, app_id)) {
DCHECK_GT(policy_installed_apps[app_id].size(), 0UL);
for (const GURL& policy_install_url : policy_installed_apps[app_id]) {
if (base::Contains(custom_manifest_values_by_url_, policy_install_url))
OverrideManifest(policy_install_url, manifest);
}
return;
}
}
// And now the first case: assume we got here from the policy install URL.
// We might have been redirected in between, so check where we started
// the current navigation.
const webapps::PreRedirectionURLObserver* const pre_redirect =
webapps::PreRedirectionURLObserver::FromWebContents(
content::WebContents::FromRenderFrameHost(frame_host));
if (!pre_redirect)
return;
GURL install_url = pre_redirect->last_url();
if (base::Contains(custom_manifest_values_by_url_, install_url))
OverrideManifest(install_url, manifest);
}
// TODO(crbug.com/329823863): This method should be placed somewhere else, as it
// is also used for IWAs, which do not use `WebAppPolicyManager`, but
// `IsolatedWebAppPolicyManager`.
bool WebAppPolicyManager::IsPreventCloseEnabled(
const webapps::AppId& app_id) const {
#if BUILDFLAG(IS_CHROMEOS)
if (!base::FeatureList::IsEnabled(features::kDesktopPWAsRunOnOsLogin) ||
!base::FeatureList::IsEnabled(features::kDesktopPWAsPreventClose)) {
return false;
}
if (!provider_->registrar_unsafe().IsInstalledByPolicy(app_id)) {
return false;
}
const webapps::ManifestId manifest_id =
provider_->registrar_unsafe().GetComputedManifestId(app_id);
auto it = settings_by_url_.find(manifest_id.spec());
if (it != settings_by_url_.end()) {
return it->second.prevent_close;
}
// `default_settings_` must be ignored for prevent close feature. Only app
// specific value is applied.
return false;
#else
return false;
#endif // BUILDFLAG(IS_CHROMEOS)
}
void WebAppPolicyManager::RefreshPolicyInstalledAppsForTesting(
bool allow_close_and_relaunch) {
RefreshPolicyInstalledApps(allow_close_and_relaunch);
}
void WebAppPolicyManager::OnAppsSynchronized(
std::map<GURL, ExternallyManagedAppManager::InstallResult> install_results,
std::map<GURL, webapps::UninstallResultCode> uninstall_results) {
is_refreshing_ = false;
if (!install_results.empty())
ApplyPolicySettings();
if (needs_refresh_)
RefreshPolicyInstalledApps();
for (const auto& url_and_result : install_results) {
base::UmaHistogramEnumeration(kInstallResultHistogramName,
url_and_result.second.code);
}
OnWebAppForceInstallPolicyParsed();
}
bool WebAppPolicyManager::WebAppSetting::Parse(const base::Value::Dict& dict,
bool for_default_settings) {
const std::string* run_on_os_login_str = dict.FindString(kRunOnOsLogin);
if (run_on_os_login_str) {
if (*run_on_os_login_str == kAllowed) {
run_on_os_login_policy = RunOnOsLoginPolicy::kAllowed;
} else if (*run_on_os_login_str == kBlocked) {
run_on_os_login_policy = RunOnOsLoginPolicy::kBlocked;
} else if (!for_default_settings && *run_on_os_login_str == kRunWindowed) {
run_on_os_login_policy = RunOnOsLoginPolicy::kRunWindowed;
} else {
SYSLOG(WARNING) << "Malformed web app run on os login preference.";
return false;
}
}
// The value of "prevent_close" shall only be considered for non-default
// settings if run-on-os-login is enforced.
if (!for_default_settings &&
run_on_os_login_policy == RunOnOsLoginPolicy::kRunWindowed) {
prevent_close = dict.FindBool(kPreventClose).value_or(false);
}
if (IsForceUnregistrationPolicyEnabled()) {
std::optional<bool> force_unregistration_value =
dict.FindBool(kForceUnregisterOsIntegration);
force_unregister_os_integration =
force_unregistration_value.value_or(false);
}
return true;
}
WebAppPolicyManager::CustomManifestValues::CustomManifestValues() = default;
WebAppPolicyManager::CustomManifestValues::CustomManifestValues(
const WebAppPolicyManager::CustomManifestValues&) = default;
WebAppPolicyManager::CustomManifestValues::~CustomManifestValues() = default;
void WebAppPolicyManager::CustomManifestValues::SetName(
const std::string& utf8_name) {
name = base::UTF8ToUTF16(utf8_name);
}
void WebAppPolicyManager::CustomManifestValues::SetIcon(const GURL& icon_gurl) {
blink::Manifest::ImageResource icon;
icon.src = GURL(icon_gurl);
icon.sizes.emplace_back(0, 0); // Represents size "any".
icon.purpose.push_back(blink::mojom::ManifestImageResource::Purpose::ANY);
// Initialize icons to only contain icon, possibly resetting icons:
icons.emplace(1, icon);
}
void WebAppPolicyManager::ObserveDisabledSystemFeaturesPolicy() {
#if BUILDFLAG(IS_CHROMEOS)
PrefService* const local_state = g_browser_process->local_state();
if (!local_state) { // Sometimes it's not available in tests.
return;
}
local_state_pref_change_registrar_.Init(local_state);
local_state_pref_change_registrar_.Add(
policy::policy_prefs::kSystemFeaturesDisableList,
base::BindRepeating(&WebAppPolicyManager::OnDisableListPolicyChanged,
base::Unretained(this)));
local_state_pref_change_registrar_.Add(
policy::policy_prefs::kSystemFeaturesDisableMode,
base::BindRepeating(&WebAppPolicyManager::OnDisableModePolicyChanged,
base::Unretained(this)));
if (ash::features::IsGraduationEnabled()) {
pref_change_registrar_.Add(
ash::prefs::kGraduationEnablementStatus,
base::BindRepeating(&WebAppPolicyManager::OnDisableListPolicyChanged,
weak_ptr_factory_.GetWeakPtr()));
}
pref_change_registrar_.Add(
ash::prefs::kClassManagementToolsAvailabilitySetting,
base::BindRepeating(&WebAppPolicyManager::OnDisableListPolicyChanged,
weak_ptr_factory_.GetWeakPtr()));
// Make sure we get the right disabled mode in case it was changed before
// policy registration.
OnDisableModePolicyChanged();
#endif // BUILDFLAG(IS_CHROMEOS)
}
void WebAppPolicyManager::OnDisableModePolicyChanged() {
#if BUILDFLAG(IS_CHROMEOS)
provider_->sync_bridge_unsafe().UpdateAppsDisableMode();
#endif // BUILDFLAG(IS_CHROMEOS)
}
void WebAppPolicyManager::PopulateDisabledWebAppsIdsLists() {
disabled_web_apps_.clear();
#if BUILDFLAG(IS_CHROMEOS)
disabled_system_apps_.clear();
disabled_system_apps_not_hidden_.clear();
if (ash::features::IsGraduationEnabled() &&
!ash::graduation::IsEligibleForGraduation(pref_service_)) {
disabled_system_apps_.insert(ash::SystemWebAppType::GRADUATION);
disabled_system_apps_not_hidden_.insert(ash::SystemWebAppType::GRADUATION);
}
if (!ash::features::IsBocaEnabled() &&
pref_service_->GetString(
ash::prefs::kClassManagementToolsAvailabilitySetting) == kDisabled) {
disabled_system_apps_.insert(ash::SystemWebAppType::BOCA);
disabled_system_apps_not_hidden_.insert(ash::SystemWebAppType::BOCA);
}
PrefService* const local_state = g_browser_process->local_state();
if (!local_state) // Sometimes it's not available in tests.
return;
const base::Value::List& disabled_system_features_pref =
local_state->GetList(policy::policy_prefs::kSystemFeaturesDisableList);
for (const auto& entry : disabled_system_features_pref) {
switch (static_cast<policy::SystemFeature>(entry.GetInt())) {
case policy::SystemFeature::kCanvas:
disabled_web_apps_.insert(ash::kCanvasAppId);
break;
case policy::SystemFeature::kCamera:
disabled_system_apps_.insert(ash::SystemWebAppType::CAMERA);
break;
case policy::SystemFeature::kOsSettings:
disabled_system_apps_.insert(ash::SystemWebAppType::SETTINGS);
break;
case policy::SystemFeature::kScanning:
disabled_system_apps_.insert(ash::SystemWebAppType::SCANNING);
break;
case policy::SystemFeature::kExplore:
disabled_system_apps_.insert(ash::SystemWebAppType::HELP);
break;
case policy::SystemFeature::kCrosh:
disabled_system_apps_.insert(ash::SystemWebAppType::CROSH);
break;
case policy::SystemFeature::kTerminal:
disabled_system_apps_.insert(ash::SystemWebAppType::TERMINAL);
break;
case policy::SystemFeature::kGallery:
disabled_system_apps_.insert(ash::SystemWebAppType::MEDIA);
break;
case policy::SystemFeature::kPrintJobs:
disabled_system_apps_.insert(ash::SystemWebAppType::PRINT_MANAGEMENT);
break;
case policy::SystemFeature::kKeyShortcuts:
disabled_system_apps_.insert(
ash::SystemWebAppType::SHORTCUT_CUSTOMIZATION);
break;
case policy::SystemFeature::kRecorder:
disabled_system_apps_.insert(ash::SystemWebAppType::RECORDER);
break;
case policy::SystemFeature::kGmail:
disabled_web_apps_.insert(ash::kGmailAppId);
break;
case policy::SystemFeature::kGoogleDocs:
disabled_web_apps_.insert(ash::kGoogleDocsAppId);
break;
case policy::SystemFeature::kGoogleSlides:
disabled_web_apps_.insert(ash::kGoogleSlidesAppId);
break;
case policy::SystemFeature::kGoogleSheets:
disabled_web_apps_.insert(ash::kGoogleSheetsAppId);
break;
case policy::SystemFeature::kGoogleDrive:
disabled_web_apps_.insert(ash::kGoogleDriveAppId);
break;
case policy::SystemFeature::kGoogleKeep:
disabled_web_apps_.insert(ash::kGoogleKeepAppId);
break;
case policy::SystemFeature::kGoogleCalendar:
disabled_web_apps_.insert(ash::kGoogleCalendarAppId);
break;
case policy::SystemFeature::kGoogleChat:
disabled_web_apps_.insert(ash::kGoogleChatAppId);
break;
case policy::SystemFeature::kYoutube:
disabled_web_apps_.insert(ash::kYoutubeAppId);
break;
case policy::SystemFeature::kGoogleMaps:
disabled_web_apps_.insert(ash::kGoogleMapsAppId);
break;
case policy::SystemFeature::kCalculator:
disabled_web_apps_.insert(ash::kCalculatorAppId);
break;
case policy::SystemFeature::kUnknownSystemFeature:
case policy::SystemFeature::kBrowserSettings:
case policy::SystemFeature::kWebStore:
case policy::SystemFeature::kTextEditor:
case policy::SystemFeature::kGoogleNewsDeprecated:
break;
}
}
DCHECK(system_web_apps_delegate_map_);
// TODO(413343732): Remove/fix - IDs are not (always) resolved when this
// function runs.
for (const ash::SystemWebAppType& app_type : disabled_system_apps_) {
std::optional<webapps::AppId> app_id =
GetAppIdForSystemApp(provider_->registrar_unsafe(),
*system_web_apps_delegate_map_, app_type);
if (app_id.has_value()) {
disabled_web_apps_.insert(app_id.value());
}
}
#endif // BUILDFLAG(IS_CHROMEOS)
}
void WebAppPolicyManager::OnWebAppForceInstallPolicyParsed() {
if (on_apps_synchronized_for_testing_) {
std::move(on_apps_synchronized_for_testing_).Run();
}
// Policy settings have already been applied, as that happens synchronously
// before force-installs are applied.
if (policy_settings_and_force_installs_applied_) {
std::move(policy_settings_and_force_installs_applied_).Run();
}
}
} // namespace web_app