#include "chrome/browser/chromeos/app_mode/chrome_kiosk_app_installer.h"
#include <algorithm>
#include <iterator>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/check_deref.h"
#include "base/check_op.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/syslog_logging.h"
#include "chrome/browser/chromeos/app_mode/chrome_kiosk_external_loader_broker.h"
#include "chrome/browser/chromeos/app_mode/startup_app_launcher_update_checker.h"
#include "chrome/browser/extensions/forced_extensions/install_stage_tracker.h"
#include "chrome/browser/extensions/install_tracker_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_context.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/pending_extension_manager.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_handlers/kiosk_mode_info.h"
#include "extensions/common/manifest_handlers/shared_module_info.h"
namespace chromeos {
namespace {
const std::string_view kChromeKioskExtensionUpdateErrorHistogram =
"Kiosk.ChromeApp.ExtensionUpdateError";
const std::string_view kChromeKioskExtensionHasUpdateDurationHistogram =
"Kiosk.ChromeApp.ExtensionUpdateDuration.HasUpdate";
const std::string_view kChromeKioskExtensionNoUpdateDurationHistogram =
"Kiosk.ChromeApp.ExtensionUpdateDuration.NoUpdate";
bool IsExtensionInstallPending(content::BrowserContext& browser_context,
const std::string& id) {
return extensions::PendingExtensionManager::Get(&browser_context)
->IsIdPending(id);
}
const extensions::Extension* FindInstalledExtension(
content::BrowserContext& browser_context,
const extensions::ExtensionId& id) {
return extensions::ExtensionRegistry::Get(&browser_context)
->GetInstalledExtension(id);
}
bool IsExtensionInstalled(content::BrowserContext& browser_context,
const extensions::ExtensionId& id) {
return FindInstalledExtension(browser_context, id) != nullptr;
}
bool AreExtensionsInstalled(content::BrowserContext& browser_context,
const std::vector<extensions::ExtensionId>& ids) {
return std::ranges::all_of(ids.begin(), ids.end(),
[&browser_context](const auto& id) {
return IsExtensionInstalled(browser_context, id);
});
}
bool SecondaryAppsContain(const extensions::Extension& app,
const extensions::ExtensionId& id) {
if (auto* info = extensions::KioskModeInfo::Get(&app); info != nullptr) {
auto it = std::ranges::find(info->secondary_apps, id,
[](const auto& app) { return app.id; });
return it != info->secondary_apps.end();
}
return false;
}
std::vector<extensions::ExtensionId> SecondaryAppIdsOf(
const extensions::Extension& app) {
std::vector<extensions::ExtensionId> result;
if (auto* info = extensions::KioskModeInfo::Get(&app); info != nullptr) {
std::ranges::transform(info->secondary_apps, std::back_inserter(result),
[](const auto& app) { return app.id; });
}
return result;
}
std::vector<extensions::ExtensionId> CopyIdsPendingInstall(
content::BrowserContext& browser_context,
const std::vector<extensions::ExtensionId>& app_ids) {
std::vector<extensions::ExtensionId> result;
std::ranges::copy_if(app_ids, std::back_inserter(result),
[&browser_context](const auto& id) {
return IsExtensionInstallPending(browser_context, id);
});
return result;
}
void InsertPendingSharedModules(content::BrowserContext& browser_context,
base::flat_set<extensions::ExtensionId>& ids,
const extensions::Extension& extension) {
const auto& imports = extensions::SharedModuleInfo::GetImports(&extension);
for (const auto& import_info : imports) {
if (IsExtensionInstallPending(browser_context, import_info.extension_id)) {
ids.insert(import_info.extension_id);
}
}
}
}
ChromeKioskAppInstaller::ChromeKioskAppInstaller(
Profile* profile,
const AppInstallParams& install_data)
: profile_(CHECK_DEREF(profile)), primary_app_install_data_(install_data) {}
ChromeKioskAppInstaller::~ChromeKioskAppInstaller() = default;
void ChromeKioskAppInstaller::BeginInstall(InstallCallback callback) {
DCHECK(!install_complete_);
SYSLOG(INFO) << "BeginInstall primary app id: " << primary_app_id();
on_ready_callback_ = std::move(callback);
extensions::file_util::SetUseSafeInstallation(true);
const auto* primary_app =
FindInstalledExtension(profile_.get(), primary_app_id());
if (primary_app_install_data_.crx_file_location.empty() &&
primary_app == nullptr) {
ReportInstallFailure(InstallResult::kPrimaryAppNotCached);
return;
}
ChromeKioskExternalLoaderBroker::Get()->TriggerPrimaryAppInstall(
primary_app_install_data_);
if (IsExtensionInstallPending(profile_.get(), primary_app_id())) {
ObserveInstallations({primary_app_id()});
return;
}
if (primary_app == nullptr) {
ReportInstallFailure(InstallResult::kPrimaryAppInstallFailed);
return;
}
if (!extensions::KioskModeInfo::IsKioskEnabled(primary_app)) {
ReportInstallFailure(InstallResult::kPrimaryAppNotKioskEnabled);
return;
}
MaybeInstallSecondaryApps(*primary_app);
}
void ChromeKioskAppInstaller::MaybeInstallSecondaryApps(
const extensions::Extension& primary_app) {
if (install_complete_) {
return;
}
secondary_apps_installing_ = true;
auto secondary_app_ids = SecondaryAppIdsOf(primary_app);
ChromeKioskExternalLoaderBroker::Get()->UpdateSecondaryAppList(
secondary_app_ids);
if (!secondary_app_ids.empty()) {
auto pending_ids = CopyIdsPendingInstall(profile_.get(), secondary_app_ids);
if (!pending_ids.empty()) {
ObserveInstallations(pending_ids);
return;
}
}
if (!AreExtensionsInstalled(profile_.get(), secondary_app_ids)) {
ReportInstallFailure(InstallResult::kSecondaryAppInstallFailed);
return;
}
MaybeCheckExtensionUpdate();
}
void ChromeKioskAppInstaller::MaybeCheckExtensionUpdate() {
DCHECK(!install_complete_);
SYSLOG(INFO) << "MaybeCheckExtensionUpdate";
extension_update_start_time_ = base::Time::Now();
install_stage_observation_.Observe(
extensions::InstallStageTracker::Get(&profile_.get()));
update_checker_ =
std::make_unique<StartupAppLauncherUpdateChecker>(&profile_.get());
if (!update_checker_->Run(base::BindOnce(
&ChromeKioskAppInstaller::OnExtensionUpdateCheckFinished,
weak_ptr_factory_.GetWeakPtr()))) {
update_checker_.reset();
install_stage_observation_.Reset();
SYSLOG(WARNING) << "Could not check extension updates";
FinalizeAppInstall();
return;
}
SYSLOG(INFO) << "Checking extension updates";
}
void ChromeKioskAppInstaller::OnExtensionUpdateCheckFinished(
bool update_found) {
DCHECK(!install_complete_);
SYSLOG(INFO) << "OnExtensionUpdateCheckFinished";
update_checker_.reset();
install_stage_observation_.Reset();
if (update_found) {
SYSLOG(INFO) << "Reloading extension with id " << primary_app_id();
extensions::ExtensionRegistrar::Get(&profile_.get())
->ReloadExtension(primary_app_id());
SYSLOG(INFO) << "Reloaded extension with id " << primary_app_id();
}
base::UmaHistogramMediumTimes(
update_found ? kChromeKioskExtensionHasUpdateDurationHistogram
: kChromeKioskExtensionNoUpdateDurationHistogram,
base::Time::Now() - extension_update_start_time_);
FinalizeAppInstall();
}
void ChromeKioskAppInstaller::FinalizeAppInstall() {
DCHECK(!install_complete_);
install_complete_ = true;
if (primary_app_update_failed_) {
ReportInstallFailure(
ChromeKioskAppInstaller::InstallResult::kPrimaryAppUpdateFailed);
} else if (secondary_app_update_failed_) {
ReportInstallFailure(
ChromeKioskAppInstaller::InstallResult::kSecondaryAppUpdateFailed);
} else {
ReportInstallSuccess();
}
}
void ChromeKioskAppInstaller::OnFinishCrxInstall(
content::BrowserContext* context,
const base::FilePath& source_file,
const std::string& extension_id,
const extensions::Extension* extension,
bool success) {
DCHECK(!install_complete_);
SYSLOG(INFO) << (success ? "OnFinishCrxInstall succeeded for id: "
: "OnFinishCrxInstall failed for id: ")
<< extension_id;
if (!waiting_ids_.contains(extension_id)) {
return;
}
waiting_ids_.erase(extension_id);
if (extension != nullptr) {
InsertPendingSharedModules(profile_.get(), waiting_ids_, *extension);
}
const auto* primary_app =
FindInstalledExtension(profile_.get(), primary_app_id());
if (!success) {
if (primary_app == nullptr && extension_id == primary_app_id()) {
install_observation_.Reset();
ReportInstallFailure(InstallResult::kPrimaryAppInstallFailed);
return;
}
if (primary_app != nullptr &&
SecondaryAppsContain(*primary_app, extension_id) &&
!IsExtensionInstalled(profile_.get(), extension_id)) {
install_observation_.Reset();
ReportInstallFailure(InstallResult::kSecondaryAppInstallFailed);
return;
}
if (primary_app != nullptr && extension_id == primary_app_id()) {
primary_app_update_failed_ = true;
}
if (primary_app != nullptr &&
SecondaryAppsContain(*primary_app, extension_id) &&
IsExtensionInstalled(profile_.get(), extension_id)) {
secondary_app_update_failed_ = true;
}
}
if (!waiting_ids_.empty()) {
return;
}
install_observation_.Reset();
if (primary_app == nullptr) {
ReportInstallFailure(InstallResult::kPrimaryAppInstallFailed);
return;
}
if (!extensions::KioskModeInfo::IsKioskEnabled(primary_app)) {
ReportInstallFailure(InstallResult::kPrimaryAppNotKioskEnabled);
return;
}
if (!secondary_apps_installing_) {
MaybeInstallSecondaryApps(*primary_app);
} else {
MaybeCheckExtensionUpdate();
}
}
void ChromeKioskAppInstaller::OnExtensionInstallationFailed(
const extensions::ExtensionId& id,
extensions::InstallStageTracker::FailureReason reason) {
base::UmaHistogramEnumeration(kChromeKioskExtensionUpdateErrorHistogram,
reason);
}
void ChromeKioskAppInstaller::ReportInstallSuccess() {
DCHECK(install_complete_);
SYSLOG(INFO) << "Kiosk app install succeeded";
std::move(on_ready_callback_)
.Run(ChromeKioskAppInstaller::InstallResult::kSuccess);
}
void ChromeKioskAppInstaller::ReportInstallFailure(
ChromeKioskAppInstaller::InstallResult error) {
SYSLOG(ERROR) << "App install failed, error: " << static_cast<int>(error);
DCHECK_NE(ChromeKioskAppInstaller::InstallResult::kSuccess, error);
std::move(on_ready_callback_).Run(error);
}
void ChromeKioskAppInstaller::ObserveInstallations(
const std::vector<extensions::ExtensionId>& ids) {
waiting_ids_.insert(ids.begin(), ids.end());
install_observation_.Observe(
extensions::InstallTrackerFactory::GetForBrowserContext(&profile_.get()));
}
}