PluginManager.php
Namespace
Composer\PluginFile
-
vendor/
composer/ composer/ src/ Composer/ Plugin/ PluginManager.php
View source
<?php
declare (strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Plugin;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Installer\InstallerInterface;
use Composer\IO\IOInterface;
use Composer\Package\BasePackage;
use Composer\Package\CompletePackage;
use Composer\Package\Locker;
use Composer\Package\Package;
use Composer\Package\RootPackageInterface;
use Composer\Package\Version\VersionParser;
use Composer\PartialComposer;
use Composer\Pcre\Preg;
use Composer\Repository\RepositoryInterface;
use Composer\Repository\InstalledRepository;
use Composer\Repository\RepositoryUtils;
use Composer\Repository\RootPackageRepository;
use Composer\Package\PackageInterface;
use Composer\Package\Link;
use Composer\Semver\Constraint\Constraint;
use Composer\Plugin\Capability\Capability;
use Composer\Util\PackageSorter;
/**
* Plugin manager
*
* @author Nils Adermann <naderman@naderman.de>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class PluginManager {
/** @var Composer */
protected $composer;
/** @var IOInterface */
protected $io;
/** @var PartialComposer|null */
protected $globalComposer;
/** @var VersionParser */
protected $versionParser;
/** @var bool|'local'|'global' */
protected $disablePlugins = false;
/** @var array<PluginInterface> */
protected $plugins = [];
/** @var array<string, PluginInterface|InstallerInterface> */
protected $registeredPlugins = [];
/**
* @var array<non-empty-string, bool>|null
*/
private $allowPluginRules;
/**
* @var array<non-empty-string, bool>|null
*/
private $allowGlobalPluginRules;
/** @var bool */
private $runningInGlobalDir = false;
/** @var int */
private static $classCounter = 0;
/**
* @param bool|'local'|'global' $disablePlugins Whether plugins should not be loaded, can be set to local or global to only disable local/global plugins
*/
public function __construct(IOInterface $io, Composer $composer, ?PartialComposer $globalComposer = null, $disablePlugins = false) {
$this->io = $io;
$this->composer = $composer;
$this->globalComposer = $globalComposer;
$this->versionParser = new VersionParser();
$this->disablePlugins = $disablePlugins;
$this->allowPluginRules = $this->parseAllowedPlugins($composer->getConfig()
->get('allow-plugins'), $composer->getLocker());
$this->allowGlobalPluginRules = $this->parseAllowedPlugins($globalComposer !== null ? $globalComposer->getConfig()
->get('allow-plugins') : false);
}
public function setRunningInGlobalDir(bool $runningInGlobalDir) : void {
$this->runningInGlobalDir = $runningInGlobalDir;
}
/**
* Loads all plugins from currently installed plugin packages
*/
public function loadInstalledPlugins() : void {
if (!$this->arePluginsDisabled('local')) {
$repo = $this->composer
->getRepositoryManager()
->getLocalRepository();
$this->loadRepository($repo, false, $this->composer
->getPackage());
}
if ($this->globalComposer !== null && !$this->arePluginsDisabled('global')) {
$this->loadRepository($this->globalComposer
->getRepositoryManager()
->getLocalRepository(), true);
}
}
/**
* Deactivate all plugins from currently installed plugin packages
*/
public function deactivateInstalledPlugins() : void {
if (!$this->arePluginsDisabled('local')) {
$repo = $this->composer
->getRepositoryManager()
->getLocalRepository();
$this->deactivateRepository($repo, false);
}
if ($this->globalComposer !== null && !$this->arePluginsDisabled('global')) {
$this->deactivateRepository($this->globalComposer
->getRepositoryManager()
->getLocalRepository(), true);
}
}
/**
* Gets all currently active plugin instances
*
* @return array<PluginInterface> plugins
*/
public function getPlugins() : array {
return $this->plugins;
}
/**
* Gets global composer or null when main composer is not fully loaded
*/
public function getGlobalComposer() : ?PartialComposer {
return $this->globalComposer;
}
/**
* Register a plugin package, activate it etc.
*
* If it's of type composer-installer it is registered as an installer
* instead for BC
*
* @param bool $failOnMissingClasses By default this silently skips plugins that can not be found, but if set to true it fails with an exception
* @param bool $isGlobalPlugin Set to true to denote plugins which are installed in the global Composer directory
*
* @throws \UnexpectedValueException
*/
public function registerPackage(PackageInterface $package, bool $failOnMissingClasses = false, bool $isGlobalPlugin = false) : void {
if ($this->arePluginsDisabled($isGlobalPlugin ? 'global' : 'local')) {
$this->io
->writeError('<warning>The "' . $package->getName() . '" plugin was not loaded as plugins are disabled.</warning>');
return;
}
if ($package->getType() === 'composer-plugin') {
$requiresComposer = null;
foreach ($package->getRequires() as $link) {
/** @var Link $link */
if ('composer-plugin-api' === $link->getTarget()) {
$requiresComposer = $link->getConstraint();
break;
}
}
if (!$requiresComposer) {
throw new \RuntimeException("Plugin " . $package->getName() . " is missing a require statement for a version of the composer-plugin-api package.");
}
$currentPluginApiVersion = $this->getPluginApiVersion();
$currentPluginApiConstraint = new Constraint('==', $this->versionParser
->normalize($currentPluginApiVersion));
if ($requiresComposer->getPrettyString() === $this->getPluginApiVersion()) {
$this->io
->writeError('<warning>The "' . $package->getName() . '" plugin requires composer-plugin-api ' . $this->getPluginApiVersion() . ', this *WILL* break in the future and it should be fixed ASAP (require ^' . $this->getPluginApiVersion() . ' instead for example).</warning>');
}
elseif (!$requiresComposer->matches($currentPluginApiConstraint)) {
$this->io
->writeError('<warning>The "' . $package->getName() . '" plugin ' . ($isGlobalPlugin || $this->runningInGlobalDir ? '(installed globally) ' : '') . 'was skipped because it requires a Plugin API version ("' . $requiresComposer->getPrettyString() . '") that does not match your Composer installation ("' . $currentPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.</warning>');
return;
}
if ($package->getName() === 'symfony/flex' && Preg::isMatch('{^[0-9.]+$}', $package->getVersion()) && version_compare($package->getVersion(), '1.9.8', '<')) {
$this->io
->writeError('<warning>The "' . $package->getName() . '" plugin ' . ($isGlobalPlugin || $this->runningInGlobalDir ? '(installed globally) ' : '') . 'was skipped because it is not compatible with Composer 2+. Make sure to update it to version 1.9.8 or greater.</warning>');
return;
}
}
if (!$this->isPluginAllowed($package->getName(), $isGlobalPlugin, true === ($package->getExtra()['plugin-optional'] ?? false))) {
$this->io
->writeError('Skipped loading "' . $package->getName() . '" ' . ($isGlobalPlugin || $this->runningInGlobalDir ? '(installed globally) ' : '') . 'as it is not in config.allow-plugins', true, IOInterface::DEBUG);
return;
}
$oldInstallerPlugin = $package->getType() === 'composer-installer';
if (isset($this->registeredPlugins[$package->getName()])) {
return;
}
$extra = $package->getExtra();
if (empty($extra['class'])) {
throw new \UnexpectedValueException('Error while installing ' . $package->getPrettyName() . ', composer-plugin packages should have a class defined in their extra key to be usable.');
}
$classes = is_array($extra['class']) ? $extra['class'] : [
$extra['class'],
];
$localRepo = $this->composer
->getRepositoryManager()
->getLocalRepository();
$globalRepo = $this->globalComposer !== null ? $this->globalComposer
->getRepositoryManager()
->getLocalRepository() : null;
$rootPackage = clone $this->composer
->getPackage();
// clear files autoload rules from the root package as the root dependencies are not
// necessarily all present yet when booting this runtime autoloader
$rootPackageAutoloads = $rootPackage->getAutoload();
$rootPackageAutoloads['files'] = [];
$rootPackage->setAutoload($rootPackageAutoloads);
$rootPackageAutoloads = $rootPackage->getDevAutoload();
$rootPackageAutoloads['files'] = [];
$rootPackage->setDevAutoload($rootPackageAutoloads);
unset($rootPackageAutoloads);
$rootPackageRepo = new RootPackageRepository($rootPackage);
$installedRepo = new InstalledRepository([
$localRepo,
$rootPackageRepo,
]);
if ($globalRepo) {
$installedRepo->addRepository($globalRepo);
}
$autoloadPackages = [
$package->getName() => $package,
];
$autoloadPackages = $this->collectDependencies($installedRepo, $autoloadPackages, $package);
$generator = $this->composer
->getAutoloadGenerator();
$autoloads = [
[
$rootPackage,
'',
],
];
foreach ($autoloadPackages as $autoloadPackage) {
if ($autoloadPackage === $rootPackage) {
continue;
}
$installPath = $this->getInstallPath($autoloadPackage, $globalRepo && $globalRepo->hasPackage($autoloadPackage));
if ($installPath === null) {
continue;
}
$autoloads[] = [
$autoloadPackage,
$installPath,
];
}
$map = $generator->parseAutoloads($autoloads, $rootPackage);
$classLoader = $generator->createLoader($map, $this->composer
->getConfig()
->get('vendor-dir'));
$classLoader->register(false);
foreach ($map['files'] as $fileIdentifier => $file) {
// exclude laminas/laminas-zendframework-bridge:src/autoload.php as it breaks Composer in some conditions
// see https://github.com/composer/composer/issues/10349 and https://github.com/composer/composer/issues/10401
// this hack can be removed once this deprecated package stop being installed
if ($fileIdentifier === '7e9bd612cc444b3eed788ebbe46263a0') {
continue;
}
\Composer\Autoload\composerRequire($fileIdentifier, $file);
}
foreach ($classes as $class) {
if (class_exists($class, false)) {
$class = trim($class, '\\');
$path = $classLoader->findFile($class);
$code = file_get_contents($path);
$separatorPos = strrpos($class, '\\');
$className = $class;
if ($separatorPos) {
$className = substr($class, $separatorPos + 1);
}
$code = Preg::replace('{^((?:(?:final|readonly)\\s+)*(?:\\s*))class\\s+(' . preg_quote($className) . ')}mi', '$1class $2_composer_tmp' . self::$classCounter, $code, 1);
$code = strtr($code, [
'__FILE__' => var_export($path, true),
'__DIR__' => var_export(dirname($path), true),
'__CLASS__' => var_export($class, true),
]);
$code = Preg::replace('/^\\s*<\\?(php)?/i', '', $code, 1);
eval($code);
$class .= '_composer_tmp' . self::$classCounter;
self::$classCounter++;
}
if ($oldInstallerPlugin) {
if (!is_a($class, 'Composer\\Installer\\InstallerInterface', true)) {
throw new \RuntimeException('Could not activate plugin "' . $package->getName() . '" as "' . $class . '" does not implement Composer\\Installer\\InstallerInterface');
}
$this->io
->writeError('<warning>Loading "' . $package->getName() . '" ' . ($isGlobalPlugin || $this->runningInGlobalDir ? '(installed globally) ' : '') . 'which is a legacy composer-installer built for Composer 1.x, it is likely to cause issues as you are running Composer 2.x.</warning>');
$installer = new $class($this->io, $this->composer);
$this->composer
->getInstallationManager()
->addInstaller($installer);
$this->registeredPlugins[$package->getName()] = $installer;
}
elseif (class_exists($class)) {
if (!is_a($class, 'Composer\\Plugin\\PluginInterface', true)) {
throw new \RuntimeException('Could not activate plugin "' . $package->getName() . '" as "' . $class . '" does not implement Composer\\Plugin\\PluginInterface');
}
$plugin = new $class();
$this->addPlugin($plugin, $isGlobalPlugin, $package);
$this->registeredPlugins[$package->getName()] = $plugin;
}
elseif ($failOnMissingClasses) {
throw new \UnexpectedValueException('Plugin ' . $package->getName() . ' could not be initialized, class not found: ' . $class);
}
}
}
/**
* Deactivates a plugin package
*
* If it's of type composer-installer it is unregistered from the installers
* instead for BC
*
* @throws \UnexpectedValueException
*/
public function deactivatePackage(PackageInterface $package) : void {
if (!isset($this->registeredPlugins[$package->getName()])) {
return;
}
$plugin = $this->registeredPlugins[$package->getName()];
unset($this->registeredPlugins[$package->getName()]);
if ($plugin instanceof InstallerInterface) {
$this->composer
->getInstallationManager()
->removeInstaller($plugin);
}
else {
$this->removePlugin($plugin);
}
}
/**
* Uninstall a plugin package
*
* If it's of type composer-installer it is unregistered from the installers
* instead for BC
*
* @throws \UnexpectedValueException
*/
public function uninstallPackage(PackageInterface $package) : void {
if (!isset($this->registeredPlugins[$package->getName()])) {
return;
}
$plugin = $this->registeredPlugins[$package->getName()];
if ($plugin instanceof InstallerInterface) {
$this->deactivatePackage($package);
}
else {
unset($this->registeredPlugins[$package->getName()]);
$this->removePlugin($plugin);
$this->uninstallPlugin($plugin);
}
}
/**
* Returns the version of the internal composer-plugin-api package.
*/
protected function getPluginApiVersion() : string {
return PluginInterface::PLUGIN_API_VERSION;
}
/**
* Adds a plugin, activates it and registers it with the event dispatcher
*
* Ideally plugin packages should be registered via registerPackage, but if you use Composer
* programmatically and want to register a plugin class directly this is a valid way
* to do it.
*
* @param PluginInterface $plugin plugin instance
* @param ?PackageInterface $sourcePackage Package from which the plugin comes from
*/
public function addPlugin(PluginInterface $plugin, bool $isGlobalPlugin = false, ?PackageInterface $sourcePackage = null) : void {
if ($this->arePluginsDisabled($isGlobalPlugin ? 'global' : 'local')) {
return;
}
if ($sourcePackage === null) {
trigger_error('Calling PluginManager::addPlugin without $sourcePackage is deprecated, if you are using this please get in touch with us to explain the use case', E_USER_DEPRECATED);
}
elseif (!$this->isPluginAllowed($sourcePackage->getName(), $isGlobalPlugin, true === ($sourcePackage->getExtra()['plugin-optional'] ?? false))) {
$this->io
->writeError('Skipped loading "' . get_class($plugin) . ' from ' . $sourcePackage->getName() . '" ' . ($isGlobalPlugin || $this->runningInGlobalDir ? '(installed globally) ' : '') . ' as it is not in config.allow-plugins', true, IOInterface::DEBUG);
return;
}
$details = [];
if ($sourcePackage) {
$details[] = 'from ' . $sourcePackage->getName();
}
if ($isGlobalPlugin || $this->runningInGlobalDir) {
$details[] = 'installed globally';
}
$this->io
->writeError('Loading plugin ' . get_class($plugin) . ($details ? ' (' . implode(', ', $details) . ')' : ''), true, IOInterface::DEBUG);
$this->plugins[] = $plugin;
$plugin->activate($this->composer, $this->io);
if ($plugin instanceof EventSubscriberInterface) {
$this->composer
->getEventDispatcher()
->addSubscriber($plugin);
}
}
/**
* Removes a plugin, deactivates it and removes any listener the plugin has set on the plugin instance
*
* Ideally plugin packages should be deactivated via deactivatePackage, but if you use Composer
* programmatically and want to deregister a plugin class directly this is a valid way
* to do it.
*
* @param PluginInterface $plugin plugin instance
*/
public function removePlugin(PluginInterface $plugin) : void {
$index = array_search($plugin, $this->plugins, true);
if ($index === false) {
return;
}
$this->io
->writeError('Unloading plugin ' . get_class($plugin), true, IOInterface::DEBUG);
unset($this->plugins[$index]);
$plugin->deactivate($this->composer, $this->io);
$this->composer
->getEventDispatcher()
->removeListener($plugin);
}
/**
* Notifies a plugin it is being uninstalled and should clean up
*
* Ideally plugin packages should be uninstalled via uninstallPackage, but if you use Composer
* programmatically and want to deregister a plugin class directly this is a valid way
* to do it.
*
* @param PluginInterface $plugin plugin instance
*/
public function uninstallPlugin(PluginInterface $plugin) : void {
$this->io
->writeError('Uninstalling plugin ' . get_class($plugin), true, IOInterface::DEBUG);
$plugin->uninstall($this->composer, $this->io);
}
/**
* Load all plugins and installers from a repository
*
* If a plugin requires another plugin, the required one will be loaded first
*
* Note that plugins in the specified repository that rely on events that
* have fired prior to loading will be missed. This means you likely want to
* call this method as early as possible.
*
* @param RepositoryInterface $repo Repository to scan for plugins to install
*
* @phpstan-param ($isGlobalRepo is true ? null : RootPackageInterface) $rootPackage
*
* @throws \RuntimeException
*/
private function loadRepository(RepositoryInterface $repo, bool $isGlobalRepo, ?RootPackageInterface $rootPackage = null) : void {
$packages = $repo->getPackages();
$weights = [];
foreach ($packages as $package) {
if ($package->getType() === 'composer-plugin') {
$extra = $package->getExtra();
if ($package->getName() === 'composer/installers' || true === ($extra['plugin-modifies-install-path'] ?? false)) {
$weights[$package->getName()] = -10000;
}
}
}
$sortedPackages = PackageSorter::sortPackages($packages, $weights);
if (!$isGlobalRepo) {
$requiredPackages = RepositoryUtils::filterRequiredPackages($packages, $rootPackage, true);
}
foreach ($sortedPackages as $package) {
if (!$package instanceof CompletePackage) {
continue;
}
if (!in_array($package->getType(), [
'composer-plugin',
'composer-installer',
], true)) {
continue;
}
if (!$isGlobalRepo && !in_array($package, $requiredPackages, true) && !$this->isPluginAllowed($package->getName(), false, true, false)) {
$this->io
->writeError('<warning>The "' . $package->getName() . '" plugin was not loaded as it is not listed in allow-plugins and is not required by the root package anymore.</warning>');
continue;
}
if ('composer-plugin' === $package->getType()) {
$this->registerPackage($package, false, $isGlobalRepo);
// Backward compatibility
}
elseif ('composer-installer' === $package->getType()) {
$this->registerPackage($package, false, $isGlobalRepo);
}
}
}
/**
* Deactivate all plugins and installers from a repository
*
* If a plugin requires another plugin, the required one will be deactivated last
*
* @param RepositoryInterface $repo Repository to scan for plugins to install
*/
private function deactivateRepository(RepositoryInterface $repo, bool $isGlobalRepo) : void {
$packages = $repo->getPackages();
$sortedPackages = array_reverse(PackageSorter::sortPackages($packages));
foreach ($sortedPackages as $package) {
if (!$package instanceof CompletePackage) {
continue;
}
if ('composer-plugin' === $package->getType()) {
$this->deactivatePackage($package);
// Backward compatibility
}
elseif ('composer-installer' === $package->getType()) {
$this->deactivatePackage($package);
}
}
}
/**
* Recursively generates a map of package names to packages for all deps
*
* @param InstalledRepository $installedRepo Set of local repos
* @param array<string, PackageInterface> $collected Current state of the map for recursion
* @param PackageInterface $package The package to analyze
*
* @return array<string, PackageInterface> Map of package names to packages
*/
private function collectDependencies(InstalledRepository $installedRepo, array $collected, PackageInterface $package) : array {
foreach ($package->getRequires() as $requireLink) {
foreach ($installedRepo->findPackagesWithReplacersAndProviders($requireLink->getTarget()) as $requiredPackage) {
if (!isset($collected[$requiredPackage->getName()])) {
$collected[$requiredPackage->getName()] = $requiredPackage;
$collected = $this->collectDependencies($installedRepo, $collected, $requiredPackage);
}
}
}
return $collected;
}
/**
* Retrieves the path a package is installed to.
*
* @param bool $global Whether this is a global package
*
* @return string|null Install path
*/
private function getInstallPath(PackageInterface $package, bool $global = false) : ?string {
if (!$global) {
return $this->composer
->getInstallationManager()
->getInstallPath($package);
}
assert(null !== $this->globalComposer);
return $this->globalComposer
->getInstallationManager()
->getInstallPath($package);
}
/**
* @throws \RuntimeException On empty or non-string implementation class name value
* @return null|string The fully qualified class of the implementation or null if Plugin is not of Capable type or does not provide it
*/
protected function getCapabilityImplementationClassName(PluginInterface $plugin, string $capability) : ?string {
if (!$plugin instanceof Capable) {
return null;
}
$capabilities = (array) $plugin->getCapabilities();
if (!empty($capabilities[$capability]) && is_string($capabilities[$capability]) && trim($capabilities[$capability])) {
return trim($capabilities[$capability]);
}
if (array_key_exists($capability, $capabilities) && (empty($capabilities[$capability]) || !is_string($capabilities[$capability]) || !trim($capabilities[$capability]))) {
throw new \UnexpectedValueException('Plugin ' . get_class($plugin) . ' provided invalid capability class name(s), got ' . var_export($capabilities[$capability], true));
}
return null;
}
/**
* @template CapabilityClass of Capability
* @param class-string<CapabilityClass> $capabilityClassName The fully qualified name of the API interface which the plugin may provide
* an implementation of.
* @param array<mixed> $ctorArgs Arguments passed to Capability's constructor.
* Keeping it an array will allow future values to be passed w\o changing the signature.
* @phpstan-param class-string<CapabilityClass> $capabilityClassName
* @phpstan-return null|CapabilityClass
*/
public function getPluginCapability(PluginInterface $plugin, $capabilityClassName, array $ctorArgs = []) : ?Capability {
if ($capabilityClass = $this->getCapabilityImplementationClassName($plugin, $capabilityClassName)) {
if (!class_exists($capabilityClass)) {
throw new \RuntimeException("Cannot instantiate Capability, as class {$capabilityClass} from plugin " . get_class($plugin) . " does not exist.");
}
$ctorArgs['plugin'] = $plugin;
$capabilityObj = new $capabilityClass($ctorArgs);
// FIXME these could use is_a and do the check *before* instantiating once drop support for php<5.3.9
if (!$capabilityObj instanceof Capability || !$capabilityObj instanceof $capabilityClassName) {
throw new \RuntimeException('Class ' . $capabilityClass . ' must implement both Composer\\Plugin\\Capability\\Capability and ' . $capabilityClassName . '.');
}
return $capabilityObj;
}
return null;
}
/**
* @template CapabilityClass of Capability
* @param class-string<CapabilityClass> $capabilityClassName The fully qualified name of the API interface which the plugin may provide
* an implementation of.
* @param array<mixed> $ctorArgs Arguments passed to Capability's constructor.
* Keeping it an array will allow future values to be passed w\o changing the signature.
* @return CapabilityClass[]
*/
public function getPluginCapabilities($capabilityClassName, array $ctorArgs = []) : array {
$capabilities = [];
foreach ($this->getPlugins() as $plugin) {
$capability = $this->getPluginCapability($plugin, $capabilityClassName, $ctorArgs);
if (null !== $capability) {
$capabilities[] = $capability;
}
}
return $capabilities;
}
/**
* @param array<string, bool>|bool $allowPluginsConfig
* @return array<non-empty-string, bool>|null
*/
private function parseAllowedPlugins($allowPluginsConfig, ?Locker $locker = null) : ?array {
if ([] === $allowPluginsConfig && $locker !== null && $locker->isLocked() && version_compare($locker->getPluginApi(), '2.2.0', '<')) {
return null;
}
if (true === $allowPluginsConfig) {
return [
'{}' => true,
];
}
if (false === $allowPluginsConfig) {
return [
'{}' => false,
];
}
$rules = [];
foreach ($allowPluginsConfig as $pattern => $allow) {
$rules[BasePackage::packageNameToRegexp($pattern)] = $allow;
}
return $rules;
}
/**
* @internal
*
* @param 'local'|'global' $type
* @return bool
*/
public function arePluginsDisabled($type) {
return $this->disablePlugins === true || $this->disablePlugins === $type;
}
/**
* @internal
*/
public function disablePlugins() : void {
$this->disablePlugins = true;
}
/**
* @internal
*/
public function isPluginAllowed(string $package, bool $isGlobalPlugin, bool $optional = false, bool $prompt = true) : bool {
if ($isGlobalPlugin) {
$rules =& $this->allowGlobalPluginRules;
}
else {
$rules =& $this->allowPluginRules;
}
// This is a BC mode for lock files created pre-Composer-2.2 where the expectation of
// an allow-plugins config being present cannot be made.
if ($rules === null) {
if (!$this->io
->isInteractive()) {
$this->io
->writeError('<warning>For additional security you should declare the allow-plugins config with a list of packages names that are allowed to run code. See https://getcomposer.org/allow-plugins</warning>');
$this->io
->writeError('<warning>This warning will become an exception once you run composer update!</warning>');
$rules = [
'{}' => true,
];
// if no config is defined we allow all plugins for BC
return true;
}
// keep going and prompt the user
$rules = [];
}
foreach ($rules as $pattern => $allow) {
if (Preg::isMatch($pattern, $package)) {
return $allow === true;
}
}
if ($package === 'composer/package-versions-deprecated') {
return false;
}
if ($this->io
->isInteractive() && $prompt) {
$composer = $isGlobalPlugin && $this->globalComposer !== null ? $this->globalComposer : $this->composer;
$this->io
->writeError('<warning>' . $package . ($isGlobalPlugin || $this->runningInGlobalDir ? ' (installed globally)' : '') . ' contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins</warning>');
$attempts = 0;
while (true) {
// do not allow more than 5 prints of the help message, at some point assume the
// input is not interactive and bail defaulting to a disabled plugin
$default = '?';
if ($attempts > 5) {
$this->io
->writeError('Too many failed prompts, aborting.');
break;
}
switch ($answer = $this->io
->ask('Do you trust "<fg=green;options=bold>' . $package . '</>" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [<comment>y,n,d,?</comment>] ', $default)) {
case 'y':
case 'n':
case 'd':
$allow = $answer === 'y';
// persist answer in current rules to avoid prompting again if the package gets reloaded
$rules[BasePackage::packageNameToRegexp($package)] = $allow;
// persist answer in composer.json if it wasn't simply discarded
if ($answer === 'y' || $answer === 'n') {
$allowPlugins = $composer->getConfig()
->get('allow-plugins');
if (is_array($allowPlugins)) {
$allowPlugins[$package] = $allow;
if ($composer->getConfig()
->get('sort-packages')) {
ksort($allowPlugins);
}
$composer->getConfig()
->getConfigSource()
->addConfigSetting('allow-plugins', $allowPlugins);
$composer->getConfig()
->merge([
'config' => [
'allow-plugins' => $allowPlugins,
],
]);
}
}
return $allow;
case '?':
default:
$attempts++;
$this->io
->writeError([
'y - add package to allow-plugins in composer.json and let it run immediately',
'n - add package (as disallowed) to allow-plugins in composer.json to suppress further prompts',
'd - discard this, do not change composer.json and do not allow the plugin to run',
'? - print help',
]);
break;
}
}
}
elseif ($optional) {
return false;
}
throw new PluginBlockedException($package . ($isGlobalPlugin || $this->runningInGlobalDir ? ' (installed globally)' : '') . ' contains a Composer plugin which is blocked by your allow-plugins config. You may add it to the list if you consider it safe.' . PHP_EOL . 'You can run "composer ' . ($isGlobalPlugin || $this->runningInGlobalDir ? 'global ' : '') . 'config --no-plugins allow-plugins.' . $package . ' [true|false]" to enable it (true) or disable it explicitly and suppress this exception (false)' . PHP_EOL . 'See https://getcomposer.org/allow-plugins');
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
PluginManager | Plugin manager |