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