GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 5cefd1...492078 )
by Anton
04:08
created

QuestionHelper   F

Complexity

Total Complexity 117

Size/Duplication

Total Lines 580
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 258
c 1
b 0
f 0
dl 0
loc 580
rs 2
wmc 117

17 Methods

Rating   Name   Duplication   Size   Complexity  
A disableStty() 0 3 1
B ask() 0 32 8
A getName() 0 3 1
A resetIOCodepage() 0 11 4
A writeError() 0 9 3
A readInput() 0 24 5
A setIOCodepage() 0 10 2
A cloneInputStream() 0 24 4
A formatChoiceQuestionChoices() 0 13 2
A isInteractiveInput() 0 25 6
B getDefaultAnswer() 0 25 7
D autocomplete() 0 141 35
A validateAttempts() 0 19 6
C getHiddenResponse() 0 45 12
A mostRecentlyEnteredValue() 0 13 3
A writePrompt() 0 13 2
D doAsk() 0 45 16

How to fix   Complexity   

Complex Class

Complex classes like QuestionHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QuestionHelper, and based on these observations, apply Extract Interface, too.

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
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 <[email protected]>
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);
0 ignored issues
show
Bug introduced by
The call to Symfony\Component\Consol...QuestionHelper::doAsk() has too few arguments starting with question. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

68
                return $this->/** @scrutinizer ignore-call */ doAsk($output, $question);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
$output of type Symfony\Component\Console\Output\OutputInterface is incompatible with the type resource expected by parameter $inputStream of Symfony\Component\Consol...QuestionHelper::doAsk(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

68
                return $this->doAsk(/** @scrutinizer ignore-type */ $output, $question);
Loading history...
Bug introduced by
$question of type Symfony\Component\Console\Question\Question is incompatible with the type Symfony\Component\Console\Output\OutputInterface expected by parameter $output of Symfony\Component\Consol...QuestionHelper::doAsk(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

68
                return $this->doAsk($output, /** @scrutinizer ignore-type */ $question);
Loading history...
69
            }
70
71
            $interviewer = function () use ($output, $question) {
72
                return $this->doAsk($output, $question);
0 ignored issues
show
Bug introduced by
The call to Symfony\Component\Consol...QuestionHelper::doAsk() has too few arguments starting with question. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

72
                return $this->/** @scrutinizer ignore-call */ doAsk($output, $question);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
$question of type Symfony\Component\Console\Question\Question is incompatible with the type Symfony\Component\Console\Output\OutputInterface expected by parameter $output of Symfony\Component\Consol...QuestionHelper::doAsk(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

72
                return $this->doAsk($output, /** @scrutinizer ignore-type */ $question);
Loading history...
Bug introduced by
$output of type Symfony\Component\Console\Output\OutputInterface is incompatible with the type resource expected by parameter $inputStream of Symfony\Component\Consol...QuestionHelper::doAsk(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

72
                return $this->doAsk(/** @scrutinizer ignore-type */ $output, $question);
Loading history...
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)) {
0 ignored issues
show
Bug introduced by
It seems like $question can also be of type Symfony\Component\Console\Output\OutputInterface; however, parameter $question of Symfony\Component\Consol...per::getDefaultAnswer() does only seem to accept Symfony\Component\Console\Question\Question, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

79
            if (null === $fallbackOutput = $this->getDefaultAnswer(/** @scrutinizer ignore-type */ $question)) {
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The property inputStream does not exist on Symfony\Component\Console\Helper\QuestionHelper. Did you maybe forget to declare it?
Loading history...
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
                $ret = $this->readInput($inputStream, $question);
132
                if (false === $ret) {
133
                    throw new MissingInputException('Aborted.');
134
                }
135
                if ($question->isTrimmable()) {
136
                    $ret = trim($ret);
137
                }
138
            }
139
        } else {
140
            $autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete);
141
            $ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete;
142
        }
143
144
        if ($output instanceof ConsoleSectionOutput) {
145
            $output->addContent($ret);
146
        }
147
148
        $ret = \strlen($ret) > 0 ? $ret : $question->getDefault();
149
150
        if ($normalizer = $question->getNormalizer()) {
151
            return $normalizer($ret);
152
        }
153
154
        return $ret;
155
    }
