chromium/chrome/browser/extensions/extension_assets_manager_chromeos.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/extensions/extension_assets_manager_chromeos.h"

#include <stddef.h>

#include <map>
#include <memory>
#include <optional>
#include <utility>
#include <vector>

#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/singleton.h"
#include "base/not_fatal_until.h"
#include "base/system/sys_info.h"
#include "base/task/sequenced_task_runner.h"
#include "base/values.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_management.h"
#include "chrome/browser/profiles/profile.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/common/extension.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_url_handlers.h"

using content::BrowserThread;

namespace extensions {
namespace {

// Path to shared extensions install dir.
const char kSharedExtensionsDir[] = "/var/cache/shared_extensions";

// Shared install dir overrider for tests only.
static const base::FilePath* g_shared_install_dir_override = nullptr;

// This helper class lives on UI thread only. Main purpose of this class is to
// track shared installation in progress between multiple profiles.
class ExtensionAssetsManagerHelper {
 public:
  // Info about pending install request.
  struct PendingInstallInfo {
    base::FilePath unpacked_extension_root;
    base::FilePath local_install_dir;
    raw_ptr<Profile> profile;
    ExtensionAssetsManager::InstallExtensionCallback callback;
  };
  using PendingInstallList = std::vector<PendingInstallInfo>;

  ExtensionAssetsManagerHelper(const ExtensionAssetsManagerHelper&) = delete;
  ExtensionAssetsManagerHelper& operator=(const ExtensionAssetsManagerHelper&) =
      delete;

  static ExtensionAssetsManagerHelper* GetInstance() {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    return base::Singleton<ExtensionAssetsManagerHelper>::get();
  }

  // Remember that shared install is in progress. Return true if there is no
  // other installs for given id and version.
  bool RecordSharedInstall(
      const std::string& id,
      const std::string& version,
      const base::FilePath& unpacked_extension_root,
      const base::FilePath& local_install_dir,
      Profile* profile,
      ExtensionAssetsManager::InstallExtensionCallback callback) {
    PendingInstallInfo install_info;
    install_info.unpacked_extension_root = unpacked_extension_root;
    install_info.local_install_dir = local_install_dir;
    install_info.profile = profile;
    install_info.callback = std::move(callback);

    std::vector<PendingInstallInfo>& callbacks =
        install_queue_[InstallQueue::key_type(id, version)];
    callbacks.push_back(std::move(install_info));

    return callbacks.size() == 1;
  }

  // Remove record about shared installation in progress and return
  // |pending_installs|.
  void SharedInstallDone(const std::string& id,
                         const std::string& version,
                         PendingInstallList* pending_installs) {
    InstallQueue::iterator it = install_queue_.find(
        InstallQueue::key_type(id, version));
    CHECK(it != install_queue_.end(), base::NotFatalUntil::M130);
    pending_installs->swap(it->second);
    install_queue_.erase(it);
  }

 private:
  friend struct base::DefaultSingletonTraits<ExtensionAssetsManagerHelper>;

  ExtensionAssetsManagerHelper() = default;
  ~ExtensionAssetsManagerHelper() = default;

  // Extension ID + version pair.
  using InstallItem = std::pair<std::string, std::string>;

  // Queue of pending installs in progress.
  using InstallQueue = std::map<InstallItem, std::vector<PendingInstallInfo>>;

  InstallQueue install_queue_;
};

}  // namespace

const char ExtensionAssetsManagerChromeOS::kSharedExtensions[] =
    "SharedExtensions";

const char ExtensionAssetsManagerChromeOS::kSharedExtensionPath[] = "path";

const char ExtensionAssetsManagerChromeOS::kSharedExtensionUsers[] = "users";

ExtensionAssetsManagerChromeOS::ExtensionAssetsManagerChromeOS() { }

ExtensionAssetsManagerChromeOS::~ExtensionAssetsManagerChromeOS() {
  if (g_shared_install_dir_override) {
    delete g_shared_install_dir_override;
    g_shared_install_dir_override = nullptr;
  }
}

// static
ExtensionAssetsManagerChromeOS* ExtensionAssetsManagerChromeOS::GetInstance() {
  return base::Singleton<ExtensionAssetsManagerChromeOS>::get();
}

// static
void ExtensionAssetsManagerChromeOS::RegisterPrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(kSharedExtensions);
}

