| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/manifest_v2_handler.h" |
| |
| #include "base/auto_reset.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/one_shot_event.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/types/pass_key.h" |
| #include "components/keyed_service/content/browser_context_dependency_manager.h" |
| #include "content/public/browser/browser_context.h" |
| #include "extensions/browser/disable_reason.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/extension_prefs_factory.h" |
| #include "extensions/browser/extension_registrar.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_registry_factory.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/extension_system_provider.h" |
| #include "extensions/browser/extensions_browser_client.h" |
| #include "extensions/browser/pref_types.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "extensions/common/extension.h" |
| |
| static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Whether to override the MV2 deprecation for testing purposes. |
| bool g_allow_mv2_for_testing = false; |
| |
| // Returns the suffix to use for histograms related to the manifest location |
| // grouping. |
| const char* GetHistogramManifestLocation(mojom::ManifestLocation location) { |
| switch (location) { |
| case mojom::ManifestLocation::kComponent: |
| case mojom::ManifestLocation::kExternalComponent: |
| return "Component"; |
| case mojom::ManifestLocation::kExternalPolicy: |
| case mojom::ManifestLocation::kExternalPolicyDownload: |
| return "Policy"; |
| case mojom::ManifestLocation::kCommandLine: |
| case mojom::ManifestLocation::kUnpacked: |
| return "Unpacked"; |
| case mojom::ManifestLocation::kExternalRegistry: |
| case mojom::ManifestLocation::kExternalPref: |
| case mojom::ManifestLocation::kExternalPrefDownload: |
| return "External"; |
| case mojom::ManifestLocation::kInternal: |
| return "Internal"; |
| case mojom::ManifestLocation::kInvalidLocation: |
| NOTREACHED(); |
| } |
| } |
| |
| class ManifestV2HandlerFactory : public BrowserContextKeyedServiceFactory { |
| public: |
| ManifestV2HandlerFactory(); |
| ManifestV2HandlerFactory(const ManifestV2HandlerFactory&) = delete; |
| ManifestV2HandlerFactory& operator=(const ManifestV2HandlerFactory&) = delete; |
| ~ManifestV2HandlerFactory() override = default; |
| |
| ManifestV2Handler* GetForBrowserContext(content::BrowserContext* context); |
| |
| private: |
| // BrowserContextKeyedServiceFactory: |
| content::BrowserContext* GetBrowserContextToUse( |
| content::BrowserContext* context) const override; |
| std::unique_ptr<KeyedService> BuildServiceInstanceForBrowserContext( |
| content::BrowserContext* context) const override; |
| bool ServiceIsCreatedWithBrowserContext() const override; |
| }; |
| |
| ManifestV2HandlerFactory::ManifestV2HandlerFactory() |
| : BrowserContextKeyedServiceFactory( |
| "ManifestV2Handler", |
| BrowserContextDependencyManager::GetInstance()) { |
| DependsOn(ExtensionPrefsFactory::GetInstance()); |
| DependsOn(ExtensionRegistryFactory::GetInstance()); |
| DependsOn(ExtensionsBrowserClient::Get()->GetExtensionSystemFactory()); |
| } |
| |
| ManifestV2Handler* ManifestV2HandlerFactory::GetForBrowserContext( |
| content::BrowserContext* browser_context) { |
| return static_cast<ManifestV2Handler*>( |
| GetServiceForBrowserContext(browser_context, /*create=*/true)); |
| } |
| |
| content::BrowserContext* ManifestV2HandlerFactory::GetBrowserContextToUse( |
| content::BrowserContext* browser_context) const { |
| return ExtensionsBrowserClient::Get() |
| ->GetContextRedirectedToOriginalWithoutAshInternals(browser_context); |
| } |
| |
| std::unique_ptr<KeyedService> |
| ManifestV2HandlerFactory::BuildServiceInstanceForBrowserContext( |
| content::BrowserContext* context) const { |
| return std::make_unique<ManifestV2Handler>(context); |
| } |
| |
| bool ManifestV2HandlerFactory::ServiceIsCreatedWithBrowserContext() const { |
| return true; |
| } |
| |
| // Returns true if legacy extensions should be disabled. |
| bool ShouldDisableLegacyExtensions() { |
| if (g_allow_mv2_for_testing) { |
| // We allow legacy MV2 extensions for testing purposes. |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| ManifestV2Handler::ManifestV2Handler(content::BrowserContext* browser_context) |
| : browser_context_(browser_context) { |
| registry_observation_.Observe(ExtensionRegistry::Get(browser_context)); |
| |
| ExtensionSystem::Get(browser_context) |
| ->ready() |
| .Post(FROM_HERE, |
| base::BindOnce(&ManifestV2Handler::OnExtensionSystemReady, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| ManifestV2Handler::~ManifestV2Handler() = default; |
| |
| // static |
| ManifestV2Handler* ManifestV2Handler::Get( |
| content::BrowserContext* browser_context) { |
| return static_cast<ManifestV2HandlerFactory*>(GetFactory()) |
| ->GetForBrowserContext(browser_context); |
| } |
| |
| // static |
| BrowserContextKeyedServiceFactory* ManifestV2Handler::GetFactory() { |
| static base::NoDestructor<ManifestV2HandlerFactory> g_factory; |
| return g_factory.get(); |
| } |
| |
| bool ManifestV2Handler::IsExtensionAffected(const Extension& extension) { |
| return impact_checker_.IsExtensionAffected(extension); |
| } |
| |
| bool ManifestV2Handler::ShouldBlockExtensionInstallation( |
| int manifest_version, |
| Manifest::Type manifest_type, |
| mojom::ManifestLocation manifest_location) { |
| if (!ShouldDisableLegacyExtensions()) { |
| return false; |
| } |
| |
| // Otherwise, if the extension is affected by the deprecation, it should be |
| // blocked. |
| return impact_checker_.IsExtensionAffected(manifest_version, manifest_type, |
| manifest_location); |
| } |
| |
| bool ManifestV2Handler::ShouldBlockExtensionEnable(const Extension& extension) { |
| if (!ShouldDisableLegacyExtensions()) { |
| return false; |
| } |
| |
| return impact_checker_.IsExtensionAffected( |
| extension.manifest_version(), extension.GetType(), extension.location()); |
| } |
| |
| bool ManifestV2Handler::DidUserAcknowledgeNoticeGlobally() { |
| return extension_prefs()->GetPrefAsBoolean( |
| kMV2DeprecationUnsupportedAcknowledgedGloballyPref); |
| } |
| |
| void ManifestV2Handler::MarkNoticeAsAcknowledgedGlobally() { |
| extension_prefs()->SetBooleanPref( |
| kMV2DeprecationUnsupportedAcknowledgedGloballyPref, true); |
| } |
| |
| ExtensionPrefs* ManifestV2Handler::extension_prefs() { |
| if (!extension_prefs_) { |
| extension_prefs_ = ExtensionPrefs::Get(browser_context_); |
| } |
| return extension_prefs_; |
| } |
| |
| void ManifestV2Handler::OnExtensionSystemReady() { |
| CheckDisabledExtensions(); |
| DisableAffectedExtensions(); |
| |
| EmitMetricsForProfileReady(); |
| } |
| |
| void ManifestV2Handler::DisableAffectedExtensions() { |
| if (!ShouldDisableLegacyExtensions()) { |
| return; |
| } |
| |
| ExtensionRegistry* extension_registry = |
| ExtensionRegistry::Get(browser_context_); |
| std::set<scoped_refptr<const Extension>> extensions_to_disable; |
| |
| // Disable all applicable MV2 extensions. |
| for (const auto& extension : extension_registry->enabled_extensions()) { |
| if (!impact_checker_.IsExtensionAffected(*extension)) { |
| continue; |
| } |
| |
| extensions_to_disable.insert(extension); |
| } |
| |
| auto* registrar = ExtensionRegistrar::Get(browser_context_); |
| for (const auto& extension : extensions_to_disable) { |
| registrar->DisableExtension( |
| extension->id(), |
| {disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION}); |
| } |
| } |
| |
| void ManifestV2Handler::CheckDisabledExtensions() { |
| ExtensionRegistry* extension_registry = |
| ExtensionRegistry::Get(browser_context_); |
| ExtensionSet disabled_extensions; |
| // Loop through all disabled extensions. For each, check if they should be |
| // re-enabled (e.g. because they've updated to MV3 or a change in policy |
| // settings). |
| // Use a copy of the set to avoid changing the set while iterating. |
| disabled_extensions.InsertAll(extension_registry->disabled_extensions()); |
| for (const auto& extension : disabled_extensions) { |
| MaybeReEnableExtension(*extension); |
| } |
| } |
| |
| void ManifestV2Handler::MaybeReEnableExtension(const Extension& extension) { |
| if (!extension_prefs()->HasDisableReason( |
| extension.id(), |
| disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION)) { |
| // We only care about extensions that were disabled for this reason. |
| return; |
| } |
| |
| // Check if the extension is still affected *and* whether the environment is |
| // still one in which extensions should be disabled. It's possible the global |
| // state changed (e.g. in testing), in which case extensions should be |
| // re-enabled. |
| if (impact_checker_.IsExtensionAffected(extension) && |
| ShouldDisableLegacyExtensions()) { |
| return; |
| } |
| |
| // Remove the disable reason (possibly re-enabling the extension). |
| ExtensionRegistrar::Get(browser_context_) |
| ->RemoveDisableReasonAndMaybeEnable( |
| extension.id(), disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION); |
| } |
| |
| void ManifestV2Handler::EmitMetricsForProfileReady() { |
| if (!ShouldDisableLegacyExtensions()) { |
| // Don't bother reporting MV2-specific metrics if the user isn't in an |
| // environment in which extensions could be disabled. |
| return; |
| } |
| |
| if (!ExtensionsBrowserClient::Get()->CanUseNonComponentExtensions( |
| browser_context_)) { |
| // Don't report metrics if the user can't install extensions in this |
| // profile. |
| return; |
| } |
| |
| ExtensionRegistry* extension_registry = |
| ExtensionRegistry::Get(browser_context_); |
| |
| auto emit_state_for_mv2_extension = [this](const Extension& extension, |
| bool is_enabled) { |
| if (extension.manifest_version() != 2) { |
| return; |
| } |
| |
| if (extension.GetType() != Manifest::Type::kExtension && |
| extension.GetType() != Manifest::Type::kLoginScreenExtension) { |
| return; |
| } |
| |
| MV2ExtensionState extension_state = MV2ExtensionState::kUnaffected; |
| if (!impact_checker_.IsExtensionAffected(extension)) { |
| extension_state = MV2ExtensionState::kUnaffected; |
| } else if (extension_prefs()->HasDisableReason( |
| extension.id(), |
| disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION)) { |
| CHECK(!is_enabled); |
| extension_state = MV2ExtensionState::kHardDisabled; |
| } else { |
| extension_state = MV2ExtensionState::kOther; |
| } |
| |
| std::string histogram_name = |
| base::StringPrintf("Extensions.MV2Deprecation.MV2ExtensionState.%s", |
| GetHistogramManifestLocation(extension.location())); |
| |
| base::UmaHistogramEnumeration(histogram_name, extension_state); |
| }; |
| |
| for (const auto& extension : extension_registry->enabled_extensions()) { |
| emit_state_for_mv2_extension(*extension, /*is_enabled=*/true); |
| } |
| for (const auto& extension : extension_registry->disabled_extensions()) { |
| emit_state_for_mv2_extension(*extension, /*is_enabled=*/false); |
| } |
| } |
| |
| void ManifestV2Handler::OnExtensionInstalled( |
| content::BrowserContext* browser_context, |
| const Extension* extension, |
| bool is_update) { |
| if (!is_update) { |
| // We would only ever re-enable a disabled extension if it was already |
| // installed. No need to look at new installs. |
| return; |
| } |
| |
| MaybeReEnableExtension(*extension); |
| } |
| |
| void ManifestV2Handler::DisableAffectedExtensionsForTesting() { // IN-TEST |
| DisableAffectedExtensions(); |
| } |
| |
| void ManifestV2Handler::EmitMetricsForProfileReadyForTesting() { // IN-TEST |
| EmitMetricsForProfileReady(); |
| } |
| |
| base::AutoReset<bool> |
| ManifestV2Handler::AllowMV2ExtensionsForTesting( // IN-TEST |
| base::PassKey<ScopedTestMV2Enabler> pass_key) { |
| return base::AutoReset<bool>(&g_allow_mv2_for_testing, true); |
| } |
| |
| } // namespace extensions |