156
157
    /**
158
     * @return mixed
159
     */
160
    private function getDefaultAnswer(Question $question)
161
    {
162
        $default = $question->getDefault();
163
164
        if (null === $default) {
165
            return $default;
166
        }
167
168
        if ($validator = $question->getValidator()) {
0 ignored issues
show
Unused Code introduced by
The assignment to $validator is dead and can be removed.
Loading history...
169
            return \call_user_func($question->getValidator(), $default);
170
        } elseif ($question instanceof ChoiceQuestion) {
171
            $choices = $question->getChoices();
172
173
            if (!$question->isMultiselect()) {
174
                return $choices[$default] ?? $default;
175
            }
176
177
            $default = explode(',', $default);
0 ignored issues
show
Bug introduced by
It seems like $default can also be of type boolean; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

177
            $default = explode(',', /** @scrutinizer ignore-type */ $default);
Loading history...
178
            foreach ($default as $k => $v) {
179
                $v = $question->isTrimmable() ? trim($v) : $v;
180
                $default[$k] = $choices[$v] ?? $v;
181
            }
182
        }
183
184
        return $default;
185
    }
186
187
    /**
188
     * Outputs the question prompt.
189
     */
190
    protected function writePrompt(OutputInterface $output, Question $question)
191
    {
192
        $message = $question->getQuestion();
193
194
        if ($question instanceof ChoiceQuestion) {
195
            $output->writeln(array_merge([
196
                $question->getQuestion(),
197
            ], $this->formatChoiceQuestionChoices($question, 'info')));
198
199
            $message = $question->getPrompt();
200
        }
201
202
        $output->write($message);
203
    }
204
205
    /**
206
     * @return string[]
207
     */
208
    protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag)
209
    {
210
        $messages = [];
211
212
        $maxWidth = max(array_map([__CLASS__, 'width'], array_keys($choices = $question->getChoices())));
213
214
        foreach ($choices as $key => $value) {
215
            $padding = str_repeat(' ', $maxWidth - self::width($key));
216
217
            $messages[] = sprintf("  [<$tag>%s$padding</$tag>] %s", $key, $value);
218
        }
219
220
        return $messages;
221
    }
222
223
    /**
224
     * Outputs an error message.
225
     */
226
    protected function writeError(OutputInterface $output, \Exception $error)
227
    {
228
        if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
229
            $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
0 ignored issues
show
Bug introduced by
The method formatBlock() does not exist on Symfony\Component\Console\Helper\HelperInterface. It seems like you code against a sub-type of Symfony\Component\Console\Helper\HelperInterface such as Symfony\Component\Console\Helper\FormatterHelper. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

229
            $message = $this->getHelperSet()->get('formatter')->/** @scrutinizer ignore-call */ formatBlock($error->getMessage(), 'error');
Loading history...
230
        } else {
231
            $message = '<error>'.$error->getMessage().'</error>';
232
        }
233
234
        $output->writeln($message);
235
    }
236
237
    /**
238
     * Autocompletes a question.
239
     *
240
     * @param resource $inputStream
241
     */
242
    private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
243
    {
244
        $cursor = new Cursor($output, $inputStream);
245
246
        $fullChoice = '';
247
        $ret = '';
248
249
        $i = 0;
250
        $ofs = -1;
251
        $matches = $autocomplete($ret);
252
        $numMatches = \count($matches);
253
254
        $sttyMode = shell_exec('stty -g');
255
        $isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null);
256
        $r = [$inputStream];
257
        $w = [];
258
259
        // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
