PluginProbe
Depicter — Popup & Slider Builder / trunk
Depicter — Popup & Slider Builder vtrunk
4.8.1 trunk 1.0.0 1.1.0 1.1.2 1.1.4 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.3.5 1.3.8 1.5.0 1.5.1 1.5.2 1.5.5 1.6.0 1.6.1 1.6.2 1.7.0 All 76 releases
depicter / vendor / symfony / console / Helper / QuestionHelper.php

QuestionHelper.php in Depicter — Popup & Slider Builder trunk, at vendor/symfony/console/Helper/QuestionHelper.php

614 lines 19.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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\Cursor;
15 use Symfony\Component\Console\Exception\MissingInputException;
16 use Symfony\Component\Console\Exception\RuntimeException;
17 use Symfony\Component\Console\Formatter\OutputFormatter;
18 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
19 use Symfony\Component\Console\Input\InputInterface;
20 use Symfony\Component\Console\Input\StreamableInputInterface;
21 use Symfony\Component\Console\Output\ConsoleOutputInterface;
22 use Symfony\Component\Console\Output\ConsoleSectionOutput;
23 use Symfony\Component\Console\Output\OutputInterface;
24 use Symfony\Component\Console\Question\ChoiceQuestion;
25 use Symfony\Component\Console\Question\Question;
26 use Symfony\Component\Console\Terminal;
27
28 use function Symfony\Component\String\s;
29
30 /**
31 * The QuestionHelper class provides helpers to interact with the user.
32 *
33 * @author Fabien Potencier <fabien@symfony.com>
34 */
35 class QuestionHelper extends Helper
36 {
37 /**
38 * @var resource|null
39 */
40 private $inputStream;
41
42 private static $stty = true;
43 private static $stdinIsInteractive;
44
45 /**
46 * Asks a question to the user.
47 *
48 * @return mixed The user answer
49 *
50 * @throws RuntimeException If there is no data to read in the input stream
51 */
52 public function ask(InputInterface $input, OutputInterface $output, Question $question)
53 {
54 if ($output instanceof ConsoleOutputInterface) {
55 $output = $output->getErrorOutput();
56 }
57
58 if (!$input->isInteractive()) {
59 return $this->getDefaultAnswer($question);
60 }
61
62 if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) {
63 $this->inputStream = $stream;
64 }
65
66 try {
67 if (!$question->getValidator()) {
68 return $this->doAsk($output, $question);
69 }
70
71 $interviewer = function () use ($output, $question) {
72 return $this->doAsk($output, $question);
73 };
74
75 return $this->validateAttempts($interviewer, $output, $question);
76 } catch (MissingInputException $exception) {
77 $input->setInteractive(false);
78
79 if (null === $fallbackOutput = $this->getDefaultAnswer($question)) {
80 throw $exception;
81 }
82
83 return $fallbackOutput;
84 }
85 }
86
87 /**
88 * {@inheritdoc}
89 */
90 public function getName()
91 {
92 return 'question';
93 }
94
95 /**
96 * Prevents usage of stty.
97 */
98 public static function disableStty()
99 {
100 self::$stty = false;
101 }
102
103 /**
104 * Asks the question to the user.
105 *
106 * @return mixed
107 *
108 * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
109 */
110 private function doAsk(OutputInterface $output, Question $question)
111 {
112 $this->writePrompt($output, $question);
113
114 $inputStream = $this->inputStream ?: \STDIN;
115 $autocomplete = $question->getAutocompleterCallback();
116
117 if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
118 $ret = false;
119 if ($question->isHidden()) {
120 try {
121 $hiddenResponse = $this->getHiddenResponse($output, $inputStream, $question->isTrimmable());
122 $ret = $question->isTrimmable() ? trim($hiddenResponse) : $hiddenResponse;
123 } catch (RuntimeException $e) {
124 if (!$question->isHiddenFallback()) {
125 throw $e;
126 }
127 }
128 }
129
130 if (false === $ret) {
131 $isBlocked = stream_get_meta_data($inputStream)['blocked'] ?? true;
132
133 if (!$isBlocked) {
134 stream_set_blocking($inputStream, true);
135 }
136
137 $ret = $this->readInput($inputStream, $question);
138
139 if (!$isBlocked) {
140 stream_set_blocking($inputStream, false);
141 }
142
143 if (false === $ret) {
144 throw new MissingInputException('Aborted.');
145 }
146 if ($question->isTrimmable()) {
147 $ret = trim($ret);
148 }
149 }
150 } else {
151 $autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete);
152 $ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete;
153 }
154
155 if ($output instanceof ConsoleSectionOutput) {
156 $output->addContent($ret);
157 }
158
159 $ret = \strlen($ret) > 0 ? $ret : $question->getDefault();
160
161 if ($normalizer = $question->getNormalizer()) {
162 return $normalizer($ret);
163 }
164
165 return $ret;
166 }
167
168 /**
169 * @return mixed
170 */
171 private function getDefaultAnswer(Question $question)
172 {
173 $default = $question->getDefault();
174
175 if (null === $default) {
176 return $default;
177 }
178
179 if ($validator = $question->getValidator()) {
180 return \call_user_func($question->getValidator(), $default);
181 } elseif ($question instanceof ChoiceQuestion) {
182 $choices = $question->getChoices();
183
184 if (!$question->isMultiselect()) {
185 return $choices[$default] ?? $default;
186 }
187
188 $default = explode(',', $default);
189 foreach ($default as $k => $v) {
190 $v = $question->isTrimmable() ? trim($v) : $v;
191 $default[$k] = $choices[$v] ?? $v;
192 }
193 }
194
195 return $default;
196 }
197
198 /**
199 * Outputs the question prompt.
200 */
201 protected function writePrompt(OutputInterface $output, Question $question)
202 {
203 $message = $question->getQuestion();
204
205 if ($question instanceof ChoiceQuestion) {
206 $output->writeln(array_merge([
207 $question->getQuestion(),
208 ], $this->formatChoiceQuestionChoices($question, 'info')));
209
210 $message = $question->getPrompt();
211 }
212
213 $output->write($message);
214 }
215
216 /**
217 * @return string[]
218 */
219 protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag)
220 {
221 $messages = [];
222
223 $maxWidth = max(array_map([__CLASS__, 'width'], array_keys($choices = $question->getChoices())));
224
225 foreach ($choices as $key => $value) {
226 $padding = str_repeat(' ', $maxWidth - self::width($key));
227
228 $messages[] = sprintf(" [<$tag>%s$padding</$tag>] %s", $key, $value);
229 }
230
231 return $messages;
232 }
233
234 /**
235 * Outputs an error message.
236 */
237 protected function writeError(OutputInterface $output, \Exception $error)
238 {
239 if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
240 $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
241 } else {
242 $message = '<error>'.$error->getMessage().'</error>';
243 }
244
245 $output->writeln($message);
246 }
247
248 /**
249 * Autocompletes a question.
250 *
251 * @param resource $inputStream
252 */
253 private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
254 {
255 $cursor = new Cursor($output, $inputStream);
256
257 $fullChoice = '';
258 $ret = '';
259
260 $i = 0;
261 $ofs = -1;
262 $matches = $autocomplete($ret);
263 $numMatches = \count($matches);
264
265 $sttyMode = shell_exec('stty -g');
266 $isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null);
267 $r = [$inputStream];
268 $w = [];
269
270 // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
271 shell_exec('stty -icanon -echo');
272
273 // Add highlighted text style
274 $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
275
276 // Read a keypress
277 while (!feof($inputStream)) {
278 while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) {
279 // Give signal handlers a chance to run
280 $r = [$inputStream];
281 }
282 $c = fread($inputStream, 1);
283
284 // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
285 if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
286 shell_exec('stty '.$sttyMode);
287 throw new MissingInputException('Aborted.');
288 } elseif ("\177" === $c) { // Backspace Character
289 if (0 === $numMatches && 0 !== $i) {
290 --$i;
291 $cursor->moveLeft(s($fullChoice)->slice(-1)->width(false));
292
293 $fullChoice = self::substr($fullChoice, 0, $i);
294 }
295
296 if (0 === $i) {
297 $ofs = -1;
298 $matches = $autocomplete($ret);
299 $numMatches = \count($matches);
300 } else {
301 $numMatches = 0;
302 }
303
304 // Pop the last character off the end of our string
305 $ret = self::substr($ret, 0, $i);
306 } elseif ("\033" === $c) {
307 // Did we read an escape sequence?
308 $c .= fread($inputStream, 2);
309
310 // A = Up Arrow. B = Down Arrow
311 if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
312 if ('A' === $c[2] && -1 === $ofs) {
313 $ofs = 0;
314 }
315
316 if (0 === $numMatches) {
317 continue;
318 }
319
320 $ofs += ('A' === $c[2]) ? -1 : 1;
321 $ofs = ($numMatches + $ofs) % $numMatches;
322 }
323 } elseif (\ord($c) < 32) {
324 if ("\t" === $c || "\n" === $c) {
325 if ($numMatches > 0 && -1 !== $ofs) {
326 $ret = (string) $matches[$ofs];
327 // Echo out remaining chars for current match
328 $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
329 $output->write($remainingCharacters);
330 $fullChoice .= $remainingCharacters;
331 $i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding);
332
333 $matches = array_filter(
334 $autocomplete($ret),
335 function ($match) use ($ret) {
336 return '' === $ret || str_starts_with($match, $ret);
337 }
338 );
339 $numMatches = \count($matches);
340 $ofs = -1;
341 }
342
343 if ("\n" === $c) {
344 $output->write($c);
345 break;
346 }
347
348 $numMatches = 0;
349 }
350
351 continue;
352 } else {
353 if ("\x80" <= $c) {
354 $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]);
355 }
356
357 $output->write($c);
358 $ret .= $c;
359 $fullChoice .= $c;
360 ++$i;
361
362 $tempRet = $ret;
363
364 if ($question instanceof ChoiceQuestion && $question->isMultiselect()) {
365 $tempRet = $this->mostRecentlyEnteredValue($fullChoice);
366 }
367
368 $numMatches = 0;
369 $ofs = 0;
370
371 foreach ($autocomplete($ret) as $value) {
372 // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
373 if (str_starts_with($value, $tempRet)) {
374 $matches[$numMatches++] = $value;
375 }
376 }
377 }
378
379 $cursor->clearLineAfter();
380
381 if ($numMatches > 0 && -1 !== $ofs) {
382 $cursor->savePosition();
383 // Write highlighted text, complete the partially entered response
384 $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)));
385 $output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'</hl>');
386 $cursor->restorePosition();
387 }
388 }
389
390 // Reset stty so it behaves normally again
391 shell_exec('stty '.$sttyMode);
392
393 return $fullChoice;
394 }
395
396 private function mostRecentlyEnteredValue(string $entered): string
397 {
398 // Determine the most recent value that the user entered
399 if (!str_contains($entered, ',')) {
400 return $entered;
401 }
402
403 $choices = explode(',', $entered);
404 if ('' !== $lastChoice = trim($choices[\count($choices) - 1])) {
405 return $lastChoice;
406 }
407
408 return $entered;
409 }
410
411 /**
412 * Gets a hidden response from user.
413 *
414 * @param resource $inputStream The handler resource
415 * @param bool $trimmable Is the answer trimmable
416 *
417 * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
418 */
419 private function getHiddenResponse(OutputInterface $output, $inputStream, bool $trimmable = true): string
420 {
421 if ('\\' === \DIRECTORY_SEPARATOR) {
422 $exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
423
424 // handle code running from a phar
425 if ('phar:' === substr(__FILE__, 0, 5)) {
426 $tmpExe = sys_get_temp_dir().'/hiddeninput.exe';
427 copy($exe, $tmpExe);
428 $exe = $tmpExe;
429 }
430
431 $sExec = shell_exec('"'.$exe.'"');
432 $value = $trimmable ? rtrim($sExec) : $sExec;
433 $output->writeln('');
434
435 if (isset($tmpExe)) {
436 unlink($tmpExe);
437 }
438
439 return $value;
440 }
441
442 if (self::$stty && Terminal::hasSttyAvailable()) {
443 $sttyMode = shell_exec('stty -g');
444 shell_exec('stty -echo');
445 } elseif ($this->isInteractiveInput($inputStream)) {
446 throw new RuntimeException('Unable to hide the response.');
447 }
448
449 $value = fgets($inputStream, 4096);
450
451 if (self::$stty && Terminal::hasSttyAvailable()) {
452 shell_exec('stty '.$sttyMode);
453 }
454
455 if (false === $value) {
456 throw new MissingInputException('Aborted.');
457 }
458 if ($trimmable) {
459 $value = trim($value);
460 }
461 $output->writeln('');
462
463 return $value;
464 }
465
466 /**
467 * Validates an attempt.
468 *
469 * @param callable $interviewer A callable that will ask for a question and return the result
470 *
471 * @return mixed The validated response
472 *
473 * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
474 */
475 private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question)
476 {
477 $error = null;
478 $attempts = $question->getMaxAttempts();
479
480 while (null === $attempts || $attempts--) {
481 if (null !== $error) {
482 $this->writeError($output, $error);
483 }
484
485 try {
486 return $question->getValidator()($interviewer());
487 } catch (RuntimeException $e) {
488 throw $e;
489 } catch (\Exception $error) {
490 }
491 }
492
493 throw $error;
494 }
495
496 private function isInteractiveInput($inputStream): bool
497 {
498 if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) {
499 return false;
500 }
501
502 if (null !== self::$stdinIsInteractive) {
503 return self::$stdinIsInteractive;
504 }
505
506 return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r'));
507 }
508
509 /**
510 * Reads one or more lines of input and returns what is read.
511 *
512 * @param resource $inputStream The handler resource
513 * @param Question $question The question being asked
514 *
515 * @return string|false The input received, false in case input could not be read
516 */
517 private function readInput($inputStream, Question $question)
518 {
519 if (!$question->isMultiline()) {
520 $cp = $this->setIOCodepage();
521 $ret = fgets($inputStream, 4096);
522
523 return $this->resetIOCodepage($cp, $ret);
524 }
525
526 $multiLineStreamReader = $this->cloneInputStream($inputStream);
527 if (null === $multiLineStreamReader) {
528 return false;
529 }
530
531 $ret = '';
532 $cp = $this->setIOCodepage();
533 while (false !== ($char = fgetc($multiLineStreamReader))) {
534 if (\PHP_EOL === "{$ret}{$char}") {
535 break;
536 }
537 $ret .= $char;
538 }
539
540 return $this->resetIOCodepage($cp, $ret);
541 }
542
543 /**
544 * Sets console I/O to the host code page.
545 *
546 * @return int Previous code page in IBM/EBCDIC format
547 */
548 private function setIOCodepage(): int
549 {
550 if (\function_exists('sapi_windows_cp_set')) {
551 $cp = sapi_windows_cp_get();
552 sapi_windows_cp_set(sapi_windows_cp_get('oem'));
553
554 return $cp;
555 }
556
557 return 0;
558 }
559
560 /**
561 * Sets console I/O to the specified code page and converts the user input.
562 *
563 * @param string|false $input
564 *
565 * @return string|false
566 */
567 private function resetIOCodepage(int $cp, $input)
568 {
569 if (0 !== $cp) {
570 sapi_windows_cp_set($cp);
571
572 if (false !== $input && '' !== $input) {
573 $input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input);
574 }
575 }
576
577 return $input;
578 }
579
580 /**
581 * Clones an input stream in order to act on one instance of the same
582 * stream without affecting the other instance.
583 *
584 * @param resource $inputStream The handler resource
585 *
586 * @return resource|null The cloned resource, null in case it could not be cloned
587 */
588 private function cloneInputStream($inputStream)
589 {
590 $streamMetaData = stream_get_meta_data($inputStream);
591 $seekable = $streamMetaData['seekable'] ?? false;
592 $mode = $streamMetaData['mode'] ?? 'rb';
593 $uri = $streamMetaData['uri'] ?? null;
594
595 if (null === $uri) {
596 return null;
597 }
598
599 $cloneStream = fopen($uri, $mode);
600
601 // For seekable and writable streams, add all the same data to the
602 // cloned stream and then seek to the same offset.
603 if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) {
604 $offset = ftell($inputStream);
605 rewind($inputStream);
606 stream_copy_to_stream($inputStream, $cloneStream);
607 fseek($inputStream, $offset);
608 fseek($cloneStream, $offset);
609 }
610
611 return $cloneStream;
612 }
613 }
614