class ComposerPluginsValidator
Validates the allowed Composer plugins, both in active and stage.
Composer plugins can make far-reaching changes on the filesystem. That is why they can cause Package Manager (more specifically the infrastructure it uses: php-tuf/composer-stager) to not work reliably; potentially even break a site!
This validator restricts the use of Composer plugins:
- Allowing all plugins to run indiscriminately is discouraged by Composer, but disallowed by this module (it is too risky): `config.allowed-plugins = true` is forbidden.
- Installed Composer plugins that are not allowed (in composer.json's `config.allowed-plugins ) are not executed by Composer, so these are safe.
- Installed Composer plugins that are allowed need to be either explicitly supported by this validator (they may still need their own validation to ensure their configuration is safe, for example Drupal core's vendor hardening plugin), or explicitly trusted by adding it to the `package_manager.settings` configuration's `additional_trusted_composer_plugins` list.
@todo Determine how other Composer plugins will be supported in https://drupal.org/i/3339417.
@internal This is an internal part of Package Manager and may be changed or removed at any time without warning. External code should not interact with this class.
Hierarchy
- class \Drupal\package_manager\Validator\ComposerPluginsValidator implements \Symfony\Component\EventDispatcher\EventSubscriberInterface uses \Drupal\Core\StringTranslation\StringTranslationTrait
Expanded class hierarchy of ComposerPluginsValidator
See also
https://getcomposer.org/doc/04-schema.md#type
https://getcomposer.org/doc/articles/plugins.md
File
-
core/
modules/ package_manager/ src/ Validator/ ComposerPluginsValidator.php, line 51
Namespace
Drupal\package_manager\ValidatorView source
final class ComposerPluginsValidator implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* Composer plugins known to modify other packages, but are validated.
*
* The validation guarantees they are safe to use.
*
* @var string[]
* Keys are Composer plugin package names, values are version constraints
* for those plugins that this validator explicitly supports.
*/
private const SUPPORTED_PLUGINS_THAT_DO_MODIFY = [
// @see \Drupal\package_manager\Validator\ComposerPatchesValidator
'cweagans/composer-patches' => '^1.7.3 || ^2',
// @see \Drupal\package_manager\PathExcluder\VendorHardeningExcluder
'drupal/core-vendor-hardening' => '*',
'php-http/discovery' => '*',
];
/**
* Composer plugins known to NOT modify other packages.
*
* @var string[]
* Keys are Composer plugin package names, values are version constraints
* for those plugins that this validator explicitly supports.
*/
private const SUPPORTED_PLUGINS_THAT_DO_NOT_MODIFY = [
'composer/installers' => '^2.0',
'dealerdirect/phpcodesniffer-composer-installer' => '^0.7.1 || ^1.0.0',
'drupal/core-composer-scaffold' => '*',
'drupal/core-project-message' => '*',
'phpstan/extension-installer' => '^1.1',
PhpTufValidator::PLUGIN_NAME => '^1',
];
/**
* The additional trusted Composer plugin package names.
*
* The package names are normalized.
*
* @var string[]
* Keys are package names, values are version constraints.
*/
private array $additionalTrustedComposerPlugins;
public function __construct(ConfigFactoryInterface $config_factory, ComposerInspector $inspector, PathLocator $pathLocator) {
$settings = $config_factory->get('package_manager.settings');
$this->additionalTrustedComposerPlugins = array_fill_keys(array_map([
__CLASS__,
'normalizePackageName',
], $settings->get('additional_trusted_composer_plugins')), '*');
}
/**
* Normalizes a package name.
*
* @param string $package_name
* A package name.
*
* @return string
* The normalized package name.
*/
private static function normalizePackageName(string $package_name) : string {
return strtolower($package_name);
}
/**
* Validates the allowed Composer plugins, both in active and stage.
*/
public function validate(PreOperationStageEvent $event) : void {
$stage = $event->stage;
// When about to copy the changes from the stage directory to the active
// directory, use the stage directory's composer instead of the active.
// Because composer plugins may be added or removed; the only thing that
// matters is the set of composer plugins that *will* apply — if a composer
// plugin is being removed, that's fine.
$dir = $event instanceof PreApplyEvent ? $stage->getStageDirectory() : $this->pathLocator
->getProjectRoot();
try {
$allowed_plugins = $this->inspector
->getAllowPluginsConfig($dir);
} catch (RuntimeException $exception) {
$event->addErrorFromThrowable($exception, $this->t('Unable to determine Composer <code>allow-plugins</code> setting.'));
return;
}
if ($allowed_plugins === TRUE) {
$event->addError([
$this->t('All composer plugins are allowed because <code>config.allow-plugins</code> is configured to <code>true</code>. This is an unacceptable security risk.'),
]);
return;
}
// TRICKY: additional trusted Composer plugins is listed first, to allow
// site owners who know what they're doing to use unsupported versions of
// supported Composer plugins.
$trusted_plugins = $this->additionalTrustedComposerPlugins + self::SUPPORTED_PLUGINS_THAT_DO_MODIFY + self::SUPPORTED_PLUGINS_THAT_DO_NOT_MODIFY;
assert(is_array($allowed_plugins));
// Only packages with `true` as a value are actually executed by Composer.
$allowed_plugins = array_keys(array_filter($allowed_plugins));
// The keys are normalized package names, and the values are the original,
// non-normalized package names.
$allowed_plugins = array_combine(array_map([
__CLASS__,
'normalizePackageName',
], $allowed_plugins), $allowed_plugins);
$installed_packages = $this->inspector
->getInstalledPackagesList($dir);
// Determine which plugins are both trusted by us, AND allowed by Composer's
// configuration.
$supported_plugins = array_intersect_key($allowed_plugins, $trusted_plugins);
// Create an array whose keys are the names of those plugins, and the values
// are their installed versions.
$supported_plugins_installed_versions = array_combine($supported_plugins, array_map(fn(string $name): ?string => $installed_packages[$name]?->version, $supported_plugins));
// Find the plugins whose installed versions aren't in the supported range.
$unsupported_installed_versions = array_filter($supported_plugins_installed_versions, fn(?string $version, string $name): bool => $version && !Semver::satisfies($version, $trusted_plugins[$name]), ARRAY_FILTER_USE_BOTH);
$untrusted_plugins = array_diff_key($allowed_plugins, $trusted_plugins);
$messages = array_map(fn(string $raw_name) => $this->t('<code>@name</code>', [
'@name' => $raw_name,
]), $untrusted_plugins);
foreach ($unsupported_installed_versions as $name => $installed_version) {
$messages[] = $this->t("<code>@name</code> is supported, but only version <code>@supported_version</code>, found <code>@installed_version</code>.", [
'@name' => $name,
'@supported_version' => $trusted_plugins[$name],
'@installed_version' => $installed_version,
]);
}
if ($messages) {
$summary = $this->formatPlural(count($messages), 'An unsupported Composer plugin was detected.', 'Unsupported Composer plugins were detected.');
$event->addError($messages, $summary);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() : array {
return [
PreCreateEvent::class => 'validate',
PreApplyEvent::class => 'validate',
StatusCheckEvent::class => 'validate',
];
}
}
Members
Title Sort descending | Modifiers | Object type | Summary | Overrides |
---|---|---|---|---|
ComposerPluginsValidator::$additionalTrustedComposerPlugins | private | property | The additional trusted Composer plugin package names. | |
ComposerPluginsValidator::getSubscribedEvents | public static | function | ||
ComposerPluginsValidator::normalizePackageName | private static | function | Normalizes a package name. | |
ComposerPluginsValidator::SUPPORTED_PLUGINS_THAT_DO_MODIFY | private | constant | Composer plugins known to modify other packages, but are validated. | |
ComposerPluginsValidator::SUPPORTED_PLUGINS_THAT_DO_NOT_MODIFY | private | constant | Composer plugins known to NOT modify other packages. | |
ComposerPluginsValidator::validate | public | function | Validates the allowed Composer plugins, both in active and stage. | |
ComposerPluginsValidator::__construct | public | function | ||
StringTranslationTrait::$stringTranslation | protected | property | The string translation service. | 3 |
StringTranslationTrait::formatPlural | protected | function | Formats a string containing a count of items. | |
StringTranslationTrait::getNumberOfPlurals | protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait::getStringTranslation | protected | function | Gets the string translation service. | |
StringTranslationTrait::setStringTranslation | public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait::t | protected | function | Translates a string to the current language or to a given language. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.