260
        shell_exec('stty -icanon -echo');
261
262
        // Add highlighted text style
263
        $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
264
265
        // Read a keypress
266
        while (!feof($inputStream)) {
267
            while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) {
268
                // Give signal handlers a chance to run
269
                $r = [$inputStream];
270
            }
271
            $c = fread($inputStream, 1);
272
273
            // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
274
            if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
275
                shell_exec('stty '.$sttyMode);
276
                throw new MissingInputException('Aborted.');
277
            } elseif ("\177" === $c) { // Backspace Character
278
                if (0 === $numMatches && 0 !== $i) {
279
                    --$i;
280
                    $cursor->moveLeft(s($fullChoice)->slice(-1)->width(false));
281
282
                    $fullChoice = self::substr($fullChoice, 0, $i);
283
                }
284
285
                if (0 === $i) {
286
                    $ofs = -1;
287
                    $matches = $autocomplete($ret);
288
                    $numMatches = \count($matches);
289
                } else {
290
                    $numMatches = 0;
291
                }
292
293
                // Pop the last character off the end of our string
294
                $ret = self::substr($ret, 0, $i);
295
            } elseif ("\033" === $c) {
296
                // Did we read an escape sequence?
297
                $c .= fread($inputStream, 2);
298
299
                // A = Up Arrow. B = Down Arrow
300
                if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
301
                    if ('A' === $c[2] && -1 === $ofs) {
302
                        $ofs = 0;
303
                    }
304
305
                    if (0 === $numMatches) {
306
                        continue;
307
                    }
308
309
                    $ofs += ('A' === $c[2]) ? -1 : 1;
310
                    $ofs = ($numMatches + $ofs) % $numMatches;
311
                }
312
            } elseif (\ord($c) < 32) {
313
                if ("\t" === $c || "\n" === $c) {
314
                    if ($numMatches > 0 && -1 !== $ofs) {
315
                        $ret = (string) $matches[$ofs];
316
                        // Echo out remaining chars for current match
317
                        $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
318
                        $output->write($remainingCharacters);
319
                        $fullChoice .= $remainingCharacters;
320
                        $i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding);
321
322
                        $matches = array_filter(
323
                            $autocomplete($ret),
324
                            function ($match) use ($ret) {
325
                                return '' === $ret || str_starts_with($match, $ret);
326
                            }
327
                        );
328
                        $numMatches = \count($matches);
0 ignored issues
show
Unused Code introduced by
The assignment to $numMatches is dead and can be removed.
Loading history...
329
                        $ofs = -1;
330
                    }
331
332
                    if ("\n" === $c) {
333
                        $output->write($c);
334
                        break;
335
                    }
336
337
                    $numMatches = 0;
338
                }
339
340
                continue;
341
            } else {
342
                if ("\x80" <= $c) {
343
                    $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]);
0 ignored issues
show
Bug introduced by
Are you sure you want to use the bitwise & or did you mean &&?
Loading history...
344
                }
345
346
                $output->write($c);
347
                $ret .= $c;
348
                $fullChoice .= $c;
349
                ++$i;
350
351
                $tempRet = $ret;
352
353
                if ($question instanceof ChoiceQuestion && $question->isMultiselect()) {
354
                    $tempRet = $this->mostRecentlyEnteredValue($fullChoice);
355
                }
356
357
                $numMatches = 0;
358
                $ofs = 0;
359
360
                foreach ($autocomplete($ret) as $value) {
361
                    // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
362
                    if (str_starts_with($value, $tempRet)) {
363
                        $matches[$numMatches++] = $value;
364
                    }
365
                }
366
            }
367
368
            $cursor->clearLineAfter();
369
370
            if ($numMatches > 0 && -1 !== $ofs) {
371
                $cursor->savePosition();
372
                // Write highlighted text, complete the partially entered response
373
                $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)));
374
                $output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'</hl>');
375
                $cursor->restorePosition();
