PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.8.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.8.2
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / vendor / prefixed / symfony / console / Helper / QuestionHelper.php
matomo / app / vendor / prefixed / symfony / console / Helper Last commit date
DebugFormatterHelper.php 1 year ago DescriptorHelper.php 1 year ago Dumper.php 1 year ago FormatterHelper.php 1 year ago Helper.php 1 year ago HelperInterface.php 2 years ago HelperSet.php 2 years ago InputAwareHelper.php 2 years ago ProcessHelper.php 2 years ago ProgressBar.php 1 year ago ProgressIndicator.php 1 year ago QuestionHelper.php 1 year ago SymfonyQuestionHelper.php 1 year ago Table.php 1 year ago TableCell.php 2 years ago TableCellStyle.php 2 years ago TableRows.php 2 years ago TableSeparator.php 2 years ago TableStyle.php 1 year ago
QuestionHelper.php
504 lines
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 namespace Matomo\Dependencies\Symfony\Component\Console\Helper;
12
13 use Matomo\Dependencies\Symfony\Component\Console\Cursor;
14 use Matomo\Dependencies\Symfony\Component\Console\Exception\MissingInputException;
15 use Matomo\Dependencies\Symfony\Component\Console\Exception\RuntimeException;
16 use Matomo\Dependencies\Symfony\Component\Console\Formatter\OutputFormatter;
17 use Matomo\Dependencies\Symfony\Component\Console\Formatter\OutputFormatterStyle;
18 use Matomo\Dependencies\Symfony\Component\Console\Input\InputInterface;
19 use Matomo\Dependencies\Symfony\Component\Console\Input\StreamableInputInterface;
20 use Matomo\Dependencies\Symfony\Component\Console\Output\ConsoleOutputInterface;
21 use Matomo\Dependencies\Symfony\Component\Console\Output\ConsoleSectionOutput;
22 use Matomo\Dependencies\Symfony\Component\Console\Output\OutputInterface;
23 use Matomo\Dependencies\Symfony\Component\Console\Question\ChoiceQuestion;
24 use Matomo\Dependencies\Symfony\Component\Console\Question\Question;
25 use Matomo\Dependencies\Symfony\Component\Console\Terminal;
26 use function Matomo\Dependencies\Symfony\Component\String\s;
27 /**
28 * The QuestionHelper class provides helpers to interact with the user.
29 *
30 * @author Fabien Potencier <fabien@symfony.com>
31 */
32 class QuestionHelper extends Helper
33 {
34 /**
35 * @var resource|null
36 */
37 private $inputStream;
38 private static $stty = \true;
39 private static $stdinIsInteractive;
40 /**
41 * Asks a question to the user.
42 *
43 * @return mixed The user answer
44 *
45 * @throws RuntimeException If there is no data to read in the input stream
46 */
47 public function ask(InputInterface $input, OutputInterface $output, Question $question)
48 {
49 if ($output instanceof ConsoleOutputInterface) {
50 $output = $output->getErrorOutput();
51 }
52 if (!$input->isInteractive()) {
53 return $this->getDefaultAnswer($question);
54 }
55 if ($input instanceof StreamableInputInterface && ($stream = $input->getStream())) {
56 $this->inputStream = $stream;
57 }
58 try {
59 if (!$question->getValidator()) {
60 return $this->doAsk($output, $question);
61 }
62 $interviewer = function () use($output, $question) {
63 return $this->doAsk($output, $question);
64 };
65 return $this->validateAttempts($interviewer, $output, $question);
66 } catch (MissingInputException $exception) {
67 $input->setInteractive(\false);
68 if (null === ($fallbackOutput = $this->getDefaultAnswer($question))) {
69 throw $exception;
70 }
71 return $fallbackOutput;
72 }
73 }
74 /**
75 * {@inheritdoc}
76 */
77 public function getName()
78 {
79 return 'question';
80 }
81 /**
82 * Prevents usage of stty.
83 */
84 public static function disableStty()
85 {
86 self::$stty = \false;
87 }
88 /**
89 * Asks the question to the user.
90 *
91 * @return mixed
92 *
93 * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
94 */
95 private function doAsk(OutputInterface $output, Question $question)
96 {
97 $this->writePrompt($output, $question);
98 $inputStream = $this->inputStream ?: \STDIN;
99 $autocomplete = $question->getAutocompleterCallback();
100 if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
101 $ret = \false;
102 if ($question->isHidden()) {
103 try {
104 $hiddenResponse = $this->getHiddenResponse($output, $inputStream, $question->isTrimmable());
105 $ret = $question->isTrimmable() ? trim($hiddenResponse) : $hiddenResponse;
106 } catch (RuntimeException $e) {
107 if (!$question->isHiddenFallback()) {
108 throw $e;
109 }
110 }
111 }
112 if (\false === $ret) {
113 $isBlocked = stream_get_meta_data($inputStream)['blocked'] ?? \true;
114 if (!$isBlocked) {
115 stream_set_blocking($inputStream, \true);
116 }
117 $ret = $this->readInput($inputStream, $question);
118 if (!$isBlocked) {
119 stream_set_blocking($inputStream, \false);
120 }
121 if (\false === $ret) {
122 throw new MissingInputException('Aborted.');
123 }
124 if ($question->isTrimmable()) {
125 $ret = trim($ret);
126 }
127 }
128 } else {
129 $autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete);
130 $ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete;
131 }
132 if ($output instanceof ConsoleSectionOutput) {
133 $output->addContent($ret);
134 }
135 $ret = \strlen($ret) > 0 ? $ret : $question->getDefault();
136 if ($normalizer = $question->getNormalizer()) {
137 return $normalizer($ret);
138 }
139 return $ret;
140 }
141 /**
142 * @return mixed
143 */
144 private function getDefaultAnswer(Question $question)
145 {
146 $default = $question->getDefault();
147 if (null === $default) {
148 return $default;
149 }
150 if ($validator = $question->getValidator()) {
151 return \call_user_func($question->getValidator(), $default);
152 } elseif ($question instanceof ChoiceQuestion) {
153 $choices = $question->getChoices();
154 if (!$question->isMultiselect()) {
155 return $choices[$default] ?? $default;
156 }
157 $default = explode(',', $default);
158 foreach ($default as $k => $v) {
159 $v = $question->isTrimmable() ? trim($v) : $v;
160 $default[$k] = $choices[$v] ?? $v;
161 }
162 }
163 return $default;
164 }
165 /**
166 * Outputs the question prompt.
167 */
168 protected function writePrompt(OutputInterface $output, Question $question)
169 {
170 $message = $question->getQuestion();
171 if ($question instanceof ChoiceQuestion) {
172 $output->writeln(array_merge([$question->getQuestion()], $this->formatChoiceQuestionChoices($question, 'info')));
173 $message = $question->getPrompt();
174 }
175 $output->write($message);
176 }
177 /**
178 * @return string[]
179 */
180 protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag)
181 {
182 $messages = [];
183 $maxWidth = max(array_map([__CLASS__, 'width'], array_keys($choices = $question->getChoices())));
184 foreach ($choices as $key => $value) {
185 $padding = str_repeat(' ', $maxWidth - self::width($key));
186 $messages[] = sprintf(" [<{$tag}>%s{$padding}</{$tag}>] %s", $key, $value);
187 }
188 return $messages;
189 }
190 /**
191 * Outputs an error message.
192 */
193 protected function writeError(OutputInterface $output, \Exception $error)
194 {
195 if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
196 $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
197 } else {
198 $message = '<error>' . $error->getMessage() . '</error>';
199 }
200 $output->writeln($message);
201 }
202 /**
203 * Autocompletes a question.
204 *
205 * @param resource $inputStream
206 */
207 private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete) : string
208 {
209 $cursor = new Cursor($output, $inputStream);
210 $fullChoice = '';
211 $ret = '';
212 $i = 0;
213 $ofs = -1;
214 $matches = $autocomplete($ret);
215 $numMatches = \count($matches);
216 $sttyMode = shell_exec('stty -g');
217 $isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null);
218 $r = [$inputStream];
219 $w = [];
220 // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
221 shell_exec('stty -icanon -echo');
222 // Add highlighted text style
223 $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
224 // Read a keypress
225 while (!feof($inputStream)) {
226 while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) {
227 // Give signal handlers a chance to run
228 $r = [$inputStream];
229 }
230 $c = fread($inputStream, 1);
231 // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
232 if (\false === $c || '' === $ret && '' === $c && null === $question->getDefault()) {
233 shell_exec('stty ' . $sttyMode);
234 throw new MissingInputException('Aborted.');
235 } elseif ("" === $c) {
236 // Backspace Character
237 if (0 === $numMatches && 0 !== $i) {
238 --$i;
239 $cursor->moveLeft(s($fullChoice)->slice(-1)->width(\false));
240 $fullChoice = self::substr($fullChoice, 0, $i);
241 }
242 if (0 === $i) {
243 $ofs = -1;
244 $matches = $autocomplete($ret);
245 $numMatches = \count($matches);
246 } else {
247 $numMatches = 0;
248 }
249 // Pop the last character off the end of our string
250 $ret = self::substr($ret, 0, $i);
251 } elseif ("\x1b" === $c) {
252 // Did we read an escape sequence?
253 $c .= fread($inputStream, 2);
254 // A = Up Arrow. B = Down Arrow
255 if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
256 if ('A' === $c[2] && -1 === $ofs) {
257 $ofs = 0;
258 }
259 if (0 === $numMatches) {
260 continue;
261 }
262 $ofs += 'A' === $c[2] ? -1 : 1;
263 $ofs = ($numMatches + $ofs) % $numMatches;
264 }
265 } elseif (\ord($c) < 32) {
266 if ("\t" === $c || "\n" === $c) {
267 if ($numMatches > 0 && -1 !== $ofs) {
268 $ret = (string) $matches[$ofs];
269 // Echo out remaining chars for current match
270 $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
271 $output->write($remainingCharacters);
272 $fullChoice .= $remainingCharacters;
273 $i = \false === ($encoding = mb_detect_encoding($fullChoice, null, \true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding);
274 $matches = array_filter($autocomplete($ret), function ($match) use($ret) {
275 return '' === $ret || str_starts_with($match, $ret);
276 });
277 $numMatches = \count($matches);
278 $ofs = -1;
279 }
280 if ("\n" === $c) {
281 $output->write($c);
282 break;
283 }
284 $numMatches = 0;
285 }
286 continue;
287 } else {
288 if ("\x80" <= $c) {
289 $c .= fread($inputStream, ["\xc0" => 1, "\xd0" => 1, "\xe0" => 2, "\xf0" => 3][$c & "\xf0"]);
290 }
291 $output->write($c);
292 $ret .= $c;
293 $fullChoice .= $c;
294 ++$i;
295 $tempRet = $ret;
296 if ($question instanceof ChoiceQuestion && $question->isMultiselect()) {
297 $tempRet = $this->mostRecentlyEnteredValue($fullChoice);
298 }
299 $numMatches = 0;
300 $ofs = 0;
301 foreach ($autocomplete($ret) as $value) {
302 // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
303 if (str_starts_with($value, $tempRet)) {
304 $matches[$numMatches++] = $value;
305 }
306 }
307 }
308 $cursor->clearLineAfter();
309 if ($numMatches > 0 && -1 !== $ofs) {
310 $cursor->savePosition();
311 // Write highlighted text, complete the partially entered response
312 $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)));
313 $output->write('<hl>' . OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)) . '</hl>');
314 $cursor->restorePosition();
315 }
316 }
317 // Reset stty so it behaves normally again
318 shell_exec('stty ' . $sttyMode);
319 return $fullChoice;
320 }
321 private function mostRecentlyEnteredValue(string $entered) : string
322 {
323 // Determine the most recent value that the user entered
324 if (!str_contains($entered, ',')) {
325 return $entered;
326 }
327 $choices = explode(',', $entered);
328 if ('' !== ($lastChoice = trim($choices[\count($choices) - 1]))) {
329 return $lastChoice;
330 }
331 return $entered;
332 }
333 /**
334 * Gets a hidden response from user.
335 *
336 * @param resource $inputStream The handler resource
337 * @param bool $trimmable Is the answer trimmable
338 *
339 * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
340 */
341 private function getHiddenResponse(OutputInterface $output, $inputStream, bool $trimmable = \true) : string
342 {
343 if ('\\' === \DIRECTORY_SEPARATOR) {
344 $exe = __DIR__ . '/../Resources/bin/hiddeninput.exe';
345 // handle code running from a phar
346 if ('phar:' === substr(__FILE__, 0, 5)) {
347 $tmpExe = sys_get_temp_dir() . '/hiddeninput.exe';
348 copy($exe, $tmpExe);
349 $exe = $tmpExe;
350 }
351 $sExec = shell_exec('"' . $exe . '"');
352 $value = $trimmable ? rtrim($sExec) : $sExec;
353 $output->writeln('');
354 if (isset($tmpExe)) {
355 unlink($tmpExe);
356 }
357 return $value;
358 }
359 if (self::$stty && Terminal::hasSttyAvailable()) {
360 $sttyMode = shell_exec('stty -g');
361 shell_exec('stty -echo');
362 } elseif ($this->isInteractiveInput($inputStream)) {
363 throw new RuntimeException('Unable to hide the response.');
364 }
365 $value = fgets($inputStream, 4096);
366 if (self::$stty && Terminal::hasSttyAvailable()) {
367 shell_exec('stty ' . $sttyMode);
368 }
369 if (\false === $value) {
370 throw new MissingInputException('Aborted.');
371 }
372 if ($trimmable) {
373 $value = trim($value);
374 }
375 $output->writeln('');
376 return $value;
377 }
378 /**
379 * Validates an attempt.
380 *
381 * @param callable $interviewer A callable that will ask for a question and return the result
382 *
383 * @return mixed The validated response
384 *
385 * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
386 */
387 private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question)
388 {
389 $error = null;
390 $attempts = $question->getMaxAttempts();
391 while (null === $attempts || $attempts--) {
392 if (null !== $error) {
393 $this->writeError($output, $error);
394 }
395 try {
396 return $question->getValidator()($interviewer());
397 } catch (RuntimeException $e) {
398 throw $e;
399 } catch (\Exception $error) {
400 }
401 }
402 throw $error;
403 }
404 private function isInteractiveInput($inputStream) : bool
405 {
406 if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) {
407 return \false;
408 }
409 if (null !== self::$stdinIsInteractive) {
410 return self::$stdinIsInteractive;
411 }
412 return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r'));
413 }
414 /**
415 * Reads one or more lines of input and returns what is read.
416 *
417 * @param resource $inputStream The handler resource
418 * @param Question $question The question being asked
419 *
420 * @return string|false The input received, false in case input could not be read
421 */
422 private function readInput($inputStream, Question $question)
423 {
424 if (!$question->isMultiline()) {
425 $cp = $this->setIOCodepage();
426 $ret = fgets($inputStream, 4096);
427 return $this->resetIOCodepage($cp, $ret);
428 }
429 $multiLineStreamReader = $this->cloneInputStream($inputStream);
430 if (null === $multiLineStreamReader) {
431 return \false;
432 }
433 $ret = '';
434 $cp = $this->setIOCodepage();
435 while (\false !== ($char = fgetc($multiLineStreamReader))) {
436 if (\PHP_EOL === "{$ret}{$char}") {
437 break;
438 }
439 $ret .= $char;
440 }
441 return $this->resetIOCodepage($cp, $ret);
442 }
443 /**
444 * Sets console I/O to the host code page.
445 *
446 * @return int Previous code page in IBM/EBCDIC format
447 */
448 private function setIOCodepage() : int
449 {
450 if (\function_exists('sapi_windows_cp_set')) {
451 $cp = sapi_windows_cp_get();
452 sapi_windows_cp_set(sapi_windows_cp_get('oem'));
453 return $cp;
454 }
455 return 0;
456 }
457 /**
458 * Sets console I/O to the specified code page and converts the user input.
459 *
460 * @param string|false $input
461 *
462 * @return string|false
463 */
464 private function resetIOCodepage(int $cp, $input)
465 {
466 if (0 !== $cp) {
467 sapi_windows_cp_set($cp);
468 if (\false !== $input && '' !== $input) {
469 $input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input);
470 }
471 }
472 return $input;
473 }
474 /**
475 * Clones an input stream in order to act on one instance of the same
476 * stream without affecting the other instance.
477 *
478 * @param resource $inputStream The handler resource
479 *
480 * @return resource|null The cloned resource, null in case it could not be cloned
481 */
482 private function cloneInputStream($inputStream)
483 {
484 $streamMetaData = stream_get_meta_data($inputStream);
485 $seekable = $streamMetaData['seekable'] ?? \false;
486 $mode = $streamMetaData['mode'] ?? 'rb';
487 $uri = $streamMetaData['uri'] ?? null;
488 if (null === $uri) {
489 return null;
490 }
491 $cloneStream = fopen($uri, $mode);
492 // For seekable and writable streams, add all the same data to the
493 // cloned stream and then seek to the same offset.
494 if (\true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) {
495 $offset = ftell($inputStream);
496 rewind($inputStream);
497 stream_copy_to_stream($inputStream, $cloneStream);
498 fseek($inputStream, $offset);
499 fseek($cloneStream, $offset);
500 }
501 return $cloneStream;
502 }
503 }
504