blob: b598de4494530286d968e0a8a851fd6f2dbf2332 [file] [log] [blame]
// Copyright 2014 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/ash/app_mode/kiosk_external_updater.h"
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/json/json_file_value_serializer.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task_runner_util.h"
#include "base/version.h"
#include "chrome/browser/ash/app_mode/kiosk_app_manager.h"
#include "chrome/browser/ash/notifications/kiosk_external_update_notification.h"
#include "chrome/grit/generated_resources.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/sandboxed_unpacker.h"
#include "extensions/common/extension.h"
#include "extensions/common/verifier_formats.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
namespace ash {
namespace {
constexpr base::FilePath::CharType kExternalUpdateManifest[] =
"external_update.json";
constexpr char kExternalCrx[] = "external_crx";
constexpr char kExternalVersion[] = "external_version";
std::pair<std::unique_ptr<base::DictionaryValue>,
KioskExternalUpdater::ErrorCode>
ParseExternalUpdateManifest(const base::FilePath& external_update_dir) {
base::FilePath manifest = external_update_dir.Append(kExternalUpdateManifest);
if (!base::PathExists(manifest)) {
return std::make_pair(nullptr,
KioskExternalUpdater::ErrorCode::kNoManifest);
}
JSONFileValueDeserializer deserializer(manifest);
std::unique_ptr<base::DictionaryValue> extensions =
base::DictionaryValue::From(deserializer.Deserialize(nullptr, nullptr));
if (!extensions) {
return std::make_pair(nullptr,
KioskExternalUpdater::ErrorCode::kInvalidManifest);
}
return std::make_pair(std::move(extensions),
KioskExternalUpdater::ErrorCode::kNone);
}
// Copies |external_crx_file| to |temp_crx_file|, and removes |temp_dir|
// created for unpacking |external_crx_file|.
bool CopyExternalCrxAndDeleteTempDir(const base::FilePath& external_crx_file,
const base::FilePath& temp_crx_file,
const base::FilePath& temp_dir) {
base::DeletePathRecursively(temp_dir);
return base::CopyFile(external_crx_file, temp_crx_file);
}
// Returns true if |version_1| < |version_2|, and
// if |update_for_same_version| is true and |version_1| = |version_2|.
bool ShouldUpdateForHigherVersion(const std::string& version_1,
const std::string& version_2,
bool update_for_same_version) {
const base::Version v1(version_1);
const base::Version v2(version_2);
if (!v1.IsValid() || !v2.IsValid())
return false;
int compare_result = v1.CompareTo(v2);
if (compare_result < 0)
return true;
return update_for_same_version && compare_result == 0;
}
} // namespace
KioskExternalUpdater::ExternalUpdate::ExternalUpdate() {
}
KioskExternalUpdater::ExternalUpdate::ExternalUpdate(
const ExternalUpdate& other) = default;
KioskExternalUpdater::ExternalUpdate::~ExternalUpdate() {
}
KioskExternalUpdater::KioskExternalUpdater(
const scoped_refptr<base::SequencedTaskRunner>& backend_task_runner,
const base::FilePath& crx_cache_dir,
const base::FilePath& crx_unpack_dir)
: backend_task_runner_(backend_task_runner),
crx_cache_dir_(crx_cache_dir),
crx_unpack_dir_(crx_unpack_dir) {
// Subscribe to DiskMountManager.
DCHECK(disks::DiskMountManager::GetInstance());
disks::DiskMountManager::GetInstance()->AddObserver(this);
}
KioskExternalUpdater::~KioskExternalUpdater() {
if (disks::DiskMountManager::GetInstance())
disks::DiskMountManager::GetInstance()->RemoveObserver(this);
}
void KioskExternalUpdater::OnMountEvent(
disks::DiskMountManager::MountEvent event,
MountError error_code,
const disks::DiskMountManager::MountPointInfo& mount_info) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (mount_info.mount_type != MountType::MOUNT_TYPE_DEVICE ||
error_code != MountError::MOUNT_ERROR_NONE) {
return;
}
if (event == disks::DiskMountManager::MOUNTING) {
// If multiple disks have been mounted, skip the rest of them if kiosk
// update has already been found.
if (!external_update_path_.empty()) {
LOG(WARNING) << "External update path already found, skip "
<< mount_info.mount_path;
return;
}
base::PostTaskAndReplyWithResult(
backend_task_runner_.get(), FROM_HERE,
base::BindOnce(&ParseExternalUpdateManifest,
base::FilePath(mount_info.mount_path)),
base::BindOnce(&KioskExternalUpdater::ProcessParsedManifest,
weak_factory_.GetWeakPtr(),
base::FilePath(mount_info.mount_path)));
return;
}
// unmounting a removable device case.
if (external_update_path_.value().empty()) {
// Clear any previously displayed message.
DismissKioskUpdateNotification();
} else if (external_update_path_.value() == mount_info.mount_path) {
DismissKioskUpdateNotification();
if (IsExternalUpdatePending()) {
LOG(ERROR) << "External kiosk update is not completed when the usb "
<< "stick is unmoutned.";
}
external_updates_.clear();
external_update_path_.clear();
}
}
void KioskExternalUpdater::OnExternalUpdateUnpackSuccess(
const std::string& app_id,
const std::string& version,
const std::string& min_browser_version,
const base::FilePath& temp_dir) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// User might pull out the usb stick before updating is completed.
if (CheckExternalUpdateInterrupted())
return;
if (!ShouldDoExternalUpdate(app_id, version, min_browser_version)) {
external_updates_[app_id].update_status = UpdateStatus::kFailed;
MaybeValidateNextExternalUpdate();
return;
}
// User might pull out the usb stick before updating is completed.
if (CheckExternalUpdateInterrupted())
return;
base::FilePath external_crx_path =
external_updates_[app_id].external_crx.path;
base::FilePath temp_crx_path =
crx_unpack_dir_.Append(external_crx_path.BaseName());
base::PostTaskAndReplyWithResult(
backend_task_runner_.get(), FROM_HERE,
base::BindOnce(&CopyExternalCrxAndDeleteTempDir, external_crx_path,
temp_crx_path, temp_dir),
base::BindOnce(&KioskExternalUpdater::PutValidatedExtension,
weak_factory_.GetWeakPtr(), app_id, temp_crx_path,
version));
}
void KioskExternalUpdater::OnExternalUpdateUnpackFailure(
const std::string& app_id) {
// User might pull out the usb stick before updating is completed.
if (CheckExternalUpdateInterrupted())
return;
external_updates_[app_id].update_status = UpdateStatus::kFailed;
external_updates_[app_id].error =
ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
IDS_KIOSK_EXTERNAL_UPDATE_BAD_CRX);
MaybeValidateNextExternalUpdate();
}
void KioskExternalUpdater::ProcessParsedManifest(
const base::FilePath& external_update_dir,
const ParseManifestResult& result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const std::unique_ptr<base::DictionaryValue>& parsed_manifest = result.first;
ErrorCode parsing_error = result.second;
if (parsing_error == ErrorCode::kNoManifest) {
KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
return;
}
if (parsing_error == ErrorCode::kInvalidManifest) {
NotifyKioskUpdateProgress(
ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
IDS_KIOSK_EXTERNAL_UPDATE_INVALID_MANIFEST));
KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
return;
}
NotifyKioskUpdateProgress(
ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
IDS_KIOSK_EXTERNAL_UPDATE_IN_PROGRESS));
external_update_path_ = external_update_dir;
for (base::DictionaryValue::Iterator it(*parsed_manifest); !it.IsAtEnd();
it.Advance()) {
std::string app_id = it.key();
std::string cached_version_str;
base::FilePath cached_crx;
if (!KioskAppManager::Get()->GetCachedCrx(
app_id, &cached_crx, &cached_version_str)) {
LOG(WARNING) << "Can't find app in existing cache " << app_id;
continue;
}
const base::DictionaryValue* extension = nullptr;
if (!it.value().GetAsDictionary(&extension)) {
LOG(ERROR) << "Found bad entry in manifest type " << it.value().type();
continue;
}
std::string external_crx_str;
if (!extension->GetString(kExternalCrx, &external_crx_str)) {
LOG(ERROR) << "Can't find external crx in manifest " << app_id;
continue;
}
std::string external_version_str;
if (extension->GetString(kExternalVersion, &external_version_str)) {
if (!ShouldUpdateForHigherVersion(
cached_version_str, external_version_str, false)) {
LOG(WARNING) << "External app " << app_id
<< " is at the same or lower version comparing to "
<< " the existing one.";
continue;
}
}
ExternalUpdate update;
KioskAppManager::App app;
if (KioskAppManager::Get()->GetApp(app_id, &app)) {
update.app_name = app.name;
} else {
NOTREACHED();
}
update.external_crx = extensions::CRXFileInfo(
external_update_path_.AppendASCII(external_crx_str),
extensions::GetExternalVerifierFormat());
update.external_crx.extension_id = app_id;
update.update_status = UpdateStatus::kPending;
external_updates_[app_id] = update;
}
if (external_updates_.empty()) {
NotifyKioskUpdateProgress(
ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
IDS_KIOSK_EXTERNAL_UPDATE_NO_UPDATES));
KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
return;
}
ValidateExternalUpdates();
}
bool KioskExternalUpdater::CheckExternalUpdateInterrupted() {
if (external_updates_.empty()) {
// This could happen if user pulls out the usb stick before the updating
// operation is completed.
LOG(ERROR) << "external_updates_ has been cleared before external "
<< "updating completes.";
return true;
}
return false;
}
void KioskExternalUpdater::ValidateExternalUpdates() {
for (const auto& it : external_updates_) {
const ExternalUpdate& update = it.second;
if (update.update_status == UpdateStatus::kPending) {
auto crx_validator = base::MakeRefCounted<KioskExternalUpdateValidator>(
backend_task_runner_, update.external_crx, crx_unpack_dir_,
weak_factory_.GetWeakPtr());
crx_validator->Start();
break;
}
}
}
bool KioskExternalUpdater::IsExternalUpdatePending() const {
for (const auto& it : external_updates_) {
if (it.second.update_status == UpdateStatus::kPending)
return true;
}
return false;
}
bool KioskExternalUpdater::IsAllExternalUpdatesSucceeded() const {
for (const auto& it : external_updates_) {
if (it.second.update_status != UpdateStatus::kSuccess)
return false;
}
return true;
}
bool KioskExternalUpdater::ShouldDoExternalUpdate(
const std::string& app_id,
const std::string& version,
const std::string& min_browser_version) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::string existing_version_str;
base::FilePath existing_path;
bool cached = KioskAppManager::Get()->GetCachedCrx(
app_id, &existing_path, &existing_version_str);
DCHECK(cached);
// Compare app version.
ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
if (!ShouldUpdateForHigherVersion(existing_version_str, version, false)) {
external_updates_[app_id].error = rb->GetLocalizedString(
IDS_KIOSK_EXTERNAL_UPDATE_SAME_OR_LOWER_APP_VERSION);
return false;
}
// Check minimum browser version.
if (!min_browser_version.empty() &&
!ShouldUpdateForHigherVersion(min_browser_version,
version_info::GetVersionNumber(), true)) {
external_updates_[app_id].error = l10n_util::GetStringFUTF16(
IDS_KIOSK_EXTERNAL_UPDATE_REQUIRE_HIGHER_BROWSER_VERSION,
base::UTF8ToUTF16(min_browser_version));
return false;
}
return true;
}
void KioskExternalUpdater::PutValidatedExtension(const std::string& app_id,
const base::FilePath& crx_file,
const std::string& version,
bool crx_copied) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (CheckExternalUpdateInterrupted())
return;
if (!crx_copied) {
LOG(ERROR) << "Cannot copy external crx file to " << crx_file.value();
external_updates_[app_id].update_status = UpdateStatus::kFailed;
external_updates_[app_id].error = l10n_util::GetStringFUTF16(
IDS_KIOSK_EXTERNAL_UPDATE_FAILED_COPY_CRX_TO_TEMP,
base::UTF8ToUTF16(crx_file.value()));
MaybeValidateNextExternalUpdate();
return;
}
KioskAppManager::Get()->PutValidatedExternalExtension(
app_id, crx_file, version,
base::BindOnce(&KioskExternalUpdater::OnPutValidatedExtension,
weak_factory_.GetWeakPtr()));
}
void KioskExternalUpdater::OnPutValidatedExtension(const std::string& app_id,
bool success) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (CheckExternalUpdateInterrupted())
return;
if (!success) {
external_updates_[app_id].update_status = UpdateStatus::kFailed;
external_updates_[app_id].error = l10n_util::GetStringFUTF16(
IDS_KIOSK_EXTERNAL_UPDATE_CANNOT_INSTALL_IN_LOCAL_CACHE,
base::UTF8ToUTF16(external_updates_[app_id].external_crx.path.value()));
} else {
external_updates_[app_id].update_status = UpdateStatus::kSuccess;
}
// Validate the next pending external update.
MaybeValidateNextExternalUpdate();
}
void KioskExternalUpdater::MaybeValidateNextExternalUpdate() {
if (IsExternalUpdatePending())
ValidateExternalUpdates();
else
MayBeNotifyKioskAppUpdate();
}
void KioskExternalUpdater::MayBeNotifyKioskAppUpdate() {
if (IsExternalUpdatePending())
return;
NotifyKioskUpdateProgress(GetUpdateReportMessage());
NotifyKioskAppUpdateAvailable();
KioskAppManager::Get()->OnKioskAppExternalUpdateComplete(
IsAllExternalUpdatesSucceeded());
}
void KioskExternalUpdater::NotifyKioskAppUpdateAvailable() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
for (const auto& it : external_updates_) {
if (it.second.update_status == UpdateStatus::kSuccess) {
KioskAppManager::Get()->OnKioskAppCacheUpdated(it.first);
}
}
}
void KioskExternalUpdater::NotifyKioskUpdateProgress(
const std::u16string& message) {
if (!notification_)
notification_ = std::make_unique<KioskExternalUpdateNotification>(message);
else
notification_->ShowMessage(message);
}
void KioskExternalUpdater::DismissKioskUpdateNotification() {
if (notification_.get()) {
notification_.reset();
}
}
std::u16string KioskExternalUpdater::GetUpdateReportMessage() const {
DCHECK(!IsExternalUpdatePending());
int updated = 0;
int failed = 0;
std::u16string updated_apps;
std::u16string failed_apps;
for (const auto& it : external_updates_) {
const ExternalUpdate& update = it.second;
std::u16string app_name = base::UTF8ToUTF16(update.app_name);
if (update.update_status == UpdateStatus::kSuccess) {
++updated;
if (updated_apps.empty())
updated_apps = app_name;
else
updated_apps += u", " + app_name;
} else { // UpdateStatus::kFailed
++failed;
if (failed_apps.empty()) {
failed_apps = app_name + u": " + update.error;
} else {
failed_apps += u"\n" + app_name + u": " + update.error;
}
}
}
std::u16string message =
ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
IDS_KIOSK_EXTERNAL_UPDATE_COMPLETE);
if (updated) {
std::u16string success_app_msg = l10n_util::GetStringFUTF16(
IDS_KIOSK_EXTERNAL_UPDATE_SUCCESSFUL_UPDATED_APPS, updated_apps);
message += u"\n" + success_app_msg;
}
if (failed) {
std::u16string failed_app_msg =
ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
IDS_KIOSK_EXTERNAL_UPDATE_FAILED_UPDATED_APPS) +
u"\n" + failed_apps;
message += u"\n" + failed_app_msg;
}
return message;
}
} // namespace ash