8000 merged branch romainneutron/ConsoleHiddenQuestion (PR #5731) · symfony/symfony@d8f6021 · GitHub
[go: up one dir, main page]

Skip to content

Commit d8f6021

Browse files
committed
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 hi 8000 ddeninput 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
2 parents 9ebc2f5 + aefa495 commit d8f6021

File tree

5 files changed

+195
-14
lines changed

5 files changed

+195
-14
lines changed

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* added support for colorization on Windows via ConEmu
8+
* add a method to Dialog Helper to ask for a question and hide the response
89

910
2.1.0
1011
-----

src/Symfony/Component/Console/Helper/DialogHelper.php

Lines changed: 174 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
class DialogHelper extends Helper
2222
{
2323
private $inputStream;
24+
private static $shell;
25+
private static $stty;
2426

2527
/**
2628
* Asks a question to the user.
@@ -71,6 +73,76 @@ public function askConfirmation(OutputInterface $output, $question, $default = t
7173
return !$answer || 'y' == strtolower($answer[0]);
7274
}
7375

76+
/**
77+
* Asks a question to the user, the response is hidden
78+
*
79+
* @param OutputInterface $output An Output instance
80+
* @param string|array $question The question
81+
* @param Boolean $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not
82+
*
83+
* @return string The answer
84+
*
85+
* @throws \RuntimeException In case the fallbac 8000 k is deactivated and the response can not be hidden
86+
*/
87+
public function askHiddenResponse(OutputInterface $output, $question, $fallback = true)
88+
{
89+
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
90+
$exe = __DIR__ . '/../../Resources/bin/hiddeninput.exe';
91+
92+
// handle code running from a phar
93+
if ('phar:' === substr(__FILE__, 0, 5)) {
94+
$tmpExe = sys_get_temp_dir() . '/../../Resources/bin/hiddeninput.exe';
95+
copy($exe, $tmpExe);
96+
$exe = $tmpExe;
97+
}
98+
99+
$output->write($question);
100+
$value = rtrim(shell_exec($exe));
101+
$output->writeln('');
102+
103+
if (isset($tmpExe)) {
104+
unlink($tmpExe);
105+
}
106+
107+
return $value;
108+
}
109+
110+
if ($this->hasSttyAvailable()) {
111+
$output->write($question);
112+
113+
$sttyMode = shell_exec('/usr/bin/env stty -g');
114+
115+
shell_exec('/usr/bin/env stty -echo');
116+
$value = fgets($this->inputStream ?: STDIN, 4096);
117+
shell_exec(sprintf('/usr/bin/env stty %s', $sttyMode));
118+
119+
if (false === $value) {
120+
throw new \RuntimeException('Aborted');
121+
}
122+
123+
$value = trim($value);
124+
$output->writeln('');
125+
126+
return $value;
127+
}
128+
129+
if (false !== $shell = $this->getShell()) {
130+
$output->write($question);
131+
$readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read mypassword';
132+
$command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
133+
$value = rtrim(shell_exec($command));
134+
$output->writeln('');
135+
136+
return $value;
137+
}
138+
139+
if ($fallback) {
140+
return $this->ask($output, $question);
141+
}
142+
143+
throw new \RuntimeException('Unable to hide the response');
144+
}
145+
74146
/**
75147
* Asks for a value and validates the response.
76148
*
@@ -80,7 +152,7 @@ public function askConfirmation(OutputInterface $output, $question, $default = t
80152
*
81153
* @param OutputInterface $output An Output instance
82154
* @param string|array $question The question to ask
83-
* @param callback $validator A PHP callback
155+
* @param callable $validator A PHP callback
84156
* @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
85157
* @param string $default The default answer if none is given by the user
86158
*
@@ -90,21 +162,43 @@ public function askConfirmation(OutputInterface $output, $question, $default = t
90162
*/
91163
public function askAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $default = null)
92164
{
93-
$error = null;
94-
while (false === $attempts || $attempts--) {
95-
if (null !== $error) {
96-
$output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'));
97-
}
165+
$that = $this;
98166

99-
$value = $this->ask($output, $question, $default);
167+
$interviewer = function() use ($output, $question, $default, $that) {
168+
return $that->ask($output, $question, $default);
169+
};
100170

101-
try {
102-
return call_user_func($validator, $value);
103-
} catch (\Exception $error) {
104-
}
105-
}
171+
return $this->validateAttempts($interviewer, $output, $validator, $attempts);
172+
}
106173

