Browse files

merged branch romainneutron/ConsoleHiddenQuestion (PR #5731)

This PR was merged into the master branch.

Commits
-------

aefa495 Move `hiddeninput.exe` to Resources/bin
c0f8a63 Fix CS and typos
26c35e0 Skip askHiddenResponse test on windows
e2eaf5a Update Changelog, add Readme note about hidden input third party
ac01d5d Fix tests and CS
e396edb [Console] Add DialogHelper::askHiddenResponse method

Discussion
----------

[Console] Add DialogHelper::askHiddenResponse method

Bug fix: no
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes
License of the code: MIT

It adds a method to `DialogHelper` to ask a question and hide the response. It's pretty cool when working with passwords.

This code is more than largely inspired by Composer, see [ConsoleIO.php at line 140](https://github.com/composer/composer/blob/master/src/Composer/IO/ConsoleIO.php#L140)

 You will notice that this PR embeds a Windows Executable binary for windows support. This windows binary is provided by @Seldaek (see https://github.com/Seldaek/hidden-input)
This dependency is not yet available via composer.

If this is a problem to embed this file, we can think of other way to provide this support (make a package from HiddenInput and add composer recommandation for example).

---------------------------------------------------------------------------

by stof at 2012-10-11T17:20:11Z

The link to the hiddeninput source code should be added in the readme.
And you should also update the changelog.

Btw, adding composer for hiddeninput does not make sense. Compsoer is about installing PHP code, not about downloading the source of a C++ program.

---------------------------------------------------------------------------

by romainneutron at 2012-10-11T17:22:58Z

This proposition comes from a discussion I had with Jordi , nothing more :)

Romain

On 11 oct. 2012, at 19:20, Christophe Coevoet <notifications@github.com>
wrote:

The link to the hiddeninput source code should be added in the readme.
And you should also update the changelog.

Btw, adding composer for hiddeninput does not make sense. Compsoer is about
installing PHP code, not about downloading the source of a C++ program.

—
Reply to this email directly or view it on
GitHub<#5731 (comment)>.

---------------------------------------------------------------------------

by romainneutron at 2012-10-12T07:33:00Z

Changelog updated, Readme note added, CS fixed

---------------------------------------------------------------------------

by stof at 2012-10-13T22:09:24Z

the missing point is now the PR to the doc for this new feature

---------------------------------------------------------------------------

by romainneutron at 2012-10-16T00:33:59Z

@stof documentation added

---------------------------------------------------------------------------

by romainneutron at 2012-10-16T09:10:35Z

@fabpot what you asked is now fixed
  • Loading branch information...
2 parents 9ebc2f5 + aefa495 commit d8f6021fc1b1a9ed7ab74216323b3f52c68057dc @fabpot fabpot committed Oct 16, 2012
View
1 src/Symfony/Component/Console/CHANGELOG.md
@@ -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
-----
View
187 src/Symfony/Component/Console/Helper/DialogHelper.php
@@ -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);
+
+ $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);
+ $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;
+ }
}
View
8 src/Symfony/Component/Console/README.md
@@ -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
---------
View
BIN src/Symfony/Component/Console/Resources/bin/hiddeninput.exe
Binary file not shown.
View
13 src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php
@@ -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?'));
+ }
+
public function testAskConfirmation()
{
$dialog = new DialogHelper();

0 comments on commit d8f6021

Please sign in to comment.