From c077df0d800c47174227ff088131970daf6de377 Mon Sep 17 00:00:00 2001 From: Theo Tonge Date: Mon, 10 Apr 2017 21:21:53 +0100 Subject: [PATCH] Fix for https://github.com/composer/composer/issues/5989 Enforce yes/no answers for Confirmation Questions --- src/Composer/IO/ConsoleIO.php | 4 +- .../Question/StrictConfirmationQuestion.php | 94 +++++++++++++++ tests/Composer/Test/IO/ConsoleIOTest.php | 2 +- .../StrictConfirmationQuestionTest.php | 110 ++++++++++++++++++ 4 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 src/Composer/Question/StrictConfirmationQuestion.php create mode 100644 tests/Composer/Test/Question/StrictConfirmationQuestionTest.php diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php index 0fd93152e..acb8a8564 100644 --- a/src/Composer/IO/ConsoleIO.php +++ b/src/Composer/IO/ConsoleIO.php @@ -16,7 +16,7 @@ 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 Symfony\Component\Console\Question\ConfirmationQuestion; +use Composer\Question\StrictConfirmationQuestion; use Symfony\Component\Console\Question\Question; /** @@ -247,7 +247,7 @@ class ConsoleIO extends BaseIO { /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ $helper = $this->helperSet->get('question'); - $question = new ConfirmationQuestion($question, $default); + $question = new StrictConfirmationQuestion($question, $default); return $helper->ask($this->input, $this->getErrorOutput(), $question); } diff --git a/src/Composer/Question/StrictConfirmationQuestion.php b/src/Composer/Question/StrictConfirmationQuestion.php new file mode 100644 index 000000000..c64890b8d --- /dev/null +++ b/src/Composer/Question/StrictConfirmationQuestion.php @@ -0,0 +1,94 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Question; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Question\Question; + +/** + * Represents a yes/no question + * Enforces strict responses rather than non-standard answers counting as default + * Based on Symfony\Component\Console\Question\ConfirmationQuestion + * + * @author Theo Tonge + */ +class StrictConfirmationQuestion extends Question +{ + private $trueAnswerRegex; + private $falseAnswerRegex; + + /** + * Constructor.s + * + * @param string $question The question to ask to the user + * @param bool $default The default answer to return, true or false + * @param string $trueAnswerRegex A regex to match the "yes" answer + * @param string $falseAnswerRegex A regex to match the "no" answer + */ + public function __construct($question, $default = true, $trueAnswerRegex = '/^y(?:es)?$/i', $falseAnswerRegex = '/^no?$/i') + { + parent::__construct($question, (bool) $default); + + $this->trueAnswerRegex = $trueAnswerRegex; + $this->falseAnswerRegex = $falseAnswerRegex; + $this->setNormalizer($this->getDefaultNormalizer()); + $this->setValidator($this->getDefaultValidator()); + } + + /** + * Returns the default answer normalizer. + * + * @return callable + */ + private function getDefaultNormalizer() + { + $default = $this->getDefault(); + $trueRegex = $this->trueAnswerRegex; + $falseRegex = $this->falseAnswerRegex; + + return function ($answer) use ($default, $trueRegex, $falseRegex) { + if (is_bool($answer)) { + return $answer; + } + if (empty($answer) && !empty($default)) { + return $default; + } + + if (preg_match($trueRegex, $answer)) { + return true; + } + + if (preg_match($falseRegex, $answer)) { + return false; + } + + return null; + }; + } + + /** + * Returns the default answer validator. + * + * @return callable + */ + private function getDefaultValidator() + { + return function ($answer) { + if (!is_bool($answer)) { + throw new InvalidArgumentException('Please answer yes, y, no, or n.'); + } + + return $answer; + }; + } +} diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php index 9d11105c2..ace27a416 100644 --- a/tests/Composer/Test/IO/ConsoleIOTest.php +++ b/tests/Composer/Test/IO/ConsoleIOTest.php @@ -179,7 +179,7 @@ class ConsoleIOTest extends TestCase ->with( $this->isInstanceOf('Symfony\Component\Console\Input\InputInterface'), $this->isInstanceOf('Symfony\Component\Console\Output\OutputInterface'), - $this->isInstanceOf('Symfony\Component\Console\Question\ConfirmationQuestion') + $this->isInstanceOf('Composer\Question\StrictConfirmationQuestion') ) ; diff --git a/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php b/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php new file mode 100644 index 000000000..b0ad8c60a --- /dev/null +++ b/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php @@ -0,0 +1,110 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Question\Test; + +use Composer\Question\StrictConfirmationQuestion; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Output\StreamOutput; + +/** + * based on Symfony\Component\Console\Tests\Helper\QuestionHelperTest + * + * @author Theo Tonge + */ +class StrictConfirmationQuestionTest extends TestCase +{ + public function getAskConfirmationBadData() + { + return array( + array('not correct'), + array('no more'), + array('yes please'), + array('yellow'), + ); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Please answer yes, y, no, or n. + * @dataProvider getAskConfirmationBadData + */ + public function testAskConfirmationBadAnswer($answer) + { + $dialog = new QuestionHelper(); + $dialog->setInputStream($this->getInputStream($answer."\n")); + $question = new StrictConfirmationQuestion('Do you like French fries?'); + $question->setMaxAttempts(1); + $dialog->ask($this->createInputInterfaceMock(), $this->createOutputInterface(), $question); + } + + /** + * @dataProvider getAskConfirmationData + */ + public function testAskConfirmation($question, $expected, $default = true) + { + $dialog = new QuestionHelper(); + + $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')); + } + + public function getAskConfirmationData() + { + return array( + array('', true), + array('', false, false), + array('y', true), + array('yes', true), + array('n', false), + array('no', false), + ); + } + + 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)); + } + + protected function getInputStream($input) + { + $stream = fopen('php://memory', 'r+', false); + fwrite($stream, $input); + rewind($stream); + + return $stream; + } + + protected function createOutputInterface() + { + return new StreamOutput(fopen('php://memory', 'r+', false)); + } + + protected function createInputInterfaceMock($interactive = true) + { + $mock = $this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(); + $mock->expects($this->any()) + ->method('isInteractive') + ->will($this->returnValue($interactive)); + + return $mock; + } +}