| // 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/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; |
| } |
| |
| const std::string* external_crx_str = |
| extension->FindStringKey(kExternalCrx); |
| if (!external_crx_str) { |
| LOG(ERROR) << "Can't find external crx in manifest " << app_id; |
| continue; |
| } |
| |
| const std::string* external_version_str = |
| extension->FindStringKey(kExternalVersion); |
| if (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 |