8998f7b0f3c217f0ccf7a09d35d790bdd35c2d98
[yaffs-website] / vendor / symfony / console / Helper / QuestionHelper.php
1 <?php
2
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Symfony\Component\Console\Helper;
13
14 use Symfony\Component\Console\Exception\InvalidArgumentException;
15 use Symfony\Component\Console\Exception\RuntimeException;
16 use Symfony\Component\Console\Input\InputInterface;
17 use Symfony\Component\Console\Output\ConsoleOutputInterface;
18 use Symfony\Component\Console\Output\OutputInterface;
19 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
20 use Symfony\Component\Console\Question\Question;
21 use Symfony\Component\Console\Question\ChoiceQuestion;
22
23 /**
24  * The QuestionHelper class provides helpers to interact with the user.
25  *
26  * @author Fabien Potencier <fabien@symfony.com>
27  */
28 class QuestionHelper extends Helper
29 {
30     private $inputStream;
31     private static $shell;
32     private static $stty;
33
34     /**
35      * Asks a question to the user.
36      *
37      * @param InputInterface  $input    An InputInterface instance
38      * @param OutputInterface $output   An OutputInterface instance
39      * @param Question        $question The question to ask
40      *
41      * @return mixed The user answer
42      *
43      * @throws RuntimeException If there is no data to read in the input stream
44      */
45     public function ask(InputInterface $input, OutputInterface $output, Question $question)
46     {
47         if ($output instanceof ConsoleOutputInterface) {
48             $output = $output->getErrorOutput();
49         }
50
51         if (!$input->isInteractive()) {
52             return $question->getDefault();
53         }
54
55         if (!$question->getValidator()) {
56             return $this->doAsk($output, $question);
57         }
58
59         $that = $this;
60
61         $interviewer = function () use ($output, $question, $that) {
62             return $that->doAsk($output, $question);
63         };
64
65         return $this->validateAttempts($interviewer, $output, $question);
66     }
67
68     /**
69      * Sets the input stream to read from when interacting with the user.
70      *
71      * This is mainly useful for testing purpose.
72      *
73      * @param resource $stream The input stream
74      *
75      * @throws InvalidArgumentException In case the stream is not a resource
76      */
77     public function setInputStream($stream)
78     {
79         if (!is_resource($stream)) {
80             throw new InvalidArgumentException('Input stream must be a valid resource.');
81         }
82
83         $this->inputStream = $stream;
84     }
85
86     /**
87      * Returns the helper's input stream.
88      *
89      * @return resource
90      */
91     public function getInputStream()
92     {
93         return $this->inputStream;
94     }
95
96     /**
97      * {@inheritdoc}
98      */
99     public function getName()
100     {
101         return 'question';
102     }
103
104     /**
105      * Asks the question to the user.
106      *
107      * This method is public for PHP 5.3 compatibility, it should be private.
108      *
109      * @param OutputInterface $output
110      * @param Question        $question
111      *
112      * @return bool|mixed|null|string
113      *
114      * @throws \Exception
115      * @throws \RuntimeException
116      */
117     public function doAsk(OutputInterface $output, Question $question)
118     {
119         $this->writePrompt($output, $question);
120
121         $inputStream = $this->inputStream ?: STDIN;
122         $autocomplete = $question->getAutocompleterValues();
123
124         if (null === $autocomplete || !$this->hasSttyAvailable()) {
125             $ret = false;
126             if ($question->isHidden()) {
127                 try {
128                     $ret = trim($this->getHiddenResponse($output, $inputStream));
129                 } catch (\RuntimeException $e) {
130                     if (!$question->isHiddenFallback()) {
131                         throw $e;
132                     }
133                 }
134             }
135
136             if (false === $ret) {
137                 $ret = fgets($inputStream, 4096);
138                 if (false === $ret) {
139                     throw new RuntimeException('Aborted');
140                 }
141                 $ret = trim($ret);
142             }
143         } else {
144             $ret = trim($this->autocomplete($output, $question, $inputStream));
145         }
146
147         $ret = strlen($ret) > 0 ? $ret : $question->getDefault();
148
149         if ($normalizer = $question->getNormalizer()) {
150             return $normalizer($ret);
151         }
152
153         return $ret;
154     }
155
156     /**
157      * Outputs the question prompt.
158      *
159      * @param OutputInterface $output
160      * @param Question        $question
161      */
162     protected function writePrompt(OutputInterface $output, Question $question)
163     {
164         $message = $question->getQuestion();
165
166         if ($question instanceof ChoiceQuestion) {
167             $maxWidth = max(array_map(array($this, 'strlen'), array_keys($question->getChoices())));
168
169             $messages = (array) $question->getQuestion();
170             foreach ($question->getChoices() as $key => $value) {
171                 $width = $maxWidth - $this->strlen($key);
172                 $messages[] = '  [<info>'.$key.str_repeat(' ', $width).'</info>] '.$value;
173             }
174
175             $output->writeln($messages);
176
177             $message = $question->getPrompt();
178         }
179
180         $output->write($message);
181     }
182
183     /**
184      * Outputs an error message.
185      *
186      * @param OutputInterface $output
187      * @param \Exception      $error
188      */
189     protected function writeError(OutputInterface $output, \Exception $error)
190     {
191         if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
192             $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
193         } else {
194             $message = '<error>'.$error->getMessage().'</error>';
195         }
196
197         $output->writeln($message);
198     }
199
200     /**
201      * Autocompletes a question.
202      *
203      * @param OutputInterface $output
204      * @param Question        $question
205      * @param resource        $inputStream
206      *
207      * @return string
208      */
209     private function autocomplete(OutputInterface $output, Question $question, $inputStream)
210     {
211         $autocomplete = $question->getAutocompleterValues();
212         $ret = '';
213
214         $i = 0;
215         $ofs = -1;
216         $matches = $autocomplete;
217         $numMatches = count($matches);
218
219         $sttyMode = shell_exec('stty -g');
220
221         // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
222         shell_exec('stty -icanon -echo');
223
224         // Add highlighted text style
225         $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
226
227         // Read a keypress
228         while (!feof($inputStream)) {
229             $c = fread($inputStream, 1);
230
231             // Backspace Character
232             if ("\177" === $c) {
233                 if (0 === $numMatches && 0 !== $i) {
234                     --$i;
235                     // Move cursor backwards
236                     $output->write("\033[1D");
237                 }
238
239                 if ($i === 0) {
240                     $ofs = -1;
241                     $matches = $autocomplete;
242                     $numMatches = count($matches);
243                 } else {
244                     $numMatches = 0;
245                 }
246
247                 // Pop the last character off the end of our string
248                 $ret = substr($ret, 0, $i);
249             } elseif ("\033" === $c) {
250                 // Did we read an escape sequence?
251                 $c .= fread($inputStream, 2);
252
253                 // A = Up Arrow. B = Down Arrow
254                 if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
255                     if ('A' === $c[2] && -1 === $ofs) {
256                         $ofs = 0;
257                     }
258
259                     if (0 === $numMatches) {
260                         continue;
261                     }
262
263                     $ofs += ('A' === $c[2]) ? -1 : 1;
264                     $ofs = ($numMatches + $ofs) % $numMatches;
265                 }
266             } elseif (ord($c) < 32) {
267                 if ("\t" === $c || "\n" === $c) {
268                     if ($numMatches > 0 && -1 !== $ofs) {
269                         $ret = $matches[$ofs];
270                         // Echo out remaining chars for current match
271                         $output->write(substr($ret, $i));
272                         $i = strlen($ret);
273                     }
274
275                     if ("\n" === $c) {
276                         $output->write($c);
277                         break;
278                     }
279
280                     $numMatches = 0;
281                 }
282
283                 continue;
284             } else {
285                 $output->write($c);
286                 $ret .= $c;
287                 ++$i;
288
289                 $numMatches = 0;
290                 $ofs = 0;
291
292                 foreach ($autocomplete as $value) {
293                     // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
294                     if (0 === strpos($value, $ret) && $i !== strlen($value)) {
295                         $matches[$numMatches++] = $value;
296                     }
297                 }
298             }
299
300             // Erase characters from cursor to end of line
301             $output->write("\033[K");
302
303             if ($numMatches > 0 && -1 !== $ofs) {
304                 // Save cursor position
305                 $output->write("\0337");
306                 // Write highlighted text
307                 $output->write('<hl>'.substr($matches[$ofs], $i).'</hl>');
308                 // Restore cursor position
309                 $output->write("\0338");
310             }
311         }
312
313         // Reset stty so it behaves normally again
314         shell_exec(sprintf('stty %s', $sttyMode));
315
316         return $ret;
317     }
318
319     /**
320      * Gets a hidden response from user.
321      *
322      * @param OutputInterface $output      An Output instance
323      * @param resource        $inputStream The handler resource
324      *
325      * @return string The answer
326      *
327      * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
328      */
329     private function getHiddenResponse(OutputInterface $output, $inputStream)
330     {
331         if ('\\' === DIRECTORY_SEPARATOR) {
332             $exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
333
334             // handle code running from a phar
335             if ('phar:' === substr(__FILE__, 0, 5)) {
336                 $tmpExe = sys_get_temp_dir().'/hiddeninput.exe';
337                 copy($exe, $tmpExe);
338                 $exe = $tmpExe;
339             }
340
341             $value = rtrim(shell_exec($exe));
342             $output->writeln('');
343
344             if (isset($tmpExe)) {
345                 unlink($tmpExe);
346             }
347
348             return $value;
349         }
350
351         if ($this->hasSttyAvailable()) {
352             $sttyMode = shell_exec('stty -g');
353
354             shell_exec('stty -echo');
355             $value = fgets($inputStream, 4096);
356             shell_exec(sprintf('stty %s', $sttyMode));
357
358             if (false === $value) {
359                 throw new RuntimeException('Aborted');
360             }
361
362             $value = trim($value);
363             $output->writeln('');
364
365             return $value;
366         }
367
368         if (false !== $shell = $this->getShell()) {
369             $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword';
370             $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
371             $value = rtrim(shell_exec($command));
372             $output->writeln('');
373
374             return $value;
375         }
376
377         throw new RuntimeException('Unable to hide the response.');
378     }
379
380     /**
381      * Validates an attempt.
382      *
383      * @param callable        $interviewer A callable that will ask for a question and return the result
384      * @param OutputInterface $output      An Output instance
385      * @param Question        $question    A Question instance
386      *
387      * @return mixed The validated response
388      *
389      * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
390      */
391     private function validateAttempts($interviewer, OutputInterface $output, Question $question)
392     {
393         $error = null;
394         $attempts = $question->getMaxAttempts();
395         while (null === $attempts || $attempts--) {
396             if (null !== $error) {
397                 $this->writeError($output, $error);
398             }
399
400             try {
401                 return call_user_func($question->getValidator(), $interviewer());
402             } catch (RuntimeException $e) {
403                 throw $e;
404             } catch (\Exception $error) {
405             }
406         }
407
408         throw $error;
409     }
410
411     /**
412      * Returns a valid unix shell.
413      *
414      * @return string|bool The valid shell name, false in case no valid shell is found
415      */
416     private function getShell()
417     {
418         if (null !== self::$shell) {
419             return self::$shell;
420         }
421
422         self::$shell = false;
423
424         if (file_exists('/usr/bin/env')) {
425             // handle other OSs with bash/zsh/ksh/csh if available to hide the answer
426             $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
427             foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) {
428                 if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
429                     self::$shell = $sh;
430                     break;
431                 }
432             }
433         }
434
435         return self::$shell;
436     }
437
438     /**
439      * Returns whether Stty is available or not.
440      *
441      * @return bool
442      */
443     private function hasSttyAvailable()
444     {
445         if (null !== self::$stty) {
446             return self::$stty;
447         }
448
449         exec('stty 2>&1', $output, $exitcode);
450
451         return self::$stty = $exitcode === 0;
452     }
453 }