[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/libraries/vendor/symfony/console/Helper/ -> QuestionHelper.php (source)

   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  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer