PluginProbe
Depicter — Popup & Slider Builder / 1.3.2
Depicter — Popup & Slider Builder v1.3.2
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 1.3.2, at vendor/symfony/console/Helper/QuestionHelper.php

506 lines 15.9 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 use function Symfony\Component\String\s;
28
29 /**
30 * The QuestionHelper class provides helpers to interact with the user.
31 *
32 * @author Fabien Potencier <fabien@symfony.com>
33 */
34 class QuestionHelper extends Helper
35 {
36 private $inputStream;
37 private static $shell;
38 private static $stty = true;
39 private static $stdinIsInteractive;
40
41 /**
42 * Asks a question to the user.
43 *
44 * @return mixed The user answer
45 *
46 * @throws RuntimeException If there is no data to read in the input stream
47 */
48 public function ask(InputInterface $input, OutputInterface $output, Question $question)
49 {
50 if ($output instanceof ConsoleOutputInterface) {
51 $output = $output->getErrorOutput();
52 }
53
54 if (!$input->isInteractive()) {
55 return $this->getDefaultAnswer($question);
56 }
57
58 if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) {
59 $this->inputStream = $stream;
60 }
61
62 try {
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 } catch (MissingInputException $exception) {
73 $input->setInteractive(false);
74
75 if (null === $fallbackOutput = $this->getDefaultAnswer($question)) {
76 throw $exception;
77 }
78
79 return $fallbackOutput;
80 }
81 }
82
83 /**
84 * {@inheritdoc}
85 */
86 public function getName()
87 {
88 return 'question';
89 }
90
91 /**
92 * Prevents usage of stty.
93 */
94 public static function disableStty()
95 {
96 self::$stty = false;
97 }
98
99 /**
100 * Asks the question to the user.
101 *
102 * @return bool|mixed|string|null
103 *
104 * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
105 */
106 private function doAsk(OutputInterface $output, Question $question)
107 {
108 $this->writePrompt($output, $question);
109
110 $inputStream = $this->inputStream ?: \STDIN;
111 $autocomplete = $question->getAutocompleterCallback();
112
113 if (\function_exists('sapi_windows_cp_set')) {
114 // Codepage used by cmd.exe on Windows to allow special characters (éàüñ).
115 @sapi_windows_cp_set(1252);
116 }
117
118 if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
119 $ret = false;
120 if ($question->isHidden()) {
121 try {
122 $hiddenResponse = $this->getHiddenResponse($output, $inputStream, $question->isTrimmable());
123 $ret = $question->isTrimmable() ? trim($hiddenResponse) : $hiddenResponse;
124 } catch (RuntimeException $e) {
125 if (!$question->isHiddenFallback()) {
126 throw $e;
127 }
128 }
129 }
130
131 if (false === $ret) {
132 $ret = fgets($inputStream, 4096);
133 if (false === $ret) {
134 throw new MissingInputException('Aborted.');
135 }
136 if ($question->isTrimmable()) {
137 $ret = trim($ret);
138 }
139 }
140 } else {
141 $autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete);
142 $ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete;
143 }
144
145 if ($output instanceof ConsoleSectionOutput) {
146 $output->addContent($ret);
147 }
148
149 $ret = \strlen($ret) > 0 ? $ret : $question->getDefault();
150
151 if ($normalizer = $question->getNormalizer()) {
152 return $normalizer($ret);
153 }
154
155 return $ret;
156 }
157
158 /**
159 * @return mixed
160 */
161 private function getDefaultAnswer(Question $question)
162 {
163 $default = $question->getDefault();
164
165 if (null === $default) {
166 return $default;
167 }
168
169 if ($validator = $question->getValidator()) {
170 return \call_user_func($question->getValidator(), $default);
171 } elseif ($question instanceof ChoiceQuestion) {
172 $choices = $question->getChoices();
173
174 if (!$question->isMultiselect()) {
175 return $choices[$default] ?? $default;
176 }
177
178 $default = explode(',', $default);
179 foreach ($default as $k => $v) {
180 $v = $question->isTrimmable() ? trim($v) : $v;
181 $default[$k] = $choices[$v] ?? $v;
182 }
183 }
184
185 return $default;
186 }
187
188 /**
189 * Outputs the question prompt.
190 */
191 protected function writePrompt(OutputInterface $output, Question $question)
192 {
193 $message = $question->getQuestion();
194
195 if ($question instanceof ChoiceQuestion) {
196 $output->writeln(array_merge([
197 $question->getQuestion(),
198 ], $this->formatChoiceQuestionChoices($question, 'info')));
199
200 $message = $question->getPrompt();
201 }
202
203 $output->write($message);
204 }
205
206 /**
207 * @return string[]
208 */
209 protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag)
210 {
211 $messages = [];
212
213 $maxWidth = max(array_map('self::strlen', array_keys($choices = $question->getChoices())));
214
215 foreach ($choices as $key => $value) {
216 $padding = str_repeat(' ', $maxWidth - self::strlen($key));
217
218 $messages[] = sprintf(" [<$tag>%s$padding</$tag>] %s", $key, $value);
219 }
220
221 return $messages;
222 }
223
224 /**
225 * Outputs an error message.
226 */
227 protected function writeError(OutputInterface $output, \Exception $error)
228 {
229 if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
230 $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
231 } else {
232 $message = '<error>'.$error->getMessage().'</error>';
233 }
234
235 $output->writeln($message);
236 }
237
238 /**
239 * Autocompletes a question.
240 *
241 * @param resource $inputStream
242 */
243 private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
244 {
245 $cursor = new Cursor($output, $inputStream);
246
247 $fullChoice = '';
248 $ret = '';
249
250 $i = 0;
251 $ofs = -1;
252 $matches = $autocomplete($ret);
253 $numMatches = \count($matches);
254
255 $sttyMode = shell_exec('stty -g');
256
257 // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
258 shell_exec('stty -icanon -echo');
259
260 // Add highlighted text style
261 $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
262
263 // Read a keypress
264 while (!feof($inputStream)) {
265 $c = fread($inputStream, 1);
266
267 // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
268 if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
269 shell_exec(sprintf('stty %s', $sttyMode));
270 throw new MissingInputException('Aborted.');
271 } elseif ("\177" === $c) { // Backspace Character
272 if (0 === $numMatches && 0 !== $i) {
273 --$i;
274 $cursor->moveLeft(s($fullChoice)->slice(-1)->width(false));
275
276 $fullChoice = self::substr($fullChoice, 0, $i);
277 }
278
279 if (0 === $i) {
280 $ofs = -1;
281 $matches = $autocomplete($ret);
282 $numMatches = \count($matches);
283 } else {
284 $numMatches = 0;
285 }
286
287 // Pop the last character off the end of our string
288 $ret = self::substr($ret, 0, $i);
289 } elseif ("\033" === $c) {
290 // Did we read an escape sequence?
291 $c .= fread($inputStream, 2);
292
293 // A = Up Arrow. B = Down Arrow
294 if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
295 if ('A' === $c[2] && -1 === $ofs) {
296 $ofs = 0;
297 }
298
299 if (0 === $numMatches) {
300 continue;
301 }
302
303 $ofs += ('A' === $c[2]) ? -1 : 1;
304 $ofs = ($numMatches + $ofs) % $numMatches;
305 }
306 } elseif (\ord($c) < 32) {
307 if ("\t" === $c || "\n" === $c) {
308 if ($numMatches > 0 && -1 !== $ofs) {
309 $ret = (string) $matches[$ofs];
310 // Echo out remaining chars for current match
311 $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
312 $output->write($remainingCharacters);
313 $fullChoice .= $remainingCharacters;
314 $i = self::strlen($fullChoice);
315
316 $matches = array_filter(
317 $autocomplete($ret),
318 function ($match) use ($ret) {
319 return '' === $ret || 0 === strpos($match, $ret);
320 }
321 );
322 $numMatches = \count($matches);
323 $ofs = -1;
324 }
325
326 if ("\n" === $c) {
327 $output->write($c);
328 break;
329 }
330
331 $numMatches = 0;
332 }
333
334 continue;
335 } else {
336 if ("\x80" <= $c) {
337 $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]);
338 }
339
340 $output->write($c);
341 $ret .= $c;
342 $fullChoice .= $c;
343 ++$i;
344
345 $tempRet = $ret;
346
347 if ($question instanceof ChoiceQuestion && $question->isMultiselect()) {
348 $tempRet = $this->mostRecentlyEnteredValue($fullChoice);
349 }
350
351 $numMatches = 0;
352 $ofs = 0;
353
354 foreach ($autocomplete($ret) as $value) {
355 // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
356 if (0 === strpos($value, $tempRet)) {
357 $matches[$numMatches++] = $value;
358 }
359 }
360 }
361
362 $cursor->clearLineAfter();
363
364 if ($numMatches > 0 && -1 !== $ofs) {
365 $cursor->savePosition();
366 // Write highlighted text, complete the partially entered response
367 $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)));
368 $output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'</hl>');
369 $cursor->restorePosition();
370 }
371 }
372
373 // Reset stty so it behaves normally again
374 shell_exec(sprintf('stty %s', $sttyMode));
375
376 return $fullChoice;
377 }
378
379 private function mostRecentlyEnteredValue(string $entered): string
380 {
381 // Determine the most recent value that the user entered
382 if (false === strpos($entered, ',')) {
383 return $entered;
384 }
385
386 $choices = explode(',', $entered);
387 if (\strlen($lastChoice = trim($choices[\count($choices) - 1])) > 0) {
388 return $lastChoice;
389 }
390
391 return $entered;
392 }
393
394 /**
395 * Gets a hidden response from user.
396 *
397 * @param resource $inputStream The handler resource
398 * @param bool $trimmable Is the answer trimmable
399 *
400 * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
401 */
402 private function getHiddenResponse(OutputInterface $output, $inputStream, bool $trimmable = true): string
403 {
404 if ('\\' === \DIRECTORY_SEPARATOR) {
405 $exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
406
407 // handle code running from a phar
408 if ('phar:' === substr(__FILE__, 0, 5)) {
409 $tmpExe = sys_get_temp_dir().'/hiddeninput.exe';
410 copy($exe, $tmpExe);
411 $exe = $tmpExe;
412 }
413
414 $sExec = shell_exec($exe);
415 $value = $trimmable ? rtrim($sExec) : $sExec;
416 $output->writeln('');
417
418 if (isset($tmpExe)) {
419 unlink($tmpExe);
420 }
421
422 return $value;
423 }
424
425 if (self::$stty && Terminal::hasSttyAvailable()) {
426 $sttyMode = shell_exec('stty -g');
427 shell_exec('stty -echo');
428 } elseif ($this->isInteractiveInput($inputStream)) {
429 throw new RuntimeException('Unable to hide the response.');
430 }
431
432 $value = fgets($inputStream, 4096);
433
434 if (self::$stty && Terminal::hasSttyAvailable()) {
435 shell_exec(sprintf('stty %s', $sttyMode));
436 }
437
438 if (false === $value) {
439 throw new MissingInputException('Aborted.');
440 }
441 if ($trimmable) {
442 $value = trim($value);
443 }
444 $output->writeln('');
445
446 return $value;
447 }
448
449 /**
450 * Validates an attempt.
451 *
452 * @param callable $interviewer A callable that will ask for a question and return the result
453 *
454 * @return mixed The validated response
455 *
456 * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
457 */
458 private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question)
459 {
460 $error = null;
461 $attempts = $question->getMaxAttempts();
462
463 while (null === $attempts || $attempts--) {
464 if (null !== $error) {
465 $this->writeError($output, $error);
466 }
467
468 try {
469 return $question->getValidator()($interviewer());
470 } catch (RuntimeException $e) {
471 throw $e;
472 } catch (\Exception $error) {
473 }
474 }
475
476 throw $error;
477 }
478
479 private function isInteractiveInput($inputStream): bool
480 {
481 if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) {
482 return false;
483 }
484
485 if (null !== self::$stdinIsInteractive) {
486 return self::$stdinIsInteractive;
487 }
488
489 if (\function_exists('stream_isatty')) {
490 return self::$stdinIsInteractive = stream_isatty(fopen('php://stdin', 'r'));
491 }
492
493 if (\function_exists('posix_isatty')) {
494 return self::$stdinIsInteractive = posix_isatty(fopen('php://stdin', 'r'));
495 }
496
497 if (!\function_exists('exec')) {
498 return self::$stdinIsInteractive = true;
499 }
500
501 exec('stty 2> /dev/null', $output, $status);
502
503 return self::$stdinIsInteractive = 1 !== $status;
504 }
505 }
506