Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

[Console] Add DialogHelper::askHiddenResponse method #5731

Merged
merged 6 commits into from Oct 16, 2012
@@ -5,6 +5,7 @@ CHANGELOG
-----
* added support for colorization on Windows via ConEmu
+ * add a method to Dialog Helper to ask for a question and hide the response
2.1.0
-----
@@ -21,6 +21,8 @@
class DialogHelper extends Helper
{
private $inputStream;
+ private static $shell;
+ private static $stty;
/**
* Asks a question to the user.
@@ -72,6 +74,76 @@ public function askConfirmation(OutputInterface $output, $question, $default = t
}
/**
+ * Asks a question to the user, the response is hidden
+ *
+ * @param OutputInterface $output An Output instance
+ * @param string|array $question The question
+ * @param Boolean $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not
+ *
+ * @return string The answer
+ *
+ * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden
+ */
+ public function askHiddenResponse(OutputInterface $output, $question, $fallback = true)
+ {
+ if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ $exe = __DIR__ . '/../../Resources/bin/hiddeninput.exe';
+
+ // handle code running from a phar
+ if ('phar:' === substr(__FILE__, 0, 5)) {
+ $tmpExe = sys_get_temp_dir() . '/../../Resources/bin/hiddeninput.exe';
+ copy($exe, $tmpExe);
+ $exe = $tmpExe;
+ }
+
+ $output->write($question);
+ $value = rtrim(shell_exec($exe));
+ $output->writeln('');
+
+ if (isset($tmpExe)) {
+ unlink($tmpExe);
+ }
+
+ return $value;
+ }
+
+ if ($this->hasSttyAvailable()) {
+ $output->write($question);
@fabpot

fabpot Oct 16, 2012

Owner

this blank line should be removed.

+
+ $sttyMode = shell_exec('/usr/bin/env stty -g');
+
+ shell_exec('/usr/bin/env stty -echo');
+ $value = fgets($this->inputStream ?: STDIN, 4096);
+ shell_exec(sprintf('/usr/bin/env stty %s', $sttyMode));
+
+ if (false === $value) {
+ throw new \RuntimeException('Aborted');
+ }
+
+ $value = trim($value);
+ $output->writeln('');
+
+ return $value;
+ }
+
+ if (false !== $shell = $this->getShell()) {
+ $output->write($question);
@fabpot

fabpot Oct 16, 2012

Owner

this blank line should be removed.

+ $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read mypassword';
+ $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
+ $value = rtrim(shell_exec($command));
+ $output->writeln('');
+
+ return $value;
+ }
+
+ if ($fallback) {
+ return $this->ask($output, $question);
+ }
+
+ throw new \RuntimeException('Unable to hide the response');
+ }
+
+ /**
* Asks for a value and validates the response.
*
* The validator receives the data to validate. It must return the
@@ -80,7 +152,7 @@ public function askConfirmation(OutputInterface $output, $question, $default = t
*
* @param OutputInterface $output An Output instance
* @param string|array $question The question to ask
- * @param callback $validator A PHP callback
+ * @param callable $validator A PHP callback
* @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
* @param string $default The default answer if none is given by the user
*
@@ -90,21 +162,43 @@ public function askConfirmation(OutputInterface $output, $question, $default = t
*/
public function askAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $default = null)
{
- $error = null;
- while (false === $attempts || $attempts--) {
- if (null !== $error) {
- $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'));
- }
+ $that = $this;
- $value = $this->ask($output, $question, $default);
+ $interviewer = function() use ($output, $question, $default, $that) {
+ return $that->ask($output, $question, $default);
+ };
- try {
- return call_user_func($validator, $value);
- } catch (\Exception $error) {
- }
- }
+ return $this->validateAttempts($interviewer, $output, $validator, $attempts);
+ }
- throw $error;
+ /**
+ * Asks for a value, hide and validates the response.
+ *
+ * The validator receives the data to validate. It must return the
+ * validated data when the data is valid and throw an exception
+ * otherwise.
+ *
+ * @param OutputInterface $output An Output instance
+ * @param string|array $question The question to ask
+ * @param callable $validator A PHP callback
+ * @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
+ * @param Boolean $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not
+ *
+ * @return string The response
+ *
+ * @throws \Exception When any of the validators return an error
+ * @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden
+ *
+ */
+ public function askHiddenResponseAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $fallback = true)
+ {
+ $that = $this;
+
+ $interviewer = function() use ($output, $question, $fallback, $that) {
+ return $that->askHiddenResponse($output, $question, $fallback);
+ };
+
+ return $this->validateAttempts($interviewer, $output, $validator, $attempts);
}
/**
@@ -136,4 +230,71 @@ public function getName()
{
return 'dialog';
}
+
+ /**
+ * Return a valid unix shell
+ *
+ * @return string|false The valid shell name, false in case no valid shell is found
+ */
+ private function getShell()
+ {
+ if (null !== self::$shell) {
+ return self::$shell;
+ }
+
+ self::$shell = false;
+
+ if (file_exists('/usr/bin/env')) {
+ // handle other OSs with bash/zsh/ksh/csh if available to hide the answer
+ $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
+ foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) {
+ if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
+ self::$shell = $sh;
+ break;
+ }
+ }
+ }
+
+ return self::$shell;
+ }
+
+ private function hasSttyAvailable()
+ {
+ if (null !== self::$stty) {
+ return self::$stty;
+ }
+
+ exec('/usr/bin/env stty', $output, $exicode);
+
+ return self::$stty = $exicode === 0;
+ }
+
+ /**
+ * Validate an attempt
+ *
+ * @param callable $interviewer A callable that will ask for a question and return the result
+ * @param OutputInterface $output An Output instance
+ * @param callable $validator A PHP callback
+ * @param integer $attempts Max number of times to ask before giving up ; false will ask infinitely
+ *
+ * @return string The validated response
+ *
+ * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
+ */
+ private function validateAttempts($interviewer, OutputInterface $output, $validator, $attempts)
+ {
+ $error = null;
+ while (false === $attempts || $attempts--) {
+ if (null !== $error) {
+ $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'));
+ }
+
+ try {
+ return call_user_func($validator, $interviewer());
+ } catch (\Exception $error) {
+ }
+ }
+
+ throw $error;
+ }
}
@@ -41,12 +41,18 @@ output abstractions (so that you can easily unit-test your commands),
validation, automatic help messages, ...
Tests
----------
+-----
You can run the unit tests with the following command:
phpunit
+Third Party
+-----------
+
+`Resources/bin/hiddeninput.exe` is a third party binary provided within this
+component. Find sources and license at https://github.com/Seldaek/hidden-input.
+
Resources
---------
Binary file not shown.
@@ -31,6 +31,19 @@ public function testAsk()
$this->assertEquals('What time is it?', stream_get_contents($output->getStream()));
}
+ public function testAskHiddenResponse()
+ {
+ if (defined('PHP_WINDOWS_VERSION_BUILD')) {
+ $this->markTestSkipped('This test is not supported on Windows');
+ }
+
+ $dialog = new DialogHelper();
+
+ $dialog->setInputStream($this->getInputStream("8AM\n"));
+
+ $this->assertEquals('8AM', $dialog->askHiddenResponse($this->getOutputStream(), 'What time is it?'));
+ }
@stof

stof Oct 13, 2012

Member

This test should be skipped on Windows as it would trigger the binary, which will not work.

+
public function testAskConfirmation()
{
$dialog = new DialogHelper();