376
            }
377
        }
378
379
        // Reset stty so it behaves normally again
380
        shell_exec('stty '.$sttyMode);
381
382
        return $fullChoice;
383
    }
384
385
    private function mostRecentlyEnteredValue(string $entered): string
386
    {
387
        // Determine the most recent value that the user entered
388
        if (!str_contains($entered, ',')) {
389
            return $entered;
390
        }
391
392
        $choices = explode(',', $entered);
393
        if ('' !== $lastChoice = trim($choices[\count($choices) - 1])) {
394
            return $lastChoice;
395
        }
396
397
        return $entered;
398
    }
399
400
    /**
401
     * Gets a hidden response from user.
402
     *
403
     * @param resource $inputStream The handler resource
404
     * @param bool     $trimmable   Is the answer trimmable
405
     *
406
     * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
407
     */
408
    private function getHiddenResponse(OutputInterface $output, $inputStream, bool $trimmable = true): string
409
    {
410
        if ('\\' === \DIRECTORY_SEPARATOR) {
411
            $exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
412
413
            // handle code running from a phar
414
            if ('phar:' === substr(__FILE__, 0, 5)) {
415
                $tmpExe = sys_get_temp_dir().'/hiddeninput.exe';
416
                copy($exe, $tmpExe);
417
                $exe = $tmpExe;
418
            }
419
420
            $sExec = shell_exec('"'.$exe.'"');
421
            $value = $trimmable ? rtrim($sExec) : $sExec;
422
            $output->writeln('');
423
424
            if (isset($tmpExe)) {
425
                unlink($tmpExe);
426
            }
427
428
            return $value;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $value could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
429
        }
430
431
        if (self::$stty && Terminal::hasSttyAvailable()) {
432
            $sttyMode = shell_exec('stty -g');
433
            shell_exec('stty -echo');
434
        } elseif ($this->isInteractiveInput($inputStream)) {
435
            throw new RuntimeException('Unable to hide the response.');
436
        }
437
438
        $value = fgets($inputStream, 4096);
439
440
        if (self::$stty && Terminal::hasSttyAvailable()) {
441
            shell_exec('stty '.$sttyMode);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sttyMode does not seem to be defined for all execution paths leading up to this point.
Loading history...
442
        }
443
444
        if (false === $value) {
445
            throw new MissingInputException('Aborted.');
446
        }
447
        if ($trimmable) {
448
            $value = trim($value);
449
        }
450
        $output->writeln('');
451
452
        return $value;
453
    }
454
455
    /**
456
     * Validates an attempt.
457
     *
458
     * @param callable $interviewer A callable that will ask for a question and return the result
459
     *
460
     * @return mixed The validated response
461
     *
462
     * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
463
     */
464
    private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question)
465
    {
466
        $error = null;
467
        $attempts = $question->getMaxAttempts();
468
469
        while (null === $attempts || $attempts--) {
470
            if (null !== $error) {
471
                $this->writeError($output, $error);
0 ignored issues
show
Bug introduced by
$error of type void is incompatible with the type Exception expected by parameter $error of Symfony\Component\Consol...ionHelper::writeError(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

471
                $this->writeError($output, /** @scrutinizer ignore-type */ $error);
Loading history...
472
            }
473
474
            try {
475
                return $question->getValidator()($interviewer());
476
            } catch (RuntimeException $e) {
477
                throw $e;
478
            } catch (\Exception $error) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
479
            }
480
        }
481
482
        throw $error;
483
    }
484
485
    private function isInteractiveInput($inputStream): bool
486
    {
487
        if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) {
488
            return false;
489
        }
490
491
        if (null !== self::$stdinIsInteractive) {
0 ignored issues
show
introduced by
The condition null !== self::stdinIsInteractive is always true.
Loading history...
492
            return self::$stdinIsInteractive;
493
        }
494
495
        if (\function_exists('stream_isatty')) {
496
            return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r'));
497
        }
