diff --git a/doc/06-config.md b/doc/06-config.md index 47e1a6f5e..70caf4432 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 93253b0b3..9fb0be111 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -404,6 +404,17 @@ class Factory // 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 ($composer instanceof Composer && 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 ($composer instanceof Composer) { $globalComposer = null; if (realpath($config->get('home')) !== $cwd) { @@ -416,17 +427,6 @@ class Factory $pm->loadInstalledPlugins(); } - // init locker if possible - if ($composer instanceof Composer && 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 bb87d6beb..751130e7f 100644 --- a/src/Composer/Installer/PluginInstaller.php +++ b/src/Composer/Installer/PluginInstaller.php @@ -43,6 +43,19 @@ class PluginInstaller extends LibraryInstaller 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 1763f42b4..12101b643 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -318,6 +318,16 @@ class Locker return $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 b93164168..f83353829 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -18,6 +18,7 @@ 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\Version\VersionParser; use Composer\PartialComposer; @@ -75,9 +76,8 @@ class PluginManager $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); } /** @@ -648,12 +648,12 @@ class PluginManager } /** - * @param array|bool|null $allowPluginsConfig + * @param array|bool $allowPluginsConfig * @return array|null */ - private function parseAllowedPlugins($allowPluginsConfig): ?array + private function parseAllowedPlugins($allowPluginsConfig, ?Locker $locker = null): ?array { - if (null === $allowPluginsConfig) { + if (array() === $allowPluginsConfig && $locker !== null && $locker->isLocked() && version_compare($locker->getPluginApi(), '2.2.0', '<')) { return null; } @@ -674,22 +674,28 @@ class PluginManager } /** + * @internal + * * @param string $package * @param bool $isGlobalPlugin * @return bool */ - private function isPluginAllowed(string $package, bool $isGlobalPlugin): bool + public function isPluginAllowed(string $package, bool $isGlobalPlugin): bool { - 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; @@ -709,59 +715,54 @@ class PluginManager return false; } - if (!isset($warned[$package])) { - if ($this->io->isInteractive()) { - $composer = $isGlobalPlugin && $this->globalComposer !== null ? $this->globalComposer : $this->composer; + 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'); - $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) { - $default = 'd'; - } - - 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,?] ', $default)) { - 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: - $attempts++; - $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; - } + $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'); + $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 "'.$package.'" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?] ', $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') { + $composer->getConfig()->getConfigSource()->addConfigSetting('allow-plugins.'.$package, $allow); + } + + return $allow; + + case '?': + default: + $attempts++; + $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' + ); } } diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 5d22400e8..cd717fa24 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -101,9 +101,9 @@ class GitHub $this->io->writeError(sprintf('Tokens will be stored in plain text in "%s" for future use by Composer.', $this->config->getAuthConfigSource()->getName())); $this->io->writeError('For additional information, check https://getcomposer.org/doc/articles/authentication-for-private-packages.md#github-oauth'); - $token = trim($this->io->askAndHideAnswer('Token (hidden): ')); + $token = trim((string) $this->io->askAndHideAnswer('Token (hidden): ')); - if (!$token) { + if ($token === '') { $this->io->writeError('No token given, aborting.'); $this->io->writeError('You can also add it manually later by using "composer config --global --auth github-oauth.github.com "');