void ExtensionAssetsManagerChromeOS::InstallExtension(
    const Extension* extension,
    const base::FilePath& unpacked_extension_root,
    const base::FilePath& local_install_dir,
    Profile* profile,
    InstallExtensionCallback callback,
    bool updates_from_webstore_or_empty_update_url) {
  if (!CanShareAssets(extension, unpacked_extension_root,
                      updates_from_webstore_or_empty_update_url)) {
    InstallLocalExtension(extension->id(), extension->VersionString(),
                          unpacked_extension_root, local_install_dir,
                          std::move(callback));
    return;
  }

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&ExtensionAssetsManagerChromeOS::CheckSharedExtension,
                     extension->id(), extension->VersionString(),
                     unpacked_extension_root, local_install_dir, profile,
                     std::move(callback)));
}

void ExtensionAssetsManagerChromeOS::UninstallExtension(
    const std::string& id,
    const std::string& profile_user_name,
    const base::FilePath& extensions_install_dir,
    const base::FilePath& extension_dir_to_delete,
    const base::FilePath& profile_dir) {
  if (extensions_install_dir.IsParent(extension_dir_to_delete)) {
    file_util::UninstallExtension(profile_dir, extensions_install_dir,
                                  extension_dir_to_delete);
    return;
  }

  if (GetSharedInstallDir().IsParent(extension_dir_to_delete)) {
    // In some test extensions installed outside local_install_dir emulate
    // previous behavior that just do nothing in this case.
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            &ExtensionAssetsManagerChromeOS::MarkSharedExtensionUnused, id,
            profile_user_name));
  }
}

// static
base::FilePath ExtensionAssetsManagerChromeOS::GetSharedInstallDir() {
  if (g_shared_install_dir_override)
    return *g_shared_install_dir_override;
  else
    return base::FilePath(kSharedExtensionsDir);
}

// static
bool ExtensionAssetsManagerChromeOS::IsSharedInstall(
    const Extension* extension) {
  return GetSharedInstallDir().IsParent(extension->path());
}

// static
bool ExtensionAssetsManagerChromeOS::CleanUpSharedExtensions(
    std::multimap<std::string, base::FilePath>* live_extension_paths) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  PrefService* local_state = g_browser_process->local_state();
  // It happens in many unit tests.
  if (!local_state)
    return false;

  ScopedDictPrefUpdate shared_extensions(local_state, kSharedExtensions);
  base::Value::Dict& shared_extension_dict = shared_extensions.Get();

  std::vector<std::string> extensions;
  extensions.reserve(shared_extension_dict.size());
  for (const auto it : shared_extension_dict)
    extensions.push_back(it.first);

  for (const std::string& id : extensions) {
    base::Value::Dict* extension_info = shared_extension_dict.FindDict(id);
    if (!extension_info) {
      NOTREACHED_IN_MIGRATION();
      return false;
    }
    if (!CleanUpExtension(id, *extension_info, live_extension_paths)) {
      return false;
    }
    if (extension_info->empty())
      shared_extension_dict.Remove(id);
  }

  return true;
}

// static
void ExtensionAssetsManagerChromeOS::SetSharedInstallDirForTesting(
    const base::FilePath& install_dir) {
  DCHECK(!g_shared_install_dir_override);
  g_shared_install_dir_override = new base::FilePath(install_dir);
}

// static
bool ExtensionAssetsManagerChromeOS::CanShareAssets(
    const Extension* extension,
    const base::FilePath& unpacked_extension_root,
    bool updates_from_webstore_or_empty_update_url) {
  if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
          ash::switches::kEnableExtensionAssetsSharing)) {
    return false;
  }

  // TODO(crbug.com/40742161): Investigate why do we allow sharing assets in
  // case of empty update URL and if the empty update URL is not required,
  // update this to consider only the updates from webstore.
  if (!updates_from_webstore_or_empty_update_url)
    return false;

  // Chrome caches crx files for installed by default apps so sharing assets is
  // also possible. User specific apps should be excluded to not expose apps
  // unique for the user outside of user's cryptohome.
  return Manifest::IsExternalLocation(extension->location());
}