498
499
        if (\function_exists('posix_isatty')) {
500
            return self::$stdinIsInteractive = @posix_isatty(fopen('php://stdin', 'r'));
501
        }
502
503
        if (!\function_exists('exec')) {
504
            return self::$stdinIsInteractive = true;
505
        }
506
507
        exec('stty 2> /dev/null', $output, $status);
508
509
        return self::$stdinIsInteractive = 1 !== $status;
510
    }
511
512
    /**
513
     * Reads one or more lines of input and returns what is read.
514
     *
515
     * @param resource $inputStream The handler resource
516
     * @param Question $question    The question being asked
517
     *
518
     * @return string|false The input received, false in case input could not be read
519
     */
520
    private function readInput($inputStream, Question $question)
521
    {
522
        if (!$question->isMultiline()) {
523
            $cp = $this->setIOCodepage();
524
            $ret = fgets($inputStream, 4096);
525
526
            return $this->resetIOCodepage($cp, $ret);
527
        }
528
529
        $multiLineStreamReader = $this->cloneInputStream($inputStream);
530
        if (null === $multiLineStreamReader) {
531
            return false;
532
        }
533
534
        $ret = '';
535
        $cp = $this->setIOCodepage();
536
        while (false !== ($char = fgetc($multiLineStreamReader))) {
537
            if (\PHP_EOL === "{$ret}{$char}") {
538
                break;
539
            }
540
            $ret .= $char;
541
        }
542
543
        return $this->resetIOCodepage($cp, $ret);
544
    }
545
546
    /**
547
     * Sets console I/O to the host code page.
548
     *
549
     * @return int Previous code page in IBM/EBCDIC format
550
     */
551
    private function setIOCodepage(): int
552
    {
553
        if (\function_exists('sapi_windows_cp_set')) {
554
            $cp = sapi_windows_cp_get();
0 ignored issues
show
Bug introduced by
The call to sapi_windows_cp_get() has too few arguments starting with kind. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

554
            $cp = /** @scrutinizer ignore-call */ sapi_windows_cp_get();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
555
            sapi_windows_cp_set(sapi_windows_cp_get('oem'));
556
557
            return $cp;
558
        }
559
560
        return 0;
561
    }
562
563
    /**
564
     * Sets console I/O to the specified code page and converts the user input.
565
     *
566
     * @param string|false $input
567
     *
568
     * @return string|false
569
     */
570
    private function resetIOCodepage(int $cp, $input)
571
    {
572
        if (0 !== $cp) {
573
            sapi_windows_cp_set($cp);
574
575
            if (false !== $input && '' !== $input) {
576
                $input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input);
577
            }
578
        }
579
580
        return $input;
581
    }
582
583
    /**
584
     * Clones an input stream in order to act on one instance of the same
585
     * stream without affecting the other instance.
586
     *
587
     * @param resource $inputStream The handler resource
588
     *
589
     * @return resource|null The cloned resource, null in case it could not be cloned
590
     */
591
    private function cloneInputStream($inputStream)
592
    {
593
        $streamMetaData = stream_get_meta_data($inputStream);
594
        $seekable = $streamMetaData['seekable'] ?? false;
595
        $mode = $streamMetaData['mode'] ?? 'rb';
596
        $uri = $streamMetaData['uri'] ?? null;
597
598
        if (null === $uri) {
599
            return null;
600
        }
601
602
        $cloneStream = fopen($uri, $mode);
603
604
        // For seekable and writable streams, add all the same data to the
605
        // cloned stream and then seek to the same offset.
606
        if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) {
607
            $offset = ftell($inputStream);
608
            rewind($inputStream);
609
            stream_copy_to_stream($inputStream, $cloneStream);
610
            fseek($inputStream, $offset);
611
            fseek($cloneStream, $offset);
612
        }
613
614
        return $cloneStream;
615
    }
616
}
617