| // 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 |