// static
void ExtensionAssetsManagerChromeOS::CheckSharedExtension(
    const std::string& id,
    const std::string& version,
    const base::FilePath& unpacked_extension_root,
    const base::FilePath& local_install_dir,
    Profile* profile,
    InstallExtensionCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  const std::string& user_id = profile->GetProfileUserName();
  user_manager::UserManager* user_manager = user_manager::UserManager::Get();
  if (!user_manager) {
    NOTREACHED_IN_MIGRATION();
    return;
  }

  if (user_manager->IsUserNonCryptohomeDataEphemeral(
          AccountId::FromUserEmail(user_id)) ||
      !user_manager->IsLoggedInAsUserWithGaiaAccount()) {
    // Don't cache anything in shared location for ephemeral user or special
    // user types.
    GetExtensionFileTaskRunner()->PostTask(
        FROM_HERE,
        base::BindOnce(&ExtensionAssetsManagerChromeOS::InstallLocalExtension,
                       id, version, unpacked_extension_root, local_install_dir,
                       std::move(callback)));
    return;
  }

  PrefService* local_state = g_browser_process->local_state();
  ScopedDictPrefUpdate shared_extensions(local_state, kSharedExtensions);
  std::string* shared_path = nullptr;
  base::Value::List* users = nullptr;
  if (base::Value::Dict* extension_info = shared_extensions->FindDict(id)) {
    if (base::Value::Dict* version_info = extension_info->FindDict(version)) {
      shared_path = version_info->FindString(kSharedExtensionPath);
      users = version_info->FindList(kSharedExtensionUsers);
    }
  }

  if (shared_path && users) {
    // This extension version already in shared location.
    bool user_found = false;
    for (const base::Value& user : *users) {
      const std::string* temp = user.GetIfString();
      if (temp && *temp == user_id) {
        // Re-installation for the same user.
        user_found = true;
        break;
      }
    }
    if (!user_found)
      users->Append(user_id);

    // unpacked_extension_root will be deleted by CrxInstaller.
    GetExtensionFileTaskRunner()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback), base::FilePath(*shared_path)));
  } else {
    // Desired version is not found in shared location.
    ExtensionAssetsManagerHelper* helper =
        ExtensionAssetsManagerHelper::GetInstance();
    if (helper->RecordSharedInstall(id, version, unpacked_extension_root,
                                    local_install_dir, profile,
                                    std::move(callback))) {
      // There is no install in progress for given <id, version> so run install.
      GetExtensionFileTaskRunner()->PostTask(
          FROM_HERE,
          base::BindOnce(
              &ExtensionAssetsManagerChromeOS::InstallSharedExtension, id,
              version, unpacked_extension_root));
    }
  }
}

// static
void ExtensionAssetsManagerChromeOS::InstallSharedExtension(
      const std::string& id,
      const std::string& version,
      const base::FilePath& unpacked_extension_root) {
  base::FilePath shared_install_dir = GetSharedInstallDir();
  base::FilePath shared_version_dir = file_util::InstallExtension(
      unpacked_extension_root, id, version, shared_install_dir);
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          &ExtensionAssetsManagerChromeOS::InstallSharedExtensionDone, id,
          version, shared_version_dir));
}

// static
void ExtensionAssetsManagerChromeOS::InstallSharedExtensionDone(
    const std::string& id,
    const std::string& version,
    const base::FilePath& shared_version_dir) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  ExtensionAssetsManagerHelper* helper =
      ExtensionAssetsManagerHelper::GetInstance();
  ExtensionAssetsManagerHelper::PendingInstallList pending_installs;
  helper->SharedInstallDone(id, version, &pending_installs);

  if (shared_version_dir.empty()) {
    // Installation to shared location failed, try local dir.
    // TODO(dpolukhin): add UMA stats reporting.
    for (size_t i = 0; i < pending_installs.size(); i++) {
      ExtensionAssetsManagerHelper::PendingInstallInfo& info =
          pending_installs[i];
      GetExtensionFileTaskRunner()->PostTask(
          FROM_HERE,
          base::BindOnce(&ExtensionAssetsManagerChromeOS::InstallLocalExtension,
                         id, version, info.unpacked_extension_root,
                         info.local_install_dir, std::move(info.callback)));
    }
    return;
  }

  PrefService* local_state = g_browser_process->local_state();
  ScopedDictPrefUpdate shared_extensions(local_state, kSharedExtensions);
  base::Value::Dict* extension_info_weak = shared_extensions->EnsureDict(id);

  CHECK(!shared_extensions->Find(version));
  base::Value::Dict version_info;
  version_info.Set(kSharedExtensionPath, shared_version_dir.value());

  base::Value::List users;
  for (size_t i = 0; i < pending_installs.size(); i++) {
    ExtensionAssetsManagerHelper::PendingInstallInfo& info =
        pending_installs[i];
    users.Append(info.profile->GetProfileUserName());

    GetExtensionFileTaskRunner()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(info.callback), shared_version_dir));
  }
  version_info.Set(kSharedExtensionUsers, std::move(users));
  extension_info_weak->Set(version, std::move(version_info));
}

// static
void ExtensionAssetsManagerChromeOS::InstallLocalExtension(
    const std::string& id,
    const std::string& version,
    const base::FilePath& unpacked_extension_root,
    const base::FilePath& local_install_dir,
    InstallExtensionCallback callback) {
  std::move(callback).Run(file_util::InstallExtension(
      unpacked_extension_root, id, version, local_install_dir));
}

