| // Copyright (c) 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/extensions/extension_assets_manager_chromeos.h" |
| |
| #include <stddef.h> |
| |
| #include <map> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "ash/constants/ash_switches.h" |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/files/file_util.h" |
| #include "base/memory/singleton.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 = NULL; |
| |
| // 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; |
| Profile* profile; |
| ExtensionAssetsManager::InstallExtensionCallback callback; |
| }; |
| typedef std::vector<PendingInstallInfo> PendingInstallList; |
| |
| 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)); |
| DCHECK(it != install_queue_.end()); |
| pending_installs->swap(it->second); |
| install_queue_.erase(it); |
| } |
| |
| private: |
| friend struct base::DefaultSingletonTraits<ExtensionAssetsManagerHelper>; |
| |
| ExtensionAssetsManagerHelper() {} |
| ~ExtensionAssetsManagerHelper() {} |
| |
| // Extension ID + version pair. |
| typedef std::pair<std::string, std::string> InstallItem; |
| |
| // Queue of pending installs in progress. |
| typedef std::map<InstallItem, std::vector<PendingInstallInfo> > InstallQueue; |
| |
| 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 = NULL; |
| } |
| } |
| |
| // 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, |
| Profile* profile, |
| const base::FilePath& local_install_dir, |
| const base::FilePath& extension_root) { |
| if (local_install_dir.IsParent(extension_root)) { |
| file_util::UninstallExtension(local_install_dir, id); |
| return; |
| } |
| |
| if (GetSharedInstallDir().IsParent(extension_root)) { |
| // 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)); |
| } |
| } |
| |
| // 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; |
| |
| DictionaryPrefUpdate shared_extensions(local_state, kSharedExtensions); |
| std::vector<std::string> extensions; |
| extensions.reserve(shared_extensions->DictSize()); |
| for (const auto it : shared_extensions->DictItems()) |
| extensions.push_back(it.first); |
| |
| for (const std::string& id : extensions) { |
| base::Value* extension_info = shared_extensions->FindDictKey(id); |
| if (!extension_info) { |
| NOTREACHED(); |
| return false; |
| } |
| if (!CleanUpExtension(id, extension_info, live_extension_paths)) { |
| return false; |
| } |
| if (extension_info->DictEmpty()) |
| shared_extensions->RemoveKey(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/1166539): 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(); |
| 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(); |
| DictionaryPrefUpdate shared_extensions(local_state, kSharedExtensions); |
| std::string* shared_path = nullptr; |
| base::Value* users = nullptr; |
| if (base::Value* extension_info = shared_extensions->FindDictPath(id)) { |
| if (base::Value* version_info = extension_info->FindDictKey(version)) { |
| shared_path = version_info->FindStringKey(kSharedExtensionPath); |
| users = version_info->FindListKey(kSharedExtensionUsers); |
| } |
| } |
| |
| if (shared_path && users) { |
| // This extension version already in shared location. |
| bool user_found = false; |
| for (const base::Value& user : users->GetListDeprecated()) { |
| 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(); |
| DictionaryPrefUpdate shared_extensions(local_state, kSharedExtensions); |
| base::Value* extension_info_weak = shared_extensions->FindDictKey(id); |
| if (!extension_info_weak) { |
| extension_info_weak = shared_extensions->SetKey( |
| id, base::Value(base::Value::Type::DICTIONARY)); |
| } |
| |
| CHECK(!shared_extensions->FindKey(version)); |
| base::Value version_info(base::Value::Type::DICTIONARY); |
| version_info.SetStringKey(kSharedExtensionPath, shared_version_dir.value()); |
| |
| base::Value users(base::Value::Type::LIST); |
| 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.SetKey(kSharedExtensionUsers, std::move(users)); |
| extension_info_weak->SetKey(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, |
| Profile* profile) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| PrefService* local_state = g_browser_process->local_state(); |
| DictionaryPrefUpdate shared_extensions(local_state, kSharedExtensions); |
| base::Value* extension_info = shared_extensions->FindDictKey(id); |
| if (!extension_info) { |
| NOTREACHED(); |
| return; |
| } |
| |
| std::vector<std::string> versions; |
| versions.reserve(extension_info->DictSize()); |
| for (const auto kv : extension_info->DictItems()) { |
| versions.push_back(kv.first); |
| } |
| |
| base::Value user_name(profile->GetProfileUserName()); |
| for (std::vector<std::string>::const_iterator it = versions.begin(); |
| it != versions.end(); it++) { |
| base::Value* version_info = extension_info->FindDictKey(*it); |
| if (!version_info) { |
| NOTREACHED(); |
| continue; |
| } |
| base::Value* users = version_info->FindListKey(kSharedExtensionUsers); |
| if (!users) { |
| NOTREACHED(); |
| continue; |
| } |
| if (users->EraseListValue(user_name) && |
| users->GetListDeprecated().empty()) { |
| std::string* shared_path = |
| version_info->FindStringKey(kSharedExtensionPath); |
| if (!shared_path) { |
| NOTREACHED(); |
| continue; |
| } |
| GetExtensionFileTaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&ExtensionAssetsManagerChromeOS::DeleteSharedVersion, |
| base::FilePath(*shared_path))); |
| extension_info->RemoveKey(*it); |
| } |
| } |
| if (extension_info->DictEmpty()) { |
| shared_extensions->RemoveKey(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* extension_info, |
| std::multimap<std::string, base::FilePath>* live_extension_paths) { |
| user_manager::UserManager* user_manager = user_manager::UserManager::Get(); |
| if (!user_manager) { |
| NOTREACHED(); |
| return false; |
| } |
| |
| std::vector<std::string> versions; |
| versions.reserve(extension_info->DictSize()); |
| for (const auto it : extension_info->DictItems()) { |
| versions.push_back(it.first); |
| } |
| |
| for (std::vector<std::string>::const_iterator it = versions.begin(); |
| it != versions.end(); it++) { |
| base::Value* version_info = extension_info->FindDictKey(*it); |
| if (!version_info) { |
| NOTREACHED(); |
| return false; |
| } |
| base::Value* users = version_info->FindListKey(kSharedExtensionUsers); |
| const std::string* shared_path = |
| version_info->FindStringKey(kSharedExtensionPath); |
| if (!users || !shared_path) { |
| NOTREACHED(); |
| return false; |
| } |
| base::Value::List& users_list = users->GetList(); |
| |
| for (auto iter = users_list.begin(); iter != users_list.end();) { |
| const std::string* user_id = iter->GetIfString(); |
| if (!user_id) { |
| NOTREACHED(); |
| 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::unique_ptr<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->RemoveKey(*it); |
| } else { |
| live_extension_paths->insert( |
| std::make_pair(id, base::FilePath(*shared_path))); |
| } |
| } |
| |
| return true; |
| } |
| |
| } // namespace extensions |