blob: 0de6fc5267c09d2733982ce32d617314cad9d647 [file] [log] [blame]
// Copyright 2025 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/external_provider_manager.h"
#include <cstddef>
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/notimplemented.h"
#include "base/trace_event/trace_event.h"
#include "base/version.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/corrupted_extension_reinstaller.h"
#include "chrome/browser/extensions/crx_installer.h"
#include "chrome/browser/extensions/extension_error_controller.h"
#include "chrome/browser/extensions/external_install_manager.h"
#include "chrome/browser/extensions/external_provider_impl.h"
#include "chrome/browser/extensions/external_provider_manager_factory.h"
#include "chrome/browser/extensions/forced_extensions/install_stage_tracker.h"
#include "chrome/browser/extensions/installed_loader.h"
#include "chrome/browser/extensions/updater/extension_updater.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/extensions/extension_constants.h"
#include "components/crx_file/id_util.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/external_install_info.h"
#include "extensions/browser/management_policy.h"
#include "extensions/browser/pending_extension_manager.h"
#include "extensions/browser/updater/extension_cache.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest.h"
#include "extensions/common/verifier_formats.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/extensions/install_limiter.h"
#endif
namespace {
bool g_external_updates_disabled_for_test_ = false;
} // namespace
using extensions::mojom::ManifestLocation;
namespace extensions {
using content::BrowserThread;
ExternalProviderManager::ExternalProviderManager(
content::BrowserContext* context)
: context_(context),
extension_prefs_(ExtensionPrefs::Get(context)),
registry_(ExtensionRegistry::Get(context)),
pending_extension_manager_(PendingExtensionManager::Get(context)),
error_controller_(ExtensionErrorController::Get(context)) {}
ExternalProviderManager::~ExternalProviderManager() = default;
// static
ExternalProviderManager* ExternalProviderManager::Get(
content::BrowserContext* context) {
return ExternalProviderManagerFactory::GetForBrowserContext(context);
}
void ExternalProviderManager::Shutdown() {
// No need to unload extensions here because they are profile-scoped, and the
// profile is in the process of being deleted.
for (const auto& provider : external_extension_providers_) {
provider->ServiceShutdown();
}
weak_ptr_factory_.InvalidateWeakPtrs();
}
void ExternalProviderManager::CreateExternalProviders() {
ExternalProviderImpl::CreateExternalProviders(
this, Profile::FromBrowserContext(context_.get()),
&external_extension_providers_);
}
// Some extensions will autoupdate themselves externally from Chrome. These
// are typically part of some larger client application package. To support
// these, the extension will register its location in the preferences file
// (and also, on Windows, in the registry) and this code will periodically
// check that location for a .crx file, which it will then install locally if
// a new version is available.
// Errors are reported through LoadErrorReporter. Success is not reported.
void ExternalProviderManager::CheckForExternalUpdates() {
if (g_external_updates_disabled_for_test_) {
return;
}
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
TRACE_EVENT0("browser,startup",
"ExternalProviderManager::CheckForExternalUpdates");
// Note that this installation is intentionally silent (since it didn't
// go through the front-end). Extensions that are registered in this
// way are effectively considered 'pre-bundled', and so implicitly
// trusted. In general, if something has HKLM or filesystem access,
// they could install an extension manually themselves anyway.
// Ask each external extension provider to give us a call back for each
// extension they know about. See OnExternalExtension(File|UpdateUrl)Found.
for (const auto& provider : external_extension_providers_) {
provider->VisitRegisteredExtension();
}
// Do any required work that we would have done after completion of all
// providers.
if (external_extension_providers_.empty()) {
OnAllExternalProvidersReady();
}
}
void ExternalProviderManager::OnAllExternalProvidersReady() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
Profile* profile = Profile::FromBrowserContext(context_.get());
#if BUILDFLAG(IS_CHROMEOS)
auto* install_limiter = InstallLimiter::Get(profile);
if (install_limiter) {
install_limiter->OnAllExternalProvidersReady();
}
#endif // BUILDFLAG(IS_CHROMEOS)
// Install any pending extensions.
ExtensionUpdater* updater = ExtensionUpdater::Get(profile);
if (update_once_all_providers_are_ready_ && updater->enabled()) {
update_once_all_providers_are_ready_ = false;
ExtensionUpdater::CheckParams params;
params.callback = external_updates_finished_callback_
? std::move(external_updates_finished_callback_)
: base::OnceClosure();
updater->CheckNow(std::move(params));
} else if (external_updates_finished_callback_) {
std::move(external_updates_finished_callback_).Run();
}
// Uninstall all the unclaimed extensions.
ExtensionPrefs::ExtensionsInfo extensions_info =
extension_prefs_->GetInstalledExtensionsInfo();
for (const auto& info : extensions_info) {
if (Manifest::IsExternalLocation(info.extension_location)) {
CheckExternalUninstall(info.extension_id);
}
}
ExtensionErrorController::Get(context_)->ShowErrorIfNeeded();
ExternalInstallManager::Get(context_)->UpdateExternalExtensionAlert();
}
void ExternalProviderManager::CheckExternalUninstall(const std::string& id) {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// We get the list of external extensions to check from preferences.
// It is possible that an extension has preferences but is not loaded.
// For example, an extension that requires experimental permissions
// will not be loaded if the experimental command line flag is not used.
// In this case, do not uninstall.
const Extension* extension = registry_->GetInstalledExtension(id);
if (!extension) {
// We can't call UninstallExtension with an unloaded/invalid
// extension ID.
LOG(WARNING) << "Checking uninstallation of unloaded/invalid extension "
<< "with id: " << id;
return;
}
// Check if the providers know about this extension.
for (const auto& provider : external_extension_providers_) {
DCHECK(provider->IsReady());
if (provider->HasExtensionWithLocation(id, extension->location())) {
// Yup, known extension, don't uninstall.
return;
}
}
ExtensionRegistrar::Get(context_)->UninstallExtension(
id, UNINSTALL_REASON_ORPHANED_EXTERNAL_EXTENSION, nullptr,
base::NullCallback());
}
void ExternalProviderManager::ReinstallProviderExtensions() {
for (const auto& provider : external_extension_providers_) {
provider->TriggerOnExternalExtensionFound();
}
}
bool ExternalProviderManager::AreAllExternalProvidersReady() const {
for (const auto& provider : external_extension_providers_) {
if (!provider->IsReady()) {
return false;
}
}
return true;
}
void ExternalProviderManager::ClearProvidersForTesting() {
external_extension_providers_.clear();
}
void ExternalProviderManager::AddProviderForTesting(
std::unique_ptr<ExternalProviderInterface> test_provider) {
CHECK(test_provider);
external_extension_providers_.push_back(std::move(test_provider));
}
base::AutoReset<bool>
ExternalProviderManager::DisableExternalUpdatesForTesting() {
return base::AutoReset<bool>(&g_external_updates_disabled_for_test_, true);
}
bool ExternalProviderManager::OnExternalExtensionFileFound(
const ExternalInstallInfoFile& info) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(crx_file::id_util::IdIsValid(info.extension_id));
if (extension_prefs_->IsExternalExtensionUninstalled(info.extension_id)) {
return false;
}
// Before even bothering to unpack, check and see if we already have this
// version. This is important because these extensions are going to get
// installed on every startup.
const Extension* existing = registry_->GetExtensionById(
info.extension_id, ExtensionRegistry::EVERYTHING);
if (existing) {
// The pre-installed apps will have the location set as INTERNAL. Since
// older pre-installed apps are installed as EXTERNAL, we override them.
// However, if the app is already installed as internal, then do the version
// check.
// TODO(grv) : Remove after Q1-2013.
bool is_preinstalled_apps_migration =
(info.crx_location == mojom::ManifestLocation::kInternal &&
Manifest::IsExternalLocation(existing->location()));
if (!is_preinstalled_apps_migration) {
switch (existing->version().CompareTo(info.version)) {
case -1: // existing version is older, we should upgrade
break;
case 0: // existing version is same, do nothing
return false;
case 1: // existing version is newer, uh-oh
LOG(WARNING) << "Found external version of extension "
<< info.extension_id
<< " that is older than current version. Current version"
<< " is: " << existing->VersionString() << ". New "
<< "version is: " << info.version.GetString()
<< ". Keeping current version.";
return false;
}
}
}
// If the extension is already pending, don't start an install.
if (!pending_extension_manager_->AddFromExternalFile(
info.extension_id, info.crx_location, info.version,
info.creation_flags, info.mark_acknowledged)) {
return false;
}
#if BUILDFLAG(IS_CHROMEOS)
if (extension_misc::IsDemoModeChromeApp(info.extension_id)) {
pending_extension_manager_->Remove(info.extension_id);
return true;
}
#endif // BUILDFLAG(IS_CHROMEOS)
// no client (silent install)
scoped_refptr<CrxInstaller> installer(CrxInstaller::CreateSilent(context_));
installer->AddInstallerCallback(base::BindOnce(
&ExternalProviderManager::InstallationFromExternalFileFinished,
weak_ptr_factory_.GetWeakPtr(), info.extension_id));
installer->set_install_source(info.crx_location);
installer->set_expected_id(info.extension_id);
installer->set_expected_version(info.version,
true /* fail_install_if_unexpected */);
installer->set_install_immediately(info.install_immediately);
installer->set_creation_flags(info.creation_flags);
CRXFileInfo file_info(
info.path, info.crx_location == mojom::ManifestLocation::kExternalPolicy
? GetPolicyVerifierFormat()
: GetExternalVerifierFormat());
#if BUILDFLAG(IS_CHROMEOS)
auto* install_limiter =
InstallLimiter::Get(Profile::FromBrowserContext(context_.get()));
if (install_limiter) {
install_limiter->Add(installer, file_info);
} else {
installer->InstallCrxFile(file_info);
}
#else
installer->InstallCrxFile(file_info);
#endif
// Depending on the source, a new external extension might not need a user
// notification on installation. For such extensions, mark them acknowledged
// now to suppress the notification.
if (info.mark_acknowledged) {
ExternalInstallManager::Get(context_)->AcknowledgeExternalExtension(
info.extension_id);
}
return true;
}
bool ExternalProviderManager::OnExternalExtensionUpdateUrlFound(
const ExternalInstallInfoUpdateUrl& info,
bool force_update) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(crx_file::id_util::IdIsValid(info.extension_id));
if (Manifest::IsExternalLocation(info.download_location)) {
// All extensions that are not user specific can be cached.
ExtensionsBrowserClient::Get()->GetExtensionCache()->AllowCaching(
info.extension_id);
}
InstallStageTracker* install_stage_tracker =
InstallStageTracker::Get(context_);
const Extension* extension = registry_->GetExtensionById(
info.extension_id, ExtensionRegistry::EVERYTHING);
if (extension) {
// Already installed. Skip this install if the current location has higher
// priority than |info.download_location|, and we aren't doing a
// reinstall of a corrupt policy force-installed extension.
ManifestLocation current = extension->location();
if (!IsReinstallForCorruptionExpected(info.extension_id) &&
current == Manifest::GetHigherPriorityLocation(
current, info.download_location)) {
install_stage_tracker->ReportFailure(
info.extension_id,
InstallStageTracker::FailureReason::ALREADY_INSTALLED);
return false;
}
// If the installation is requested from a higher priority source, update
// its install location.
ExtensionRegistrar* registrar = ExtensionRegistrar::Get(context_);
if (current !=
Manifest::GetHigherPriorityLocation(current, info.download_location)) {
registrar->RemoveExtension(info.extension_id,
UnloadedExtensionReason::UPDATE);
// Fetch the installation info from the prefs, and reload the extension
// with a modified install location.
std::optional<ExtensionInfo> installed_extension(
extension_prefs_->GetInstalledExtensionInfo(info.extension_id));
installed_extension->extension_location = info.download_location;
// Load the extension with the new install location
Profile* profile = Profile::FromBrowserContext(context_);
InstalledLoader(profile).Load(*installed_extension, false);
// Update the install location in the prefs.
extension_prefs_->SetInstallLocation(info.extension_id,
info.download_location);
// If the extension was due to any of the following reasons, and it must
// remain enabled, remove those reasons:
// - Disabled by the user.
// - User hasn't accepted a permissions increase.
// - User hasn't accepted an external extension's prompt.
if (registry_->disabled_extensions().GetByID(info.extension_id) &&
ExtensionSystem::Get(context_)
->management_policy()
->MustRemainEnabled(
registry_->GetExtensionById(info.extension_id,
ExtensionRegistry::EVERYTHING),
nullptr)) {
const DisableReasonSet to_remove = {
disable_reason::DISABLE_USER_ACTION,
disable_reason::DISABLE_EXTERNAL_EXTENSION,
disable_reason::DISABLE_PERMISSIONS_INCREASE};
extension_prefs_->RemoveDisableReasons(info.extension_id, to_remove);
// Only re-enable the extension if there are no other disable reasons.
if (extension_prefs_->GetDisableReasons(info.extension_id).empty()) {
registrar->EnableExtension(info.extension_id);
}
}
// If the extension is not corrupted, it is already installed with the
// correct install location, so there is no need to add it to the pending
// set of extensions. If the extension is corrupted, it should be
// reinstalled, thus it should be added to the pending extensions for
// installation.
if (!IsReinstallForCorruptionExpected(info.extension_id)) {
return false;
}
}
// Otherwise, overwrite the current installation.
}
// Add |info.extension_id| to the set of pending extensions. If it can not
// be added, then there is already a pending record from a higher-priority
// install source. In this case, signal that this extension will not be
// installed by returning false.
install_stage_tracker->ReportInstallationStage(
info.extension_id, InstallStageTracker::Stage::PENDING);
if (!pending_extension_manager_->AddFromExternalUpdateUrl(
info.extension_id, info.install_parameter, info.update_url,
info.download_location, info.creation_flags,
info.mark_acknowledged)) {
// We can reach here if the extension from an equal or higher priority
// source is already present in the |pending_extension_list_|. No need to
// report the failure in this case.
if (!pending_extension_manager_->IsIdPending(info.extension_id)) {
install_stage_tracker->ReportFailure(
info.extension_id,
InstallStageTracker::FailureReason::PENDING_ADD_FAILED);
}
return false;
}
if (force_update) {
update_once_all_providers_are_ready_ = true;
}
return true;
}
void ExternalProviderManager::OnExternalProviderReady(
const ExternalProviderInterface* provider) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CHECK(provider->IsReady());
// An external provider has finished loading. We only take action
// if all of them are finished. So we check them first.
if (AreAllExternalProvidersReady()) {
OnAllExternalProvidersReady();
}
}
void ExternalProviderManager::OnExternalProviderUpdateComplete(
const ExternalProviderInterface* provider,
const std::vector<ExternalInstallInfoUpdateUrl>& update_url_extensions,
const std::vector<ExternalInstallInfoFile>& file_extensions,
const std::set<std::string>& removed_extensions) {
// Update pending_extension_manager_ with the new extensions first.
for (const auto& extension : update_url_extensions) {
OnExternalExtensionUpdateUrlFound(extension, false);
}
for (const auto& extension : file_extensions) {
OnExternalExtensionFileFound(extension);
}
#if DCHECK_IS_ON()
for (const std::string& id : removed_extensions) {
for (const auto& extension : update_url_extensions) {
DCHECK_NE(id, extension.extension_id);
}
for (const auto& extension : file_extensions) {
DCHECK_NE(id, extension.extension_id);
}
}
#endif
// Then uninstall before running |updater_|.
for (const std::string& id : removed_extensions) {
CheckExternalUninstall(id);
}
Profile* profile = Profile::FromBrowserContext(context_);
ExtensionUpdater* updater = ExtensionUpdater::Get(profile);
if (!update_url_extensions.empty() && updater->enabled()) {
// Empty params will cause pending extensions to be updated.
updater->CheckNow(ExtensionUpdater::CheckParams());
}
error_controller_->ShowErrorIfNeeded();
ExternalInstallManager::Get(context_)->UpdateExternalExtensionAlert();
}
void ExternalProviderManager::InstallationFromExternalFileFinished(
const std::string& extension_id,
const std::optional<CrxInstallError>& error) {
if (error != std::nullopt) {
// When installation is finished, the extension should not remain in the
// pending extension manager. For successful installations this is done
// in OnExtensionInstalled handler.
pending_extension_manager_->Remove(extension_id);
}
}
bool ExternalProviderManager::IsReinstallForCorruptionExpected(
const ExtensionId& id) const {
auto* reinstaller = CorruptedExtensionReinstaller::Get(context_);
return reinstaller->IsReinstallForCorruptionExpected(id);
}
} // namespace extensions