chromium/chrome/browser/ash/app_mode/kiosk_external_updater.cc

// Copyright 2014 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/ash/app_mode/kiosk_external_updater.h"

#include "base/files/file_util.h"
#include "base/functional/bind.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/sequenced_task_runner.h"
#include "base/version.h"
#include "chrome/browser/ash/app_mode/kiosk_chrome_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<base::Value, 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(base::Value(),
                          KioskExternalUpdater::ErrorCode::kNoManifest);
  }

  JSONFileValueDeserializer deserializer(manifest);
  std::unique_ptr<base::Value> extensions =
      deserializer.Deserialize(nullptr, nullptr);
  if (!extensions) {
    return std::make_pair(base::Value(),
                          KioskExternalUpdater::ErrorCode::kInvalidManifest);
  }

  return std::make_pair(base::Value::FromUniquePtrValue(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() = default;

KioskExternalUpdater::ExternalUpdate::ExternalUpdate(
    const ExternalUpdate& other) = default;

KioskExternalUpdater::ExternalUpdate::~ExternalUpdate() = default;

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::MountPoint& mount_info) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (mount_info.mount_type != MountType::kDevice ||
      error_code != MountError::kSuccess) {
    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;
    }

    backend_task_runner_->PostTaskAndReplyWithResult(
        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());
  backend_task_runner_->PostTaskAndReplyWithResult(
      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 base::Value& parsed_manifest = result.first;
  ErrorCode parsing_error = result.second;
  if (parsing_error == ErrorCode::kNoManifest) {
    KioskChromeAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
    return;
  }
  if (parsing_error == ErrorCode::kInvalidManifest) {
    NotifyKioskUpdateProgress(
        ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
            IDS_KIOSK_EXTERNAL_UPDATE_INVALID_MANIFEST));
    KioskChromeAppManager::Get()->OnKioskAppExternalUpdateComplete(false);
    return;
  }

  NotifyKioskUpdateProgress(
      ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
          IDS_KIOSK_EXTERNAL_UPDATE_IN_PROGRESS));

  external_update_path_ = external_update_dir;
  for (auto manifest : parsed_manifest.GetDict()) {
    std::string app_id = manifest.first;
    std::string cached_version_str;
    base::FilePath cached_crx;
    if (!KioskChromeAppManager::Get()->GetCachedCrx(app_id, &cached_crx,
                                                    &cached_version_str)) {
      LOG(WARNING) << "Can't find app in existing cache " << app_id;
      continue;
    }

    if (!manifest.second.is_dict()) {
      LOG(ERROR) << "Found bad entry in manifest type "
                 << manifest.second.type();
      continue;
    }
    const base::Value::Dict& extension = manifest.second.GetDict();

    const std::string* external_crx_str = extension.FindString(kExternalCrx);
    if (!external_crx_str) {
      LOG(ERROR) << "Can't find external crx in manifest " << app_id;
      continue;
    }

    const std::string* external_version_str =
        extension.FindString(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;
    KioskChromeAppManager::App app;
    if (KioskChromeAppManager::Get()->GetApp(app_id, &app)) {
      update.app_name = app.name;
    } else {
      NOTREACHED_IN_MIGRATION();
    }
    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));
    KioskChromeAppManager::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 = KioskChromeAppManager::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, std::string(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;
  }

  KioskChromeAppManager::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();
  KioskChromeAppManager::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) {
      KioskChromeAppManager::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