910e62b5创建于 1月15日历史提交
// Copyright 2021 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/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";

// Returns true if the app with `id` is pending an install or update.
bool IsExtensionInstallPending(content::BrowserContext& browser_context,
                               const std::string& id) {
  return extensions::PendingExtensionManager::Get(&browser_context)
      ->IsIdPending(id);
}

// Returns the `Extension` corresponding to `id` installed in `browser_context`,
// or null if the extension is not installed.
const extensions::Extension* FindInstalledExtension(
    content::BrowserContext& browser_context,
    const extensions::ExtensionId& id) {
  return extensions::ExtensionRegistry::Get(&browser_context)
      ->GetInstalledExtension(id);
}

// Returns true if the extension with `id` is installed in `browser_context`.
bool IsExtensionInstalled(content::BrowserContext& browser_context,
                          const extensions::ExtensionId& id) {
  return FindInstalledExtension(browser_context, id) != nullptr;
}

// Returns true if all extensions in `ids` are installed in `browser_context`.
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);
                             });
}

// Returns true if the secondary apps of `app` contain the given `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;
}

// Returns the IDs of the secondary apps of `app`.
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;
}

// Returns the subset of `app_ids` that is pending install or update.
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;
}

// Inserts the shared modules `extension` imports into the set of `ids` that are
// pending install.
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);
    }
  }
}

}  // namespace

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) {
    // The extension is skipped for installation due to some error.
    ReportInstallFailure(InstallResult::kPrimaryAppInstallFailed);
    return;
  }

  if (!extensions::KioskModeInfo::IsKioskEnabled(primary_app)) {
    // The installed primary app is not kiosk enabled.
    ReportInstallFailure(InstallResult::kPrimaryAppNotKioskEnabled);
    return;
  }

  // Install secondary apps.
  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;
  }

  // Check extension update before launching the primary kiosk app.
  MaybeCheckExtensionUpdate();
}

void ChromeKioskAppInstaller::MaybeCheckExtensionUpdate() {
  DCHECK(!install_complete_);

  SYSLOG(INFO) << "MaybeCheckExtensionUpdate";

  // Record update start time to calculate time consumed by update check. When
  // `OnExtensionUpdateCheckFinished` is called the update is already finished
  // because `extensions::ExtensionUpdater::CheckParams::install_immediately` is
  // set to true.
  extension_update_start_time_ = base::Time::Now();

  // Observe installation failures.
  install_stage_observation_.Observe(
      extensions::InstallStageTracker::Get(&profile_.get()));

  // Enforce an immediate version update check for all extensions before
  // launching the primary app. After the chromeos is updated, the shared
  // module (e.g. ARC runtime) may need to be updated to a newer version
  // compatible with the new chromeos. See crbug.com/555083.
  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();

    // Reload the primary app to make sure any reference to the previous version
    // of the shared module, extension, etc will be cleaned up and the new
    // version will be loaded.
    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;

  // Exit early if this is not one of the IDs we care about.
  if (!waiting_ids_.contains(extension_id)) {
    return;
  }
  waiting_ids_.erase(extension_id);

  // Also wait for updates on any shared modules the extension imports.
  if (extension != nullptr) {
    InsertPendingSharedModules(profile_.get(), waiting_ids_, *extension);
  }

  const auto* primary_app =
      FindInstalledExtension(profile_.get(), primary_app_id());

  if (!success) {
    // Primary or secondary app install failed. Abort and report the failure.
    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;
    }
    // Primary or secondary app update failed, but there is an installed version
    // in the `profile_`. Proceed for now and report the update failure later.
    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;
    }
  }

  // Wait for `OnFinishCrxInstall` to be called for remaining `waiting_ids_`.
  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()));
}

}  // namespace chromeos