diff --git a/doc/06-config.md b/doc/06-config.md index c0b1a7d87660..4134d632e5a4 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -52,7 +52,15 @@ and **false** to disallow while suppressing further warnings and prompts. } ``` -You can also set the config option itself to `false` to disallow all plugins, or `true` to allow all plugins to run (NOT recommended). +You can also set the config option itself to `false` to disallow all plugins, or `true` to allow all plugins to run (NOT recommended). For example: + +```json +{ + "config": { + "allow-plugins": false + } +} +``` ## use-include-path diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 9288da19b914..9248071ca42e 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -427,6 +427,17 @@ public function createComposer(IOInterface $io, $localConfig = null, $disablePlu // add installers to the manager (must happen after download manager is created since they read it out of $composer) $this->createDefaultInstallers($im, $composer, $io, $process); + // init locker if possible + if ($fullLoad && isset($composerFile)) { + $lockFile = self::getLockFile($composerFile); + if (!$config->get('lock') && file_exists($lockFile)) { + $io->writeError(''.$lockFile.' is present but ignored as the "lock" config option is disabled.'); + } + + $locker = new Package\Locker($io, new JsonFile($config->get('lock') ? $lockFile : Platform::getDevNull(), null, $io), $im, file_get_contents($composerFile), $process); + $composer->setLocker($locker); + } + if ($fullLoad) { $globalComposer = null; if (realpath($config->get('home')) !== $cwd) { @@ -439,17 +450,6 @@ public function createComposer(IOInterface $io, $localConfig = null, $disablePlu $pm->loadInstalledPlugins(); } - // init locker if possible - if ($fullLoad && isset($composerFile)) { - $lockFile = self::getLockFile($composerFile); - if (!$config->get('lock') && file_exists($lockFile)) { - $io->writeError(''.$lockFile.' is present but ignored as the "lock" config option is disabled.'); - } - - $locker = new Package\Locker($io, new JsonFile($config->get('lock') ? $lockFile : Platform::getDevNull(), null, $io), $im, file_get_contents($composerFile), $process); - $composer->setLocker($locker); - } - if ($fullLoad) { $initEvent = new Event(PluginEvents::INIT); $composer->getEventDispatcher()->dispatch($initEvent->getName(), $initEvent); diff --git a/src/Composer/Installer/PluginInstaller.php b/src/Composer/Installer/PluginInstaller.php index 3fc09638befd..16bbd4b9651c 100644 --- a/src/Composer/Installer/PluginInstaller.php +++ b/src/Composer/Installer/PluginInstaller.php @@ -47,6 +47,19 @@ public function supports($packageType) return $packageType === 'composer-plugin' || $packageType === 'composer-installer'; } + /** + * @inheritDoc + */ + public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) + { + // fail install process early if it going to fail due to a plugin not being allowed + if ($type === 'install' || $type === 'update') { + $this->composer->getPluginManager()->isPluginAllowed($package->getName(), false); + } + + return parent::prepare($type, $package, $prevPackage); + } + /** * @inheritDoc */ diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 7911c4c2358b..7e001aa1374f 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -318,6 +318,16 @@ public function getAliases() return isset($lockData['aliases']) ? $lockData['aliases'] : array(); } + /** + * @return string + */ + public function getPluginApi() + { + $lockData = $this->getLockData(); + + return isset($lockData['plugin-api-version']) ? $lockData['plugin-api-version'] : '1.1.0'; + } + /** * @return array */ diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 4ffd590640ce..3cfb3fac90d4 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -18,6 +18,7 @@ use Composer\IO\IOInterface; use Composer\Package\BasePackage; use Composer\Package\CompletePackage; +use Composer\Package\Locker; use Composer\Package\Package; use Composer\Package\Version\VersionParser; use Composer\Pcre\Preg; @@ -82,9 +83,8 @@ public function __construct(IOInterface $io, Composer $composer, Composer $globa $this->globalComposer = $globalComposer; $this->versionParser = new VersionParser(); $this->disablePlugins = $disablePlugins; - - $this->allowPluginRules = $this->parseAllowedPlugins($composer->getConfig()->get('allow-plugins')); - $this->allowGlobalPluginRules = $this->parseAllowedPlugins($globalComposer !== null ? $globalComposer->getConfig()->get('allow-plugins') : false); + $this->allowPluginRules = $this->parseAllowedPlugins($composer->getConfig()->get('allow-plugins'), $composer->getLocker()); + $this->allowGlobalPluginRules = $this->parseAllowedPlugins($globalComposer !== null ? $globalComposer->getConfig()->get('allow-plugins') : false, $globalComposer !== null ? $globalComposer->getLocker() : null); } /** @@ -653,12 +653,12 @@ public function getPluginCapabilities($capabilityClassName, array $ctorArgs = ar } /** - * @param array|bool|null $allowPluginsConfig + * @param array|bool $allowPluginsConfig * @return array|null */ - private function parseAllowedPlugins($allowPluginsConfig) + private function parseAllowedPlugins($allowPluginsConfig, Locker $locker = null) { - if (null === $allowPluginsConfig) { + if (array() === $allowPluginsConfig && $locker !== null && $locker->isLocked() && version_compare($locker->getPluginApi(), '2.2.0', '<')) { return null; } @@ -679,22 +679,28 @@ private function parseAllowedPlugins($allowPluginsConfig) } /** + * @internal + * * @param string $package * @param bool $isGlobalPlugin * @return bool */ - private function isPluginAllowed($package, $isGlobalPlugin) + public function isPluginAllowed($package, $isGlobalPlugin) { - static $warned = array(); - $rules = $isGlobalPlugin ? $this->allowGlobalPluginRules : $this->allowPluginRules; + 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()) { - if (!isset($warned['all'])) { - $this->io->writeError('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'); - $this->io->writeError('You have until July 2022 to add the setting. Composer will then switch the default behavior to disallow all plugins.'); - $warned['all'] = true; - } + $this->io->writeError('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'); + $this->io->writeError('This warning will become an exception once you run composer update!'); + + $rules = array('{}' => true); // if no config is defined we allow all plugins for BC return true; @@ -714,50 +720,45 @@ private function isPluginAllowed($package, $isGlobalPlugin) return false; } - if (!isset($warned[$package])) { - if ($this->io->isInteractive()) { - $composer = $isGlobalPlugin && $this->globalComposer !== null ? $this->globalComposer : $this->composer; - - $this->io->writeError(''.$package.($isGlobalPlugin ? ' (installed globally)' : '').' contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins'); - while (true) { - switch ($answer = $this->io->ask('Do you trust "'.$package.'" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?] ', '?')) { - case 'y': - case 'n': - case 'd': - $allow = $answer === 'y'; - - // persist answer in current rules to avoid prompting again if the package gets reloaded - if ($isGlobalPlugin) { - $this->allowGlobalPluginRules[BasePackage::packageNameToRegexp($package)] = $allow; - } else { - $this->allowPluginRules[BasePackage::packageNameToRegexp($package)] = $allow; - } - - // persist answer in composer.json if it wasn't simply discarded - if ($answer === 'y' || $answer === 'n') { - $composer->getConfig()->getConfigSource()->addConfigSetting('allow-plugins.'.$package, $allow); - } - - return $allow; - - case '?': - default: - $this->io->writeError(array( - '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; - } + if ($this->io->isInteractive()) { + $composer = $isGlobalPlugin && $this->globalComposer !== null ? $this->globalComposer : $this->composer; + + $this->io->writeError(''.$package.($isGlobalPlugin ? ' (installed globally)' : '').' contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins'); + while (true) { + switch ($answer = $this->io->ask('Do you trust "'.$package.'" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?] ', + '?')) { + 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') { + $composer->getConfig()->getConfigSource()->addConfigSetting('allow-plugins.'.$package, $allow); + } + + return $allow; + + case '?': + default: + $this->io->writeError(array( + '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; } - } else { - $this->io->writeError(''.$package.($isGlobalPlugin ? ' (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. See https://getcomposer.org/allow-plugins'); - $this->io->writeError('You can run "composer '.($isGlobalPlugin ? 'global ' : '').'config --no-plugins allow-plugins.'.$package.' [true|false]" to enable it (true) or keep it disabled and suppress this warning (false)'); } - $warned[$package] = true; } - return false; + throw new \UnexpectedValueException( + $package.($isGlobalPlugin ? ' (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 ? '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' + ); } }