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