diff --git a/CHANGELOG.md b/CHANGELOG.md index 58adb6fec..43419d94a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### [2.3.10] 2022-07-13 + + * Fixed plugins from CWD/vendor being loaded in some cases like create-project or validate even though the target directory is outside of CWD (#10935) + * Fixed support for legacy (Composer 1.x, e.g. hirak/prestissimo) plugins which will not warn/error anymore if not in allow-plugins, as they are anyway not loaded (#10928) + * Fixed pre-install check for allowed plugins not taking --no-plugins into account (#10925) + * Fixed support for disable_functions containing disk_free_space (#10936) + * Fixed RootPackageRepository usages to always clone the root package to avoid interoperability issues with plugins (#10940) + ### [2.3.9] 2022-07-05 * Fixed non-interactive behavior of allow-plugins to throw instead of continue with a warning to avoid broken installs (#10920) @@ -111,6 +119,14 @@ * Fixed symlink creation in linux VM guest filesystems to be recognized by Windows (#10592) * Performance improvement in pool optimization step (#10585) +### [2.2.17] 2022-07-13 + + * Fixed plugins from CWD/vendor being loaded in some cases like create-project or validate even though the target directory is outside of CWD (#10935) + * Fixed support for legacy (Composer 1.x, e.g. hirak/prestissimo) plugins which will not warn/error anymore if not in allow-plugins, as they are anyway not loaded (#10928) + * Fixed pre-install check for allowed plugins not taking --no-plugins into account (#10925) + * Fixed support for disable_functions containing disk_free_space (#10936) + * Fixed RootPackageRepository usages to always clone the root package to avoid interoperability issues with plugins (#10940) + ### [2.2.16] 2022-07-05 * Fixed non-interactive behavior of allow-plugins to throw instead of continue with a warning to avoid broken installs (#10920) @@ -1574,6 +1590,7 @@ * Initial release +[2.3.10]: https://github.com/composer/composer/compare/2.3.9...2.3.10 [2.3.9]: https://github.com/composer/composer/compare/2.3.8...2.3.9 [2.3.8]: https://github.com/composer/composer/compare/2.3.7...2.3.8 [2.3.7]: https://github.com/composer/composer/compare/2.3.6...2.3.7 @@ -1586,6 +1603,7 @@ [2.3.0]: https://github.com/composer/composer/compare/2.3.0-RC2...2.3.0 [2.3.0-RC2]: https://github.com/composer/composer/compare/2.3.0-RC1...2.3.0-RC2 [2.3.0-RC1]: https://github.com/composer/composer/compare/2.2.9...2.3.0-RC1 +[2.2.17]: https://github.com/composer/composer/compare/2.2.16...2.2.17 [2.2.16]: https://github.com/composer/composer/compare/2.2.15...2.2.16 [2.2.15]: https://github.com/composer/composer/compare/2.2.14...2.2.15 [2.2.14]: https://github.com/composer/composer/compare/2.2.13...2.2.14 diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index bdd1aa7fb..ccf990fca 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -162,11 +162,11 @@ class Cache unlink($tempFileName); $message = sprintf( - 'Writing %1$s into cache failed after %2$u of %3$u bytes written, only %4$u bytes of free space available', + 'Writing %1$s into cache failed after %2$u of %3$u bytes written, only %4$s bytes of free space available', $tempFileName, $m[1], $m[2], - @disk_free_space(dirname($tempFileName)) + function_exists('disk_free_space') ? @disk_free_space(dirname($tempFileName)) : 'unknown' ); $this->io->writeError($message); diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index 9788ffa23..6bccf931d 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -60,7 +60,7 @@ abstract class BaseDependencyCommand extends BaseCommand $repos = []; - $repos[] = new RootPackageRepository($composer->getPackage()); + $repos[] = new RootPackageRepository(clone $composer->getPackage()); if ($input->getOption('locked')) { $locker = $composer->getLocker(); diff --git a/src/Composer/Command/CheckPlatformReqsCommand.php b/src/Composer/Command/CheckPlatformReqsCommand.php index 6854ee690..1053fc62c 100644 --- a/src/Composer/Command/CheckPlatformReqsCommand.php +++ b/src/Composer/Command/CheckPlatformReqsCommand.php @@ -77,7 +77,7 @@ EOT $requires[$require] = array($link); } - $installedRepo = new InstalledRepository(array($installedRepo, new RootPackageRepository($composer->getPackage()))); + $installedRepo = new InstalledRepository(array($installedRepo, new RootPackageRepository(clone $composer->getPackage()))); foreach ($installedRepo->getPackages() as $package) { if (in_array($package->getName(), $removePackages, true)) { continue; diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index fecdd4c89..9721656b9 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -26,6 +26,7 @@ use Composer\DependencyResolver\Operation\InstallOperation; use Composer\Package\Version\VersionSelector; use Composer\Package\AliasPackage; use Composer\Pcre\Preg; +use Composer\Plugin\PluginBlockedException; use Composer\Repository\RepositoryFactory; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; @@ -274,9 +275,15 @@ EOT $installer->disablePlugins(); } - $status = $installer->run(); - if (0 !== $status) { - return $status; + try { + $status = $installer->run(); + if (0 !== $status) { + return $status; + } + } catch (PluginBlockedException $e) { + $io->writeError('Hint: To allow running the config command recommended below before dependencies are installed, run create-project with --no-install.'); + $io->writeError('You can then cd into '.getcwd().', configure allow-plugins, and finally run a composer install to complete the process.'); + throw $e; } } @@ -409,15 +416,7 @@ EOT throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); } - $composerJson = array_merge( - // prevent version guessing from happening - array('version' => '1.0.0'), - $config->all(), - // ensure the vendor dir and its plugins does not get loaded if CWD/vendor has plugins in it - array('config' => array('vendor-dir' => Platform::getDevNull())) - ); - $factory = new Factory; - $composer = $factory->createComposer($io, $composerJson, $disablePlugins, Platform::getDevNull(), true, $disableScripts); + $composer = Factory::create($io, $config->all(), $disablePlugins, $disableScripts); $config = $composer->getConfig(); $rm = $composer->getRepositoryManager(); diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 255a2be21..4a4e2d465 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -377,6 +377,10 @@ EOT */ private function checkDiskSpace(Config $config) { + if (!function_exists('disk_free_space')) { + return true; + } + $minSpaceFree = 1024 * 1024; if ((($df = @disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree) || (($df = @disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree) diff --git a/src/Composer/Command/HomeCommand.php b/src/Composer/Command/HomeCommand.php index 8fc4bc9db..7b88a0877 100644 --- a/src/Composer/Command/HomeCommand.php +++ b/src/Composer/Command/HomeCommand.php @@ -166,7 +166,7 @@ EOT if ($composer) { return array_merge( - array(new RootPackageRepository($composer->getPackage())), // root package + array(new RootPackageRepository(clone $composer->getPackage())), // root package array($composer->getRepositoryManager()->getLocalRepository()), // installed packages $composer->getRepositoryManager()->getRepositories() // remotes ); diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index a49f1e063..aa66c3f6a 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -196,7 +196,7 @@ EOT $lockedRepo = null; if ($input->getOption('self')) { - $package = $this->requireComposer()->getPackage(); + $package = clone $this->requireComposer()->getPackage(); if ($input->getOption('name-only')) { $io->write($package->getName()); diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 82cc0b2a0..9964fe72c 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -410,7 +410,7 @@ class Application extends BaseApplication Silencer::suppress(); try { $composer = $this->getComposer(false, true); - if ($composer) { + if (null !== $composer && function_exists('disk_free_space')) { $config = $composer->getConfig(); $minSpaceFree = 1024 * 1024; diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 340f499b2..fba112be2 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -276,7 +276,7 @@ class Factory * @param IOInterface $io IO instance * @param array|string|null $localConfig either a configuration array or a filename to read from, if null it will * read from the default filename - * @param bool $disablePlugins Whether plugins should not be loaded + * @param bool|'local'|'global' $disablePlugins Whether plugins should not be loaded, can be set to local or global to only disable local/global plugins * @param bool $disableScripts Whether scripts should not be run * @param string|null $cwd * @param bool $fullLoad Whether to initialize everything or only main project stuff (used when loading the global composer) @@ -285,7 +285,7 @@ class Factory * @return Composer|PartialComposer Composer if $fullLoad is true, otherwise PartialComposer * @phpstan-return ($fullLoad is true ? Composer : PartialComposer) */ - public function createComposer(IOInterface $io, $localConfig = null, bool $disablePlugins = false, ?string $cwd = null, bool $fullLoad = true, bool $disableScripts = false) + public function createComposer(IOInterface $io, $localConfig = null, $disablePlugins = false, ?string $cwd = null, bool $fullLoad = true, bool $disableScripts = false) { $cwd = $cwd ?? Platform::getCwd(true); @@ -473,11 +473,15 @@ class Factory } /** + * @param bool|'local'|'global' $disablePlugins Whether plugins should not be loaded, can be set to local or global to only disable local/global plugins * @return PartialComposer|Composer|null By default PartialComposer, but Composer if $fullLoad is set to true * @phpstan-return ($fullLoad is true ? Composer|null : PartialComposer|null) */ - protected function createGlobalComposer(IOInterface $io, Config $config, bool $disablePlugins, bool $disableScripts, bool $fullLoad = false): ?PartialComposer + protected function createGlobalComposer(IOInterface $io, Config $config, $disablePlugins, bool $disableScripts, bool $fullLoad = false): ?PartialComposer { + // make sure if disable plugins was 'local' it is now turned off + $disablePlugins = $disablePlugins === 'global' || $disablePlugins === true; + $composer = null; try { $composer = $this->createComposer($io, $config->get('home') . '/composer.json', $disablePlugins, $config->get('home'), $fullLoad, $disableScripts); @@ -558,9 +562,10 @@ class Factory } /** + * @param bool|'local'|'global' $disablePlugins Whether plugins should not be loaded, can be set to local or global to only disable local/global plugins * @return Plugin\PluginManager */ - protected function createPluginManager(IOInterface $io, Composer $composer, PartialComposer $globalComposer = null, bool $disablePlugins = false): Plugin\PluginManager + protected function createPluginManager(IOInterface $io, Composer $composer, PartialComposer $globalComposer = null, $disablePlugins = false): Plugin\PluginManager { return new Plugin\PluginManager($io, $composer, $globalComposer, $disablePlugins); } @@ -608,14 +613,22 @@ class Factory * @param IOInterface $io IO instance * @param mixed $config either a configuration array or a filename to read from, if null it will read from * the default filename - * @param bool $disablePlugins Whether plugins should not be loaded + * @param bool|'local'|'global' $disablePlugins Whether plugins should not be loaded, can be set to local or global to only disable local/global plugins * @param bool $disableScripts Whether scripts should not be run * @return Composer */ - public static function create(IOInterface $io, $config = null, bool $disablePlugins = false, bool $disableScripts = false): Composer + public static function create(IOInterface $io, $config = null, $disablePlugins = false, bool $disableScripts = false): Composer { $factory = new static(); + // for BC reasons, if a config is passed in either as array or a path that is not the default composer.json path + // we disable local plugins as they really should not be loaded from CWD + // If you want to avoid this behavior, you should be calling createComposer directly with a $cwd arg set correctly + // to the path where the composer.json being loaded resides + if ($config !== null && $config !== self::getComposerFile() && $disablePlugins === false) { + $disablePlugins = 'local'; + } + return $factory->createComposer($io, $config, $disablePlugins, null, true, $disableScripts); } diff --git a/src/Composer/Installer/PluginInstaller.php b/src/Composer/Installer/PluginInstaller.php index 04fd446a7..0b34418db 100644 --- a/src/Composer/Installer/PluginInstaller.php +++ b/src/Composer/Installer/PluginInstaller.php @@ -49,7 +49,7 @@ class PluginInstaller extends LibraryInstaller public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) { // fail install process early if it is going to fail due to a plugin not being allowed - if (($type === 'install' || $type === 'update') && !$this->getPluginManager()->arePluginsDisabled()) { + if (($type === 'install' || $type === 'update') && !$this->getPluginManager()->arePluginsDisabled('local')) { $this->getPluginManager()->isPluginAllowed($package->getName(), false); } diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index cdefbfa43..509a33e1a 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -134,7 +134,12 @@ abstract class BasePackage implements PackageInterface public function setRepository(RepositoryInterface $repository): void { if ($this->repository && $repository !== $this->repository) { - throw new \LogicException('A package can only be added to one repository'); + throw new \LogicException(sprintf( + 'Package "%s" cannot be added to repository "%s" as it is already in repository "%s".', + $this->getPrettyName(), + $repository->getRepoName(), + $this->repository->getRepoName() + )); } $this->repository = $repository; } diff --git a/src/Composer/Plugin/PluginBlockedException.php b/src/Composer/Plugin/PluginBlockedException.php new file mode 100644 index 000000000..04a8db005 --- /dev/null +++ b/src/Composer/Plugin/PluginBlockedException.php @@ -0,0 +1,19 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Plugin; + +use UnexpectedValueException; + +class PluginBlockedException extends UnexpectedValueException +{ +} diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 36ca8a318..3f91832e5 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -48,7 +48,7 @@ class PluginManager protected $globalComposer; /** @var VersionParser */ protected $versionParser; - /** @var bool */ + /** @var bool|'local'|'global' */ protected $disablePlugins = false; /** @var array */ @@ -69,7 +69,10 @@ class PluginManager /** @var int */ private static $classCounter = 0; - public function __construct(IOInterface $io, Composer $composer, PartialComposer $globalComposer = null, bool $disablePlugins = false) + /** + * @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; @@ -87,15 +90,13 @@ class PluginManager */ public function loadInstalledPlugins(): void { - if ($this->disablePlugins) { - return; + if (!$this->arePluginsDisabled('local')) { + $repo = $this->composer->getRepositoryManager()->getLocalRepository(); + $this->loadRepository($repo, false); } - $repo = $this->composer->getRepositoryManager()->getLocalRepository(); - $globalRepo = $this->globalComposer !== null ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; - $this->loadRepository($repo, false); - if ($globalRepo) { - $this->loadRepository($globalRepo, true); + if ($this->globalComposer !== null && !$this->arePluginsDisabled('global')) { + $this->loadRepository($this->globalComposer->getRepositoryManager()->getLocalRepository(), true); } } @@ -106,13 +107,12 @@ class PluginManager */ public function deactivateInstalledPlugins(): void { - if ($this->disablePlugins) { - return; + if (!$this->arePluginsDisabled('local')) { + $repo = $this->composer->getRepositoryManager()->getLocalRepository(); + $this->deactivateRepository($repo, false); } - $repo = $this->composer->getRepositoryManager()->getLocalRepository(); - $this->deactivateRepository($repo, false); - if ($this->globalComposer !== null) { + if ($this->globalComposer !== null && !$this->arePluginsDisabled('global')) { $this->deactivateRepository($this->globalComposer->getRepositoryManager()->getLocalRepository(), true); } } @@ -151,7 +151,7 @@ class PluginManager */ public function registerPackage(PackageInterface $package, bool $failOnMissingClasses = false, bool $isGlobalPlugin = false): void { - if ($this->disablePlugins) { + if ($this->arePluginsDisabled($isGlobalPlugin ? 'global' : 'local')) { return; } @@ -310,10 +310,6 @@ class PluginManager */ public function deactivatePackage(PackageInterface $package): void { - if ($this->disablePlugins) { - return; - } - if (!isset($this->registeredPlugins[$package->getName()])) { return; } @@ -341,10 +337,6 @@ class PluginManager */ public function uninstallPackage(PackageInterface $package): void { - if ($this->disablePlugins) { - return; - } - if (!isset($this->registeredPlugins[$package->getName()])) { return; } @@ -384,6 +376,10 @@ class PluginManager */ 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)) { @@ -676,11 +672,12 @@ class PluginManager /** * @internal * + * @param 'local'|'global' $type * @return bool */ - public function arePluginsDisabled() + public function arePluginsDisabled($type) { - return $this->disablePlugins; + return $this->disablePlugins === true || $this->disablePlugins === $type; } /** @@ -769,7 +766,7 @@ class PluginManager } } - throw new \UnexpectedValueException( + throw new PluginBlockedException( $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'