107-
throw $error;
174+
/**
175+
* Asks for a value, hide and validates the response.
176+
*
177+
* The validator receives the data to validate. It must return the
178+
* validated data when the data is valid and throw an exception
179+
* otherwise.
180+
*
181+
* @param OutputInterface $output An Output instance
182+
* @param string|array $question The question to ask
183+
* @param callable $validator A PHP callback
184+
* @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite)
185+
* @param Boolean $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not
186+
*
187+
* @return string The response
188+
*
189+
* @throws \Exception When any of the validators return an error
190+
* @throws \RuntimeException In case the fallback is deactivated and the response can not be hidden
191+
*
192+
*/
193+
public function askHiddenResponseAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $fallback = true)
194+
{
195+
$that = $this;
196+
197+
$interviewer = function() use ($output, $question, $fallback, $that) {
198+
return $that->askHiddenResponse($output, $question, $fallback);
199+
};
200+
201+
return $this->validateAttempts($interviewer, $output, $validator, $attempts);
108202
}
109203

110204
/**
@@ -136,4 +230,71 @@ public function getName()
136230
{
137231
return 'dialog';
138232
}
233+
234+
/**
235+
* Return a valid unix shell
236+
*
237+
* @return string|false The valid shell name, false in case no valid shell is found
238+
*/
239+
private function getShell()
240+
{
241+
if (null !== self::$shell) {
242+
return self::$shell;
243+
}
244+
245+
self::$shell = false;
246+
247+
if (file_exists('/usr/bin/env')) {
248+
// handle other OSs with bash/zsh/ksh/csh if available to hide the answer
249+
$test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
250+
foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) {
251+
if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
252+
self::$shell = $sh;
253+
break;
254+
}
255+
}
256+
}
257+
258+
return self::$shell;
259+
}
260+
261+
private function hasSttyAvailable()
262+
{
263+
if (null !== self::$stty) {
264+
return self::$stty;
265+
}
266+
267+
exec('/usr/bin/env stty', $output, $exicode);
268+
269+
return self::$stty = $exicode === 0;
270+
}
271+
272+
/**
273+
* Validate an attempt
274+
*
275+
* @param callable $interviewer A callable that will ask for a question and return the result
276+
* @param OutputInterface $output An Output instance
277+
* @param callable $validator A PHP callback
278+
* @param integer $attempts Max number of times to ask before giving up ; false will ask infinitely
279+
*
280+
* @return string The validated response
281+
*
282+
* @throws \Exception In case the max number of attempts has been reached and no valid response has been given
283+
*/
284+
private function validateAttempts($interviewer, OutputInterface $output, $validator, $attempts)
285+
{
286+
$error = null;
287+
while (false === $attempts || $attempts--) {
288+
if (null !== $error) {
289+
$output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'));
290+
}
291+
292+
try {
293+
return call_user_func($validator, $interviewer());
294+
} catch (\Exception $error) {
295+
}
296+
}
297+
298+
throw $error;
299+
}
139300
}

src/Symfony/Component/Console/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,18 @@ output abstractions (so that you can easily unit-test your commands),
4141
validation, automatic help messages, ...
4242

4343
Tests
44-
---------
44+
-----
4545

4646
You can run the unit tests with the following command:
4747

4848
phpunit
4949

50+
Third Party
51+
-----------
52+
53+
`Resources/bin/hiddeninput.exe` is a third party binary provided within this
54+
component. Find sources and license at https://github.com/Seldaek/hidden-input.
55+
5056
Resources
5157
---------
5258

Binary file not shown.

src/Symfony/Component/Console/Tests/Helper/DialogHelperTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ public function testAsk()
3131
$this->assertEquals('What time is it?', stream_get_contents($output->getStream()));
3232
}
3333

34+
public function testAskHiddenResponse()
35+
{
36+
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
37+
$this->markTestSkipped('This test is not supported on Windows');
38+
}
39+
40+
$dialog = new DialogHelper();
41+
42+
$dialog->setInputStream($this->getInputStream("8AM\n"));
43+
44+
$this->assertEquals('8AM', $dialog->askHiddenResponse($this->getOutputStream(), 'What time is it?'));
45+
}
46+
3447
public function testAskConfirmation()
3548
{
3649
$dialog = new DialogHelper();

0 commit comments

Comments
 (0)
0