diff --git a/composer.json b/composer.json index 78fa537f6..ef2b400df 100644 --- a/composer.json +++ b/composer.json @@ -28,10 +28,10 @@ "composer/semver": "^1.0", "composer/spdx-licenses": "^1.0", "seld/jsonlint": "^1.4", - "symfony/console": "^2.7 || ^3.0", - "symfony/finder": "^2.7 || ^3.0", - "symfony/process": "^2.7 || ^3.0", - "symfony/filesystem": "^2.7 || ^3.0", + "symfony/console": "^2.7 || ^3.0 || ^4.0", + "symfony/finder": "^2.7 || ^3.0 || ^4.0", + "symfony/process": "^2.7 || ^3.0 || ^4.0", + "symfony/filesystem": "^2.7 || ^3.0 || ^4.0", "seld/phar-utils": "^1.0", "seld/cli-prompt": "^1.0", "psr/log": "^1.0" diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index e850b96d6..694e23874 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -12,32 +12,33 @@ namespace Composer\Command; -use Composer\DependencyResolver\Pool; +use Composer\Composer; use Composer\DependencyResolver\DefaultPolicy; +use Composer\DependencyResolver\Pool; use Composer\Json\JsonFile; -use Composer\Package\CompletePackageInterface; -use Composer\Package\Version\VersionParser; use Composer\Package\BasePackage; +use Composer\Package\CompletePackageInterface; +use Composer\Package\PackageInterface; +use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionSelector; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; -use Composer\Package\PackageInterface; +use Composer\Repository\ArrayRepository; +use Composer\Repository\ComposerRepository; +use Composer\Repository\CompositeRepository; +use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryFactory; +use Composer\Repository\RepositoryInterface; use Composer\Semver\Constraint\ConstraintInterface; +use Composer\Semver\Semver; +use Composer\Spdx\SpdxLicenses; use Composer\Util\Platform; use Symfony\Component\Console\Formatter\OutputFormatterStyle; -use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Composer\Repository\ArrayRepository; -use Composer\Repository\CompositeRepository; -use Composer\Repository\ComposerRepository; -use Composer\Repository\PlatformRepository; -use Composer\Repository\RepositoryInterface; -use Composer\Repository\RepositoryFactory; -use Composer\Spdx\SpdxLicenses; -use Composer\Composer; -use Composer\Semver\Semver; +use Symfony\Component\Console\Terminal; /** * @author Robert Schönthal @@ -255,7 +256,13 @@ EOT $packageListFilter = $this->getRootRequires(); } - list($width) = $this->getApplication()->getTerminalDimensions(); + if (class_exists('Symfony\Component\Console\Terminal')) { + $terminal = new Terminal(); + $width = $terminal->getWidth(); + } else { + // For versions of Symfony console before 3.2 + list($width) = $this->getApplication()->getTerminalDimensions(); + } if (null === $width) { // In case the width is not detected, we're probably running the command // outside of a real terminal, use space without a limit diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php index acb8a8564..f3a99edf2 100644 --- a/src/Composer/IO/ConsoleIO.php +++ b/src/Composer/IO/ConsoleIO.php @@ -12,11 +12,12 @@ namespace Composer\IO; +use Composer\Question\StrictConfirmationQuestion; +use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Helper\HelperSet; -use Composer\Question\StrictConfirmationQuestion; +use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; /** @@ -281,11 +282,14 @@ class ConsoleIO extends BaseIO */ public function select($question, $choices, $default, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false) { - if ($this->isInteractive()) { - return $this->helperSet->get('dialog')->select($this->getErrorOutput(), $question, $choices, $default, $attempts, $errorMessage, $multiselect); - } + /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ + $helper = $this->helperSet->get('question'); + $question = new ChoiceQuestion($question, $choices, $default); + $question->setMaxAttempts($attempts ?: null); // IOInterface requires false, and Question requires null or int + $question->setErrorMessage($errorMessage); + $question->setMultiselect($multiselect); - return $default; + return $helper->ask($this->input, $this->getErrorOutput(), $question); } /** diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 1074b2fce..408aa9eae 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -12,9 +12,9 @@ namespace Composer\Util; +use Composer\IO\IOInterface; use Symfony\Component\Process\Process; use Symfony\Component\Process\ProcessUtils; -use Composer\IO\IOInterface; /** * @author Robert Schönthal @@ -131,6 +131,59 @@ class ProcessExecutor */ public static function escape($argument) { - return ProcessUtils::escapeArgument($argument); + if (method_exists('Symfony\Component\Process\ProcessUtils', 'escapeArgument')) { + return ProcessUtils::escapeArgument($argument); + } + return self::escapeArgument($argument); + } + + /** + * Copy of ProcessUtils::escapeArgument() that is removed in Symfony 4. + * + * @param string $argument + * + * @return string + */ + private static function escapeArgument($argument) + { + //Fix for PHP bug #43784 escapeshellarg removes % from given string + //Fix for PHP bug #49446 escapeshellarg doesn't work on Windows + //@see https://bugs.php.net/bug.php?id=43784 + //@see https://bugs.php.net/bug.php?id=49446 + if ('\\' === DIRECTORY_SEPARATOR) { + if ('' === $argument) { + return escapeshellarg($argument); + } + + $escapedArgument = ''; + $quote = false; + foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + if ('"' === $part) { + $escapedArgument .= '\\"'; + } elseif (self::isSurroundedBy($part, '%')) { + // Avoid environment variable expansion + $escapedArgument .= '^%"'.substr($part, 1, -1).'"^%'; + } else { + // escape trailing backslash + if ('\\' === substr($part, -1)) { + $part .= '\\'; + } + $quote = true; + $escapedArgument .= $part; + } + } + if ($quote) { + $escapedArgument = '"'.$escapedArgument.'"'; + } + + return $escapedArgument; + } + + return "'".str_replace("'", "'\\''", $argument)."'"; + } + + private static function isSurroundedBy($arg, $char) + { + return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; } } diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php index ff96a011f..183012b63 100644 --- a/tests/Composer/Test/IO/ConsoleIOTest.php +++ b/tests/Composer/Test/IO/ConsoleIOTest.php @@ -229,27 +229,27 @@ class ConsoleIOTest extends TestCase { $inputMock = $this->getMock('Symfony\Component\Console\Input\InputInterface'); $outputMock = $this->getMock('Symfony\Component\Console\Output\OutputInterface'); - $dialogMock = $this->getMock('Symfony\Component\Console\Helper\DialogHelper'); - $helperMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet'); + $helperMock = $this->getMock('Symfony\Component\Console\Helper\QuestionHelper'); + $setMock = $this->getMock('Symfony\Component\Console\Helper\HelperSet'); - $inputMock->expects($this->once()) - ->method('isInteractive') - ->will($this->returnValue(true)); - $dialogMock->expects($this->once()) - ->method('select') - ->with($this->isInstanceOf('Symfony\Component\Console\Output\OutputInterface'), - $this->equalTo('Select item'), - $this->equalTo(array("item1", "item2")), - $this->equalTo(null), - $this->equalTo(false), - $this->equalTo("Error message"), - $this->equalTo(true)); - $helperMock->expects($this->once()) + $helperMock + ->expects($this->once()) + ->method('ask') + ->with( + $this->isInstanceOf('Symfony\Component\Console\Input\InputInterface'), + $this->isInstanceOf('Symfony\Component\Console\Output\OutputInterface'), + $this->isInstanceOf('Symfony\Component\Console\Question\Question') + ) + ; + + $setMock + ->expects($this->once()) ->method('get') - ->with($this->equalTo('dialog')) - ->will($this->returnValue($dialogMock)); + ->with($this->equalTo('question')) + ->will($this->returnValue($helperMock)) + ; - $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock); + $consoleIO = new ConsoleIO($inputMock, $outputMock, $setMock); $consoleIO->select('Select item', array("item1", "item2"), null, false, "Error message", true); } diff --git a/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php b/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php index b0ad8c60a..a69d1aeef 100644 --- a/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php +++ b/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php @@ -16,6 +16,8 @@ use Composer\Question\StrictConfirmationQuestion; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\StreamableInputInterface; use Symfony\Component\Console\Output\StreamOutput; /** @@ -42,11 +44,11 @@ class StrictConfirmationQuestionTest extends TestCase */ public function testAskConfirmationBadAnswer($answer) { - $dialog = new QuestionHelper(); - $dialog->setInputStream($this->getInputStream($answer."\n")); + list($input, $dialog) = $this->createInput($answer."\n"); + $question = new StrictConfirmationQuestion('Do you like French fries?'); $question->setMaxAttempts(1); - $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question); + $dialog->ask($input, $this->createOutputInterface(), $question); } /** @@ -54,11 +56,10 @@ class StrictConfirmationQuestionTest extends TestCase */ public function testAskConfirmation($question, $expected, $default = true) { - $dialog = new QuestionHelper(); + list($input, $dialog) = $this->createInput($question."\n"); - $dialog->setInputStream($this->getInputStream($question."\n")); $question = new StrictConfirmationQuestion('Do you like French fries?', $default); - $this->assertEquals($expected, $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question), 'confirmation question should '.($expected ? 'pass' : 'cancel')); + $this->assertEquals($expected, $dialog->ask($input, $this->createOutputInterface(), $question), 'confirmation question should '.($expected ? 'pass' : 'cancel')); } public function getAskConfirmationData() @@ -75,13 +76,13 @@ class StrictConfirmationQuestionTest extends TestCase public function testAskConfirmationWithCustomTrueAndFalseAnswer() { - $dialog = new QuestionHelper(); - $question = new StrictConfirmationQuestion('Do you like French fries?', false, '/^ja$/i', '/^nein$/i'); - $dialog->setInputStream($this->getInputStream("ja\n")); - $this->assertTrue($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); - $dialog->setInputStream($this->getInputStream("nein\n")); - $this->assertFalse($dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question)); + + list($input, $dialog) = $this->createInput("ja\n"); + $this->assertTrue($dialog->ask($input, $this->createOutputInterface(), $question)); + + list($input, $dialog) = $this->createInput("nein\n"); + $this->assertFalse($dialog->ask($input, $this->createOutputInterface(), $question)); } protected function getInputStream($input) @@ -98,13 +99,19 @@ class StrictConfirmationQuestionTest extends TestCase return new StreamOutput(fopen('php://memory', 'r+', false)); } - protected function createInputInterfaceMock($interactive = true) + protected function createInput($entry) { - $mock = $this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(); - $mock->expects($this->any()) - ->method('isInteractive') - ->will($this->returnValue($interactive)); + $stream = $this->getInputStream($entry); + $input = new ArrayInput(array('--no-interaction')); + $dialog = new QuestionHelper(); - return $mock; + if (method_exists($dialog, 'setInputStream')) { + $dialog->setInputStream($stream); + } + if ($input instanceof StreamableInputInterface) { + $input->setStream($stream); + } + + return array($input, $dialog); } }