// static
void ExtensionAssetsManagerChromeOS::MarkSharedExtensionUnused(
    const std::string& id,
    const std::string& profile_user_name) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  PrefService* local_state = g_browser_process->local_state();
  ScopedDictPrefUpdate shared_extensions(local_state, kSharedExtensions);
  base::Value::Dict& shared_extensions_dict = shared_extensions.Get();
  base::Value::Dict* extension_info = shared_extensions_dict.FindDict(id);
  if (!extension_info) {
    NOTREACHED_IN_MIGRATION();
    return;
  }

  std::vector<std::string> versions;
  versions.reserve(extension_info->size());
  for (const auto kv : *extension_info) {
    versions.push_back(kv.first);
  }

  base::Value user_name(profile_user_name);
  for (std::vector<std::string>::const_iterator it = versions.begin();
       it != versions.end(); it++) {
    base::Value::Dict* version_info = extension_info->FindDict(*it);
    if (!version_info) {
      NOTREACHED_IN_MIGRATION();
      continue;
    }
    base::Value::List* users = version_info->FindList(kSharedExtensionUsers);
    if (!users) {
      NOTREACHED_IN_MIGRATION();
      continue;
    }
    if (users->EraseValue(user_name) && users->empty()) {
      std::string* shared_path = version_info->FindString(kSharedExtensionPath);
      if (!shared_path) {
        NOTREACHED_IN_MIGRATION();
        continue;
      }
      GetExtensionFileTaskRunner()->PostTask(
          FROM_HERE,
          base::BindOnce(&ExtensionAssetsManagerChromeOS::DeleteSharedVersion,
                         base::FilePath(*shared_path)));
      extension_info->Remove(*it);
    }
  }
  if (extension_info->empty()) {
    shared_extensions_dict.Remove(id);
    // Don't remove extension dir in shared location. It will be removed by GC
    // when it is safe to do so, and this avoids a race condition between
    // concurrent uninstall by one user and install by another.
  }
}

// static
void ExtensionAssetsManagerChromeOS::DeleteSharedVersion(
    const base::FilePath& shared_version_dir) {
  CHECK(GetSharedInstallDir().IsParent(shared_version_dir));
  base::DeletePathRecursively(shared_version_dir);
}

// static
bool ExtensionAssetsManagerChromeOS::CleanUpExtension(
    const std::string& id,
    base::Value::Dict& extension_info,
    std::multimap<std::string, base::FilePath>* live_extension_paths) {
  user_manager::UserManager* user_manager = user_manager::UserManager::Get();
  if (!user_manager) {
    NOTREACHED_IN_MIGRATION();
    return false;
  }

  std::vector<std::string> versions;
  versions.reserve(extension_info.size());
  for (const auto it : extension_info) {
    versions.push_back(it.first);
  }

  for (std::vector<std::string>::const_iterator it = versions.begin();
       it != versions.end(); it++) {
    base::Value::Dict* version_info = extension_info.FindDict(*it);
    if (!version_info) {
      NOTREACHED_IN_MIGRATION();
      return false;
    }
    base::Value::List* users_list =
        version_info->FindList(kSharedExtensionUsers);
    const std::string* shared_path =
        version_info->FindString(kSharedExtensionPath);
    if (!users_list || !shared_path) {
      NOTREACHED_IN_MIGRATION();
      return false;
    }

    for (auto iter = users_list->begin(); iter != users_list->end();) {
      const std::string* user_id = iter->GetIfString();
      if (!user_id) {
        NOTREACHED_IN_MIGRATION();
        return false;
      }
      const user_manager::User* user =
          user_manager->FindUser(AccountId::FromUserEmail(*user_id));
      bool not_used = false;
      if (!user) {
        not_used = true;
      } else if (user->is_logged_in()) {
        // For logged in user also check that this path is actually used as
        // installed extension or as delayed install.
        Profile* profile = ash::ProfileHelper::Get()->GetProfileByUser(user);
        DCHECK(profile);
        ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(profile);
        if (!extension_prefs || extension_prefs->pref_service()->ReadOnly())
          return false;

        std::optional<ExtensionInfo> info =
            extension_prefs->GetInstalledExtensionInfo(id);
        if (!info || info->extension_path != base::FilePath(*shared_path)) {
          info = extension_prefs->GetDelayedInstallInfo(id);
          if (!info || info->extension_path != base::FilePath(*shared_path)) {
            not_used = true;
          }
        }
      }

      if (not_used) {
        iter = users_list->erase(iter);
      } else {
        ++iter;
      }
    }

    if (users_list->empty()) {
      extension_info.Remove(*it);
    } else {
      live_extension_paths->insert(
          std::make_pair(id, base::FilePath(*shared_path)));
    }
  }

  return true;
}

}  // namespace extensions