diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5f85c6bd9..a0fd0ce54 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -21,6 +21,11 @@ If your issue involves installing, updating or resolving dependencies, the chance of us being able to reproduce your issue will be much higher if you share your `composer.json` with us. +Coding Style Fixes +------------------ + +We do not accept CS fixes pull requests. Fixes are done by the project maintainers when appropriate to avoid causing too many unnecessary conflicts between branches and pull requests. + Security Reports ---------------- diff --git a/CHANGELOG.md b/CHANGELOG.md index c7e13cf55..cf06a0fd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +### [1.8.0] 2018-12-03 + + * Changed `post-package-install` / `post-package-update` event to be fired *after* the lock file has been updated as opposed to before + * Added support for removing packages using a wildcard with the `remove` command, e.g. `composer remove foo/*` + * Added `chat` to the list of `support` channels you can list in composer.json + * Added signal handling on require command to restore the composer.json in case of abort + * Added `--ignore` to `outdated` command to pass one or more packages that you do not want to be listed + * Added `--no-dev` to `check-platform-reqs` command to skip dev requirements even if they are installed + * Added support for running plugin commands from sub-directories within a project much like other Composer commands + * Added support for running Composer via phpdbg + * Added `lib-imagick` platform package + * Fixed validate command always checking for disabled checks when used with `--strict` + ### [1.7.3] 2018-11-01 * Fixed handling of replace/conflict rules. This may affect dependency resolution in some edge cases. @@ -696,6 +709,7 @@ * Initial release +[1.8.0]: https://github.com/composer/composer/compare/1.7.3...1.8.0 [1.7.3]: https://github.com/composer/composer/compare/1.7.2...1.7.3 [1.7.2]: https://github.com/composer/composer/compare/1.7.1...1.7.2 [1.7.1]: https://github.com/composer/composer/compare/1.7.0...1.7.1 diff --git a/bin/composer b/bin/composer index 7a80288b6..5e142662c 100755 --- a/bin/composer +++ b/bin/composer @@ -1,7 +1,7 @@ #!/usr/bin/env php yes]? '; + if ($input->isInteractive() && $this->hasDependencies($options) && $io->askConfirmation($question, true)) { + $this->installDependencies($output); + } } /** @@ -208,7 +214,7 @@ EOT } $name = strtolower($name); } else { - if (!preg_match('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}', $name)) { + if (!preg_match('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D', $name)) { throw new \InvalidArgumentException( 'The package name '.$name.' is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+' ); @@ -222,7 +228,7 @@ EOT return $name; } - if (!preg_match('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}', $value)) { + if (!preg_match('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D', $value)) { throw new \InvalidArgumentException( 'The package name '.$value.' is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+' ); @@ -381,7 +387,7 @@ EOT return $this->repos; } - protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array(), $phpVersion = null, $preferredStability = 'stable') + protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array(), $phpVersion = null, $preferredStability = 'stable', $checkProvidedVersions = true) { if ($requires) { $requires = $this->normalizeRequirements($requires); @@ -404,7 +410,7 @@ EOT )); } else { // check that the specified version/constraint exists before we proceed - list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $phpVersion, $preferredStability, $requirement['version'], 'dev'); + list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $phpVersion, $preferredStability, $checkProvidedVersions ? $requirement['version'] : null, 'dev'); // replace package name from packagist.org $requirement['name'] = $name; @@ -767,4 +773,23 @@ EOT return array_keys(array_slice($similarPackages, 0, 5)); } + + private function installDependencies($output) + { + try { + $installCommand = $this->getApplication()->find('install'); + $installCommand->run(new ArrayInput(array()), $output); + } catch (\Exception $e) { + $this->getIO()->writeError('Could not install dependencies. Run `composer install` to see more information.'); + } + + } + + private function hasDependencies($options) + { + $requires = (array) $options['require']; + $devRequires = isset($options['require-dev']) ? (array) $options['require-dev'] : array(); + + return !empty($requires) || !empty($devRequires); + } } diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index c97130c3a..27be1a0ca 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -22,6 +22,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; +use Composer\Package\BasePackage; /** * @author Pierre du Plessis @@ -94,12 +95,25 @@ EOT if (isset($composer[$type][$package])) { $json->removeLink($type, $composer[$type][$package]); } elseif (isset($composer[$altType][$package])) { - $io->writeError(''.$composer[$altType][$package].' could not be found in '.$type.' but it is present in '.$altType.''); + $io->writeError('' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . ''); if ($io->isInteractive()) { - if ($io->askConfirmation('Do you want to remove it from '.$altType.' [yes]? ', true)) { + if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [yes]? ', true)) { $json->removeLink($altType, $composer[$altType][$package]); } } + } elseif (isset($composer[$type]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$type]))) { + foreach ($matches as $matchedPackage) { + $json->removeLink($type, $matchedPackage); + } + } elseif (isset($composer[$altType]) && $matches = preg_grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$altType]))) { + foreach ($matches as $matchedPackage) { + $io->writeError('' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . ''); + if ($io->isInteractive()) { + if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [yes]? ', true)) { + $json->removeLink($altType, $matchedPackage); + } + } + } } else { $io->writeError(''.$package.' is not required in your composer.json and has not been removed'); } diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 147e0cf74..1f29751b9 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -66,7 +66,7 @@ class RequireCommand extends InitCommand <<repos->findPackage('php', '*')->getPrettyVersion(); - $requirements = $this->determineRequirements($input, $output, $input->getArgument('packages'), $phpVersion, $preferredStability); + $requirements = $this->determineRequirements($input, $output, $input->getArgument('packages'), $phpVersion, $preferredStability, !$input->getOption('no-update')); $requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; $removeKey = $input->getOption('dev') ? 'require' : 'require-dev'; diff --git a/src/Composer/Command/ValidateCommand.php b/src/Composer/Command/ValidateCommand.php index 52bba1838..b86fd92ea 100644 --- a/src/Composer/Command/ValidateCommand.php +++ b/src/Composer/Command/ValidateCommand.php @@ -95,9 +95,10 @@ EOT $lockErrors[] = 'The lock file is not up to date with the latest changes in composer.json, it is recommended that you run `composer update`.'; } - $this->outputResult($io, $file, $errors, $warnings, $checkPublish, $publishErrors, $checkLock, $lockErrors, true); + $this->outputResult($io, $file, $errors, $warnings, $checkPublish, $publishErrors, $checkLock, $lockErrors, true, $isStrict); - $exitCode = $errors || ($publishErrors && $checkPublish) || ($lockErrors && $checkLock) ? 2 : ($isStrict && $warnings ? 1 : 0); + // $errors include publish and lock errors when exists + $exitCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0); if ($input->getOption('with-dependencies')) { $localRepo = $composer->getRepositoryManager()->getLocalRepository(); @@ -108,7 +109,7 @@ EOT list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll); $this->outputResult($io, $package->getPrettyName(), $errors, $warnings, $checkPublish, $publishErrors); - $depCode = $errors || ($publishErrors && $checkPublish) ? 2 : ($isStrict && $warnings ? 1 : 0); + $depCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0); $exitCode = max($depCode, $exitCode); } } @@ -121,7 +122,7 @@ EOT return $exitCode; } - private function outputResult($io, $name, &$errors, &$warnings, $checkPublish = false, $publishErrors = array(), $checkLock = false, $lockErrors = array(), $printSchemaUrl = false) + private function outputResult($io, $name, &$errors, &$warnings, $checkPublish = false, $publishErrors = array(), $checkLock = false, $lockErrors = array(), $printSchemaUrl = false, $isStrict = false) { if (!$errors && !$publishErrors && !$warnings) { $io->write('' . $name . ' is valid'); @@ -141,16 +142,18 @@ EOT } // If checking publish errors, display them as errors, otherwise just show them as warnings + // Skip when it is a strict check and we don't want to check publish errors if ($checkPublish) { $errors = array_merge($errors, $publishErrors); - } else { + } elseif (!$isStrict) { $warnings = array_merge($warnings, $publishErrors); } // If checking lock errors, display them as errors, otherwise just show them as warnings + // Skip when it is a strict check and we don't want to check lock errors if ($checkLock) { $errors = array_merge($errors, $lockErrors); - } else { + } elseif (!$isStrict) { $warnings = array_merge($warnings, $lockErrors); } diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index 648874ee2..3da43777b 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -176,8 +176,12 @@ class EventDispatcher $return = false === call_user_func($callable, $event) ? 1 : 0; } elseif ($this->isComposerScript($callable)) { $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable), true, IOInterface::VERBOSE); - $scriptName = substr($callable, 1); - $args = $event->getArguments(); + + $script = explode(' ', substr($callable, 1)); + $scriptName = $script[0]; + unset($script[0]); + + $args = array_merge($script, $event->getArguments()); $flags = $event->getFlags(); if (substr($callable, 0, 10) === '@composer ') { $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . substr($callable, 9); @@ -262,16 +266,17 @@ class EventDispatcher protected function getPhpExecCommand() { $finder = new PhpExecutableFinder(); - $phpPath = $finder->find(); + $phpPath = $finder->find(false); if (!$phpPath) { throw new \RuntimeException('Failed to locate PHP binary to execute '.$phpPath); } - + $phpArgs = $finder->findArguments(); + $phpArgs = $phpArgs ? ' ' . implode(' ', $phpArgs) : ''; $allowUrlFOpenFlag = ' -d allow_url_fopen=' . ProcessExecutor::escape(ini_get('allow_url_fopen')); $disableFunctionsFlag = ' -d disable_functions=' . ProcessExecutor::escape(ini_get('disable_functions')); $memoryLimitFlag = ' -d memory_limit=' . ProcessExecutor::escape(ini_get('memory_limit')); - return ProcessExecutor::escape($phpPath) . $allowUrlFOpenFlag . $disableFunctionsFlag . $memoryLimitFlag; + return ProcessExecutor::escape($phpPath) . $phpArgs . $allowUrlFOpenFlag . $disableFunctionsFlag . $memoryLimitFlag; } /** diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 883abcaf3..259e20057 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -33,6 +33,7 @@ use Composer\Installer\NoopInstaller; use Composer\Installer\SuggestedPackagesReporter; use Composer\IO\IOInterface; use Composer\Package\AliasPackage; +use Composer\Package\BasePackage; use Composer\Package\CompletePackage; use Composer\Package\Link; use Composer\Package\Loader\ArrayLoader; @@ -1247,7 +1248,7 @@ class Installer } foreach ($this->updateWhitelist as $whiteListedPattern => $void) { - $patternRegexp = $this->packageNameToRegexp($whiteListedPattern); + $patternRegexp = BasePackage::packageNameToRegexp($whiteListedPattern); if (preg_match($patternRegexp, $package->getName())) { return true; } @@ -1256,19 +1257,6 @@ class Installer return false; } - /** - * Build a regexp from a package name, expanding * globs as required - * - * @param string $whiteListedPattern - * @return string - */ - private function packageNameToRegexp($whiteListedPattern) - { - $cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern)); - - return "{^" . $cleanedWhiteListedPattern . "$}i"; - } - /** * @param array $links * @return array @@ -1334,7 +1322,7 @@ class Installer // check if the name is a glob pattern that did not match directly if (!$nameMatchesRequiredPackage) { - $whitelistPatternRegexp = $this->packageNameToRegexp($packageName); + $whitelistPatternRegexp = BasePackage::packageNameToRegexp($packageName); foreach ($rootRequiredPackageNames as $rootRequiredPackageName) { if (preg_match($whitelistPatternRegexp, $rootRequiredPackageName)) { $nameMatchesRequiredPackage = true; diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 1e3081c7e..65ea6860f 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -234,4 +234,17 @@ abstract class BasePackage implements PackageInterface $this->repository = null; $this->id = -1; } + + /** + * Build a regexp from a package name, expanding * globs as required + * + * @param string $whiteListedPattern + * @return string + */ + public static function packageNameToRegexp($whiteListedPattern) + { + $cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern)); + + return "{^" . $cleanedWhiteListedPattern . "$}i"; + } } diff --git a/src/Composer/Package/Loader/ValidatingArrayLoader.php b/src/Composer/Package/Loader/ValidatingArrayLoader.php index 2a71be6cb..f4753025b 100644 --- a/src/Composer/Package/Loader/ValidatingArrayLoader.php +++ b/src/Composer/Package/Loader/ValidatingArrayLoader.php @@ -161,7 +161,7 @@ class ValidatingArrayLoader implements LoaderInterface } if ($this->validateArray('support') && !empty($this->config['support'])) { - foreach (array('issues', 'forum', 'wiki', 'source', 'email', 'irc', 'docs', 'rss') as $key) { + foreach (array('issues', 'forum', 'wiki', 'source', 'email', 'irc', 'docs', 'rss', 'chat') as $key) { if (isset($this->config['support'][$key]) && !is_string($this->config['support'][$key])) { $this->errors[] = 'support.'.$key.' : invalid value, must be a string'; unset($this->config['support'][$key]); @@ -178,7 +178,7 @@ class ValidatingArrayLoader implements LoaderInterface unset($this->config['support']['irc']); } - foreach (array('issues', 'forum', 'wiki', 'source', 'docs') as $key) { + foreach (array('issues', 'forum', 'wiki', 'source', 'docs', 'chat') as $key) { if (isset($this->config['support'][$key]) && !$this->filterUrl($this->config['support'][$key])) { $this->warnings[] = 'support.'.$key.' : invalid value ('.$this->config['support'][$key].'), must be an http/https URL'; unset($this->config['support'][$key]); diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 0a2e79f35..6d6e04d2f 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -157,6 +157,13 @@ class PlatformRepository extends ArrayRepository break; + case 'imagick': + $imagick = new \Imagick(); + $imageMagickVersion = $imagick->getVersion(); + preg_match('/^ImageMagick ([\d.]+)-(\d+)/', $imageMagickVersion['versionString'], $matches); + $prettyVersion = "{$matches[1]}.{$matches[2]}"; + break; + case 'libxml': $prettyVersion = LIBXML_DOTTED_VERSION; break; diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index 4e9b7f480..8dfd6624a 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -40,7 +40,7 @@ final class StreamContextFactory )); // Handle HTTP_PROXY/http_proxy on CLI only for security reasons - if (PHP_SAPI === 'cli' && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) { + if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) { $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']); } diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 0e0a5f5f0..c3c20e7e8 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\EventDispatcher; use Composer\EventDispatcher\Event; use Composer\EventDispatcher\EventDispatcher; +use Composer\EventDispatcher\ScriptExecutionException; use Composer\Installer\InstallerEvents; use Composer\Config; use Composer\Composer; @@ -251,6 +252,45 @@ class EventDispatcherTest extends TestCase $this->assertEquals($expected, $io->getOutput()); } + public function testRecursionInScriptsNames() + { + $process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') + ->setConstructorArgs(array( + $composer = $this->createComposerInstance(), + $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE), + $process + )) + ->setMethods(array( + 'getListeners' + )) + ->getMock(); + + $process->expects($this->exactly(1)) + ->method('execute') + ->will($this->returnValue(0)); + + $dispatcher->expects($this->atLeastOnce()) + ->method('getListeners') + ->will($this->returnCallback(function (Event $event) { + if($event->getName() === 'hello') { + return array('echo Hello'); + } + + if($event->getName() === 'helloWorld') { + return array('@hello World'); + } + + return array(); + })); + + $dispatcher->dispatch('helloWorld', new CommandEvent('helloWorld', $composer, $io)); + $expected = "> helloWorld: @hello World".PHP_EOL. + "> hello: echo Hello " .escapeshellarg('World').PHP_EOL; + + $this->assertEquals($expected, $io->getOutput()); + } + /** * @expectedException RuntimeException */ diff --git a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php index cc66ab399..ebe6871fe 100644 --- a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php @@ -71,6 +71,7 @@ class ValidatingArrayLoaderTest extends TestCase 'source' => 'http://example.org/', 'irc' => 'irc://example.org/example', 'rss' => 'http://example.org/rss', + 'chat' => 'http://example.org/chat', ), 'require' => array( 'a/b' => '1.*', @@ -305,6 +306,7 @@ class ValidatingArrayLoaderTest extends TestCase 'forum' => 'foo:bar', 'issues' => 'foo:bar', 'wiki' => 'foo:bar', + 'chat' => 'foo:bar', ), ), array( @@ -312,6 +314,7 @@ class ValidatingArrayLoaderTest extends TestCase 'support.forum : invalid value (foo:bar), must be an http/https URL', 'support.issues : invalid value (foo:bar), must be an http/https URL', 'support.wiki : invalid value (foo:bar), must be an http/https URL', + 'support.chat : invalid value (foo:bar), must be an http/https URL', ), ), array(