Completed
Pull Request — master (#18)
by Paulo Rodrigues
02:34
created

QuestionHelper::getHiddenResponse()   C

Complexity

Conditions 8
Paths 9

Size

Total Lines 50
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 50
rs 6.3636
cc 8
eloc 29
nc 9
nop 2
1
<?php
2
3
/**
4
 * This file was "backported" from symfony/console for compatibility with
5
 * Symfony versions < 2.5.
6
 *
7
 * You can view the license at https://github.com/symfony/symfony/blob/master/LICENSE
8
 */
9
10
/*
11
 * This file is part of the Symfony package.
12
 *
13
 * (c) Fabien Potencier <[email protected]>
14
 *
15
 * For the full copyright and license information, please view the LICENSE
16
 * file that was distributed with this source code.
17
 */
18
19
namespace Rj\FrontendBundle\Command\Options\Legacy;
20
21
use Symfony\Component\Console\Helper\Helper;
22
use Symfony\Component\Console\Exception\InvalidArgumentException;
23
use Symfony\Component\Console\Exception\RuntimeException;
24
use Symfony\Component\Console\Input\InputInterface;
25
use Symfony\Component\Console\Output\ConsoleOutputInterface;
26
use Symfony\Component\Console\Output\OutputInterface;
27
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
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
    private $inputStream;
37
    private static $shell;
38
    private static $stty;
39
40
    /**
41
     * Asks a question to the user.
42
     *
43
     * @param InputInterface  $input    An InputInterface instance
44
     * @param OutputInterface $output   An OutputInterface instance
45
     * @param Question        $question The question to ask
46
     *
47
     * @return string 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 $question->getDefault();
59
        }
60
61
        if (!$question->getValidator()) {
62
            return $this->doAsk($output, $question);
63
        }
64
65
        $that = $this;
66
67
        $interviewer = function () use ($output, $question, $that) {
68
            return $that->doAsk($output, $question);
69
        };
70
71
        return $this->validateAttempts($interviewer, $output, $question);
72
    }
73
74
    /**
75
     * Sets the input stream to read from when interacting with the user.
76
     *
77
     * This is mainly useful for testing purpose.
78
     *
79
     * @param resource $stream The input stream
80
     *
81
     * @throws InvalidArgumentException In case the stream is not a resource
82
     */
83
    public function setInputStream($stream)
84
    {
85
        if (!is_resource($stream)) {
86
            throw new InvalidArgumentException('Input stream must be a valid resource.');
87
        }
88
89
        $this->inputStream = $stream;
90
    }
91
92
    /**
93
     * Returns the helper's input stream.
94
     *
95
     * @return resource
96
     */
97
    public function getInputStream()
98
    {
99
        return $this->inputStream;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function getName()
106
    {
107
        return 'question';
108
    }
109
110
    /**
111
     * Asks the question to the user.
112
     *
113
     * This method is public for PHP 5.3 compatibility, it should be private.
114
     *
115
     * @param OutputInterface $output
116
     * @param Question        $question
117
     *
118
     * @return bool|mixed|null|string
119
     *
120
     * @throws \Exception
121
     * @throws \RuntimeException
122
     */
123
    public function doAsk(OutputInterface $output, Question $question)
124
    {
125
        $this->writePrompt($output, $question);
126
127
        $inputStream = $this->inputStream ?: STDIN;
128
        $autocomplete = $question->getAutocompleterValues();
129
130
        if (null === $autocomplete || !$this->hasSttyAvailable()) {
131
            $ret = false;
132
            if ($question->isHidden()) {
133
                try {
134
                    $ret = trim($this->getHiddenResponse($output, $inputStream));
135
                } catch (\RuntimeException $e) {
136
                    if (!$question->isHiddenFallback()) {
137
                        throw $e;
138
                    }
139
                }
140
            }
141
142
            if (false === $ret) {
143
                $ret = $this->readFromInput($inputStream);
0 ignored issues
show
Bug introduced by
It seems like $inputStream defined by $this->inputStream ?: STDIN on line 127 can also be of type string; however, Rj\FrontendBundle\Comman...Helper::readFromInput() does only seem to accept resource, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
144
            }
145
        } else {
146
            $ret = trim($this->autocomplete($output, $question, $inputStream));
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
     * Outputs the question prompt.
160
     *
161
     * @param OutputInterface $output
162
     * @param Question        $question
163
     */
164
    protected function writePrompt(OutputInterface $output, Question $question)
165
    {
166
        $message = $question->getQuestion();
167
168
        if ($question instanceof ChoiceQuestion) {
169
            $width = max(array_map('strlen', array_keys($question->getChoices())));
170
171
            $messages = (array) $question->getQuestion();
172
            foreach ($question->getChoices() as $key => $value) {
173
                $messages[] = sprintf("  [<info>%-${width}s</info>] %s", $key, $value);
174
            }
175
176
            $output->writeln($messages);
177
178
            $message = $question->getPrompt();
179
        }
180
181
        $output->write($message);
182
    }
183
184
    /**
185
     * Outputs an error message.
186
     *
187
     * @param OutputInterface $output
188
     * @param \Exception      $error
189
     */
190
    protected function writeError(OutputInterface $output, \Exception $error)
191
    {
192
        if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
193
            $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
194
        } else {
195
            $message = '<error>'.$error->getMessage().'</error>';
196
        }
197
198
        $output->writeln($message);
199
    }
200
201
    /**
202
     * Autocompletes a question.
203
     *
204
     * @param OutputInterface $output
205
     * @param Question        $question
206
     *
207
     * @return string
208
     */
209
    private function autocomplete(OutputInterface $output, Question $question, $inputStream)
210
    {
211
        $autocomplete = $question->getAutocompleterValues();
212
        $ret = '';
213
214
        $i = 0;
215
        $ofs = -1;
216
        $matches = $autocomplete;
217
        $numMatches = count($matches);
218
219
        $sttyMode = shell_exec('stty -g');
220
221
        // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
222
        shell_exec('stty -icanon -echo');
223
224
        // Add highlighted text style
225
        $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white'));
226
227
        // Read a keypress
228
        while (!feof($inputStream)) {
229
            $c = fread($inputStream, 1);
230
231
            // Backspace Character
232
            if ("\177" === $c) {
233
                if (0 === $numMatches && 0 !== $i) {
234
                    --$i;
235
                    // Move cursor backwards
236
                    $output->write("\033[1D");
237
                }
238
239
                if ($i === 0) {
240
                    $ofs = -1;
241
                    $matches = $autocomplete;
242
                    $numMatches = count($matches);
243
                } else {
244
                    $numMatches = 0;
245
                }
246
247
                // Pop the last character off the end of our string
248
                $ret = substr($ret, 0, $i);
249
            } elseif ("\033" === $c) {
250
                // Did we read an escape sequence?
251
                $c .= fread($inputStream, 2);
252
253
                // A = Up Arrow. B = Down Arrow
254
                if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) {
255
                    if ('A' === $c[2] && -1 === $ofs) {
256
                        $ofs = 0;
257
                    }
258
259
                    if (0 === $numMatches) {
260
                        continue;
261
                    }
262
263
                    $ofs += ('A' === $c[2]) ? -1 : 1;
264
                    $ofs = ($numMatches + $ofs) % $numMatches;
265
                }
266
            } elseif (ord($c) < 32) {
267
                if ("\t" === $c || "\n" === $c) {
268
                    if ($numMatches > 0 && -1 !== $ofs) {
269
                        $ret = $matches[$ofs];
270
                        // Echo out remaining chars for current match
271
                        $output->write(substr($ret, $i));
272
                        $i = strlen($ret);
273
                    }
274
275
                    if ("\n" === $c) {
276
                        $output->write($c);
277
                        break;
278
                    }
279
280
                    $numMatches = 0;
281
                }
282
283
                continue;
284
            } else {
285
                $output->write($c);
286
                $ret .= $c;
287
                ++$i;
288
289
                $numMatches = 0;
290
                $ofs = 0;
291
292
                foreach ($autocomplete as $value) {
293
                    // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
294
                    if (0 === strpos($value, $ret) && $i !== strlen($value)) {
295
                        $matches[$numMatches++] = $value;
296
                    }
297
                }
298
            }
299
300
            // Erase characters from cursor to end of line
301
            $output->write("\033[K");
302
303
            if ($numMatches > 0 && -1 !== $ofs) {
304
                // Save cursor position
305
                $output->write("\0337");
306
                // Write highlighted text
307
                $output->write('<hl>'.substr($matches[$ofs], $i).'</hl>');
308
                // Restore cursor position
309
                $output->write("\0338");
310
            }
311
        }
312
313
        // Reset stty so it behaves normally again
314
        shell_exec(sprintf('stty %s', $sttyMode));
315
316
        return $ret;
317
    }
318
319
    /**
320
     * Gets a hidden response from user.
321
     *
322
     * @param OutputInterface $output An Output instance
323
     *
324
     * @return string The answer
325
     *
326
     * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
327
     */
328
    private function getHiddenResponse(OutputInterface $output, $inputStream)
329
    {
330
        if ('\\' === DIRECTORY_SEPARATOR) {
331
            $exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
332
333
            // handle code running from a phar
334
            if ('phar:' === substr(__FILE__, 0, 5)) {
335
                $tmpExe = sys_get_temp_dir().'/hiddeninput.exe';
336
                copy($exe, $tmpExe);
337
                $exe = $tmpExe;
338
            }
339
340
            $value = rtrim(shell_exec($exe));
341
            $output->writeln('');
342
343
            if (isset($tmpExe)) {
344
                unlink($tmpExe);
345
            }
346
347
            return $value;
348
        }
349
350
        if ($this->hasSttyAvailable()) {
351
            $sttyMode = shell_exec('stty -g');
352
353
            shell_exec('stty -echo');
354
            $value = fgets($inputStream, 4096);
355
            shell_exec(sprintf('stty %s', $sttyMode));
356
357
            if (false === $value) {
358
                throw new RuntimeException('Aborted');
359
            }
360
361
            $value = trim($value);
362
            $output->writeln('');
363
364
            return $value;
365
        }
366
367
        if (false !== $shell = $this->getShell()) {
368
            $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword';
369
            $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
370
            $value = rtrim(shell_exec($command));
371
            $output->writeln('');
372
373
            return $value;
374
        }
375
376
        throw new RuntimeException('Unable to hide the response.');
377
    }
378
379
    /**
380
     * Validates an attempt.
381
     *
382
     * @param callable        $interviewer A callable that will ask for a question and return the result
383
     * @param OutputInterface $output      An Output instance
384
     * @param Question        $question    A Question instance
385
     *
386
     * @return string The validated response
387
     *
388
     * @throws \Exception In case the max number of attempts has been reached and no valid response has been given
389
     */
390
    private function validateAttempts($interviewer, OutputInterface $output, Question $question)
391
    {
392
        $error = null;
393
        $attempts = $question->getMaxAttempts();
394
        while (null === $attempts || $attempts--) {
395
            if (null !== $error) {
396
                $this->writeError($output, $error);
397
            }
398
399
            try {
400
                return call_user_func($question->getValidator(), $interviewer());
401
            } catch (\Exception $error) {
402
            }
403
        }
404
405
        throw $error;
406
    }
407
408
    /**
409
     * Returns a valid unix shell.
410
     *
411
     * @return string|bool The valid shell name, false in case no valid shell is found
412
     */
413
    private function getShell()
414
    {
415
        if (null !== self::$shell) {
416
            return self::$shell;
417
        }
418
419
        self::$shell = false;
420
421
        if (file_exists('/usr/bin/env')) {
422
            // handle other OSs with bash/zsh/ksh/csh if available to hide the answer
423
            $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
424
            foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) {
425
                if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
426
                    self::$shell = $sh;
427
                    break;
428
                }
429
            }
430
        }
431
432
        return self::$shell;
433
    }
434
435
    /**
436
     * Reads user input.
437
     *
438
     * @param resource $stream The input stream
439
     *
440
     * @return string User input
441
     *
442
     * @throws RuntimeException
443
     */
444
    private function readFromInput($stream)
445
    {
446
        if (STDIN === $stream && function_exists('readline')) {
447
            $ret = readline();
448
        } else {
449
            $ret = fgets($stream, 4096);
450
        }
451
452
        if (false === $ret) {
453
            throw new RuntimeException('Aborted');
454
        }
455
456
        return trim($ret);
457
    }
458
459
    /**
460
     * Returns whether Stty is available or not.
461
     *
462
     * @return bool
463
     */
464
    private function hasSttyAvailable()
465
    {
466
        if (null !== self::$stty) {
467
            return self::$stty;
468
        }
469
470
        exec('stty 2>&1', $output, $exitcode);
471
472
        return self::$stty = $exitcode === 0;
473
    }
474
}
475