text: // Copyright 2019 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/downgrade/downgrade_manager.h"
  
  #include <algorithm>
  #include <iterator>
  #include <utility>
  
  #include "base/bind.h"
  #include "base/callback.h"
  #include "base/command_line.h"
  #include "base/enterprise_util.h"
  #include "base/feature_list.h"
  #include "base/files/file_enumerator.h"
  #include "base/files/file_path.h"
  #include "base/files/file_util.h"
  #include "base/logging.h"
  #include "base/metrics/histogram_functions.h"
  #include "base/optional.h"
  #include "base/strings/strcat.h"
  #include "base/strings/string_util.h"
  #include "base/syslog_logging.h"
  #include "base/task/post_task.h"
  #include "base/task/thread_pool.h"
  #include "base/version.h"
  #include "build/build_config.h"
  #include "chrome/browser/browser_features.h"
  #include "chrome/browser/browser_process.h"
  #include "chrome/browser/downgrade/downgrade_utils.h"
  #include "chrome/browser/downgrade/snapshot_manager.h"
  #include "chrome/browser/downgrade/user_data_downgrade.h"
  #include "chrome/common/channel_info.h"
  #include "chrome/common/chrome_switches.h"
  #include "chrome/common/pref_names.h"
  #include "components/enterprise/browser/controller/browser_dm_token_storage.h"
  #include "components/prefs/pref_service.h"
  #include "components/version_info/version_info.h"
  #include "components/version_info/version_info_values.h"
  #include "content/public/browser/browser_thread.h"
  
  #if defined(OS_WIN)
  #include "chrome/installer/util/install_util.h"
  #endif
  
  namespace downgrade {
  
  namespace {
  
  bool g_snapshots_enabled_for_testing = false;
  
  // Moves the contents of a User Data directory at |source| to |target|, with the
  // exception of files/directories that should be left behind for a full data
  // wipe. Returns no value if the target directory could not be created, or the
  // number of items that could not be moved.
  base::Optional<int> MoveUserData(const base::FilePath& source,
                                   const base::FilePath& target) {
    // Returns true to exclude a file.
    auto exclusion_predicate =
        base::BindRepeating([](const base::FilePath& name) -> bool {
          // TODO(ydago): Share constants instead of hardcoding values here.
          static constexpr base::FilePath::StringPieceType kFilesToKeep[] = {
              FILE_PATH_LITERAL("browsermetrics"),
              FILE_PATH_LITERAL("crashpad"),
              FILE_PATH_LITERAL("first run"),
              FILE_PATH_LITERAL("last version"),
              FILE_PATH_LITERAL("lockfile"),
              FILE_PATH_LITERAL("snapshots"),
              FILE_PATH_LITERAL("stability"),
          };
          // Don't try to move the dir into which everything is being moved.
          if (name.FinalExtension() == kDowngradeDeleteSuffix)
            return true;
          return std::find_if(std::begin(kFilesToKeep), std::end(kFilesToKeep),
                              [&name](const auto& keep) {
                                return base::EqualsCaseInsensitiveASCII(
                                    name.value(), keep);
                              }) != std::end(kFilesToKeep);
        });
    auto result = MoveContents(source, target, std::move(exclusion_predicate));
  
    // Move the Last Version file last so that any crash before this point results
    // in a retry on the next launch.
    if (!result ||
        !MoveWithoutFallback(source.Append(kDowngradeLastVersionFile),
                             target.Append(kDowngradeLastVersionFile))) {
      if (result)
        *result += 1;
      // Attempt to delete Last Version if all else failed so that Chrome does not
      // continually attempt to perform a migration.
      base::DeleteFile(source.Append(kDowngradeLastVersionFile));
      // Inform system administrators that things have gone awry.
      SYSLOG(ERROR) << "Failed to perform User Data migration following a Chrome "
                       "version downgrade. Chrome will run with User Data from a "
                       "higher version and may behave unpredictably.";
      // At this point, Chrome will relaunch with --user-data-migrated. This
      // switch suppresses downgrade processing, so that launch will go through
      // normal startup.
    }
    return result;
  }
  
  // Renames |disk_cache_dir| in its containing folder. If that fails, an attempt
  // is made to move its contents.
  void MoveCache(const base::FilePath& disk_cache_dir) {
    // A cache dir at the root of a volume is not supported.
    const base::FilePath parent = disk_cache_dir.DirName();
    if (parent == disk_cache_dir)
      return;
  
    // Move the cache within its parent directory from, for example, CacheDir
    // to CacheDir.CHROME_DELETE.
    const base::FilePath target =
        GetTempDirNameForDelete(parent, disk_cache_dir.BaseName());
  
    // A simple move succeeds in approx 2/3 of attempts.
    if (MoveWithoutFallback(disk_cache_dir, target))
      return;
  
    // The directory couldn't be moved whole-hog. Attempt a recursive move of its
    // contents. This succeeds in nearly all cases.
    MoveContents(disk_cache_dir, target, ExclusionPredicate());
  }
  
  // Deletes all subdirectories in |dir| named |name|*.CHROME_DELETE.
  void DeleteAllRenamedUserDirectories(const base::FilePath& dir,
                                       const base::FilePath& name) {
    base::FilePath::StringType pattern = base::StrCat(
        {name.value(), FILE_PATH_LITERAL("*"), kDowngradeDeleteSuffix});
    base::FileEnumerator enumerator(dir, false, base::FileEnumerator::DIRECTORIES,
                                    pattern);
    for (base::FilePath to_delete = enumerator.Next(); !to_delete.empty();
         to_delete = enumerator.Next()) {
      base::DeletePathRecursively(to_delete);
    }
  }
  
  // Deletes all moved User Data, Snapshots and Cache directories for the given
  // dirs.
  void DeleteMovedUserData(const base::FilePath& user_data_dir,
                           const base::FilePath& disk_cache_dir) {
    DeleteAllRenamedUserDirectories(user_data_dir, user_data_dir.BaseName());
    DeleteAllRenamedUserDirectories(user_data_dir, base::FilePath(kSnapshotsDir));
  
    // Prior to Chrome M78, User Data was moved to a new name under its parent. In
    // that case, User Data at a volume's root was unsupported.
    base::FilePath parent = user_data_dir.DirName();
    if (parent != user_data_dir)
      DeleteAllRenamedUserDirectories(parent, user_data_dir.BaseName());
  
    if (!disk_cache_dir.empty()) {
      // Cache dir at a volume's root is unsupported.
      parent = disk_cache_dir.DirName();
      if (parent != disk_cache_dir)
        DeleteAllRenamedUserDirectories(parent, disk_cache_dir.BaseName());
    }
  }
  
  bool UserDataSnapshotEnabled() {
    if (g_snapshots_enabled_for_testing)
      return true;
    bool is_enterprise_managed =
  #if defined(OS_WIN) || defined(OS_MAC)
        base::IsMachineExternallyManaged() ||
  #endif
        policy::BrowserDMTokenStorage::Get()->RetrieveDMToken().is_valid();
    return is_enterprise_managed &&
           base::FeatureList::IsEnabled(features::kUserDataSnapshot);
  }
  
  #if defined(OS_WIN)
  bool IsAdministratorDrivenDowngrade(uint16_t current_milestone) {
    const auto downgrade_version = InstallUtil::GetDowngradeVersion();
    return downgrade_version &&
           downgrade_version->components()[0] > current_milestone;
  }
  #endif
  
  }  // namespace
  
  bool DowngradeManager::PrepareUserDataDirectoryForCurrentVersion(
      const base::FilePath& user_data_dir) {
    DCHECK_EQ(type_, Type::kNone);
    DCHECK(!user_data_dir.empty());
  
    // Do not attempt migration if this process is the product of a relaunch from
    // a previous in which migration was attempted/performed.
    auto& command_line = *base::CommandLine::ForCurrentProcess();
    if (command_line.HasSwitch(switches::kUserDataMigrated)) {
      // Strip the switch from the command line so that it does not propagate to
      // any subsequent relaunches.
      command_line.RemoveSwitch(switches::kUserDataMigrated);
      return false;
    }
  
    base::Optional<base::Version> last_version = GetLastVersion(user_data_dir);
    if (!last_version)
      return false;
  
    const base::Version& current_version = version_info::GetVersion();
  
    const bool user_data_snapshot_enabled = UserDataSnapshotEnabled();
  
    if (!user_data_snapshot_enabled) {
      if (current_version >= *last_version)
        return false;  // Same version or upgrade.
  
      type_ = GetDowngradeType(user_data_dir, current_version, *last_version);
      DCHECK(type_ == Type::kAdministrativeWipe || type_ == Type::kUnsupported);
      base::UmaHistogramEnumeration("Downgrade.Type", type_);
      return type_ == Type::kAdministrativeWipe;
    }
  
    if (current_version == *last_version)
      return false;  // Nothing to do if the version has not changed.
  
    if (current_version < *last_version) {
      type_ = GetDowngradeTypeWithSnapshot(user_data_dir, current_version,
                                           *last_version);
      if (type_ != Type::kNone)
        base::UmaHistogramEnumeration("Downgrade.Type", type_);
  
      return type_ == Type::kAdministrativeWipe ||
             type_ == Type::kSnapshotRestore;
    }
  
    auto current_milestone = current_version.components()[0];
    int max_number_of_snapshots = g_browser_process->local_state()->GetInteger(
        prefs::kUserDataSnapshotRetentionLimit);
    base::Optional<uint32_t> purge_milestone;
    if (current_milestone == last_version->components()[0]) {
      // Mid-milestone snapshots are only taken on canary installs.
      if (chrome::GetChannel() != version_info::Channel::CANARY)
        return false;
      // Keep one snapshot in this milestone unless snapshots are disabled.
      max_number_of_snapshots = std::min(max_number_of_snapshots, 1);
      purge_milestone = current_milestone;
    }
    SnapshotManager snapshot_manager(user_data_dir);
    snapshot_manager.TakeSnapshot(*last_version);
    snapshot_manager.PurgeInvalidAndOldSnapshots(max_number_of_snapshots,
                                                 purge_milestone);
    return false;
  }
  
  void DowngradeManager::UpdateLastVersion(const base::FilePath& user_data_dir) {
    DCHECK(!user_data_dir.empty());
    DCHECK_NE(type_, Type::kAdministrativeWipe);
    const base::StringPiece version(PRODUCT_VERSION);
    base::WriteFile(GetLastVersionFile(user_data_dir), version.data(),
                    version.size());
  }
  
  void DowngradeManager::DeleteMovedUserDataSoon(
      const base::FilePath& user_data_dir) {
    DCHECK(!user_data_dir.empty());
    // IWYU note: base/location.h and base/task/task_traits.h are guaranteed to be
    // available via base/task/post_task.h.
    content::BrowserThread::PostBestEffortTask(
        FROM_HERE,
        base::ThreadPool::CreateTaskRunner(
            {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
             base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}),
        base::BindOnce(&DeleteMovedUserData, user_data_dir, GetDiskCacheDir()));
  }
  
  void DowngradeManager::ProcessDowngrade(const base::FilePath& user_data_dir) {
    DCHECK(type_ == Type::kAdministrativeWipe || type_ == Type::kSnapshotRestore);
    DCHECK(!user_data_dir.empty());
  
    const base::FilePath disk_cache_dir(GetDiskCacheDir());
    if (!disk_cache_dir.empty())
      MoveCache(disk_cache_dir);
  
    // User Data requires special treatment, as certain files/directories should
    // be left behind. Furthermore, User Data is moved to a new directory within
    // itself (for example, to User Data/User Data.CHROME_DELETE) to guarantee
    // that the movement isn't across volumes.
    const auto failure_count = MoveUserData(
        user_data_dir,
        GetTempDirNameForDelete(user_data_dir, user_data_dir.BaseName()));
    enum class UserDataMoveResult {
      kCreateTargetFailure = 0,
      kSuccess = 1,
      kPartialSuccess = 2,
      kMaxValue = kPartialSuccess
    };
    UserDataMoveResult move_result =
        !failure_count ? UserDataMoveResult::kCreateTargetFailure
                       : (*failure_count ? UserDataMoveResult::kPartialSuccess
                                         : UserDataMoveResult::kSuccess);
    base::UmaHistogramEnumeration("Downgrade.UserDataDirMove.Result",
                                  move_result);
    if (failure_count && *failure_count) {
      // Report precise values rather than an exponentially bucketed histogram.
      base::UmaHistogramExactLinear("Downgrade.UserDataDirMove.FailureCount",
                                    *failure_count, 50);
    }
  
    if (type_ == Type::kSnapshotRestore) {
      SnapshotManager snapshot_manager(user_data_dir);
      snapshot_manager.RestoreSnapshot(version_info::GetVersion());
    }
  
    // Add the migration switch to the command line so that it is propagated to
    // the relaunched process. This is used to prevent a relaunch bomb in case of
    // pathological failure.
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        switches::kUserDataMigrated);
  }
  
  // static
  void DowngradeManager::EnableSnapshotsForTesting(bool enable) {
    g_snapshots_enabled_for_testing = enable;
  }
  
  // static
  DowngradeManager::Type DowngradeManager::GetDowngradeType(
      const base::FilePath& user_data_dir,
      const base::Version& current_version,
      const base::Version& last_version) {
    DCHECK(!user_data_dir.empty());
    DCHECK_LT(current_version, last_version);
  
  #if defined(OS_WIN)
      // Move User Data aside for a clean launch if it follows an
      // administrator-driven downgrade.
    if (IsAdministratorDrivenDowngrade(current_version.components()[0]))
      return Type::kAdministrativeWipe;
  #endif
      return Type::kUnsupported;
  }
  
  // static
  DowngradeManager::Type DowngradeManager::GetDowngradeTypeWithSnapshot(
      const base::FilePath& user_data_dir,
      const base::Version& current_version,
      const base::Version& last_version) {
    DCHECK(!user_data_dir.empty());
    DCHECK_LT(current_version, last_version);
  
    const uint16_t milestone = current_version.components()[0];
  
    // Move User Data and restore from a snapshot if there is a candidate
    // snapshot to restore.
    const auto snapshot_to_restore =
        GetSnapshotToRestore(current_version, user_data_dir);
  
  #if defined(OS_WIN)
    // Move User Data aside for a clean launch if it follows an
    // administrator-driven downgrade when no snapshot is found.
    if (!snapshot_to_restore && IsAdministratorDrivenDowngrade(milestone))
      return Type::kAdministrativeWipe;
  #endif
  
    const uint16_t last_milestone = last_version.components()[0];
    if (last_milestone > milestone)
      return snapshot_to_restore ? Type::kSnapshotRestore : Type::kUnsupported;
  
    return Type::kMinorDowngrade;
  }
  
  }  // namespace downgrade
  