Issues (6)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/CompletionHandler.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Stecman\Component\Symfony\Console\BashCompletion;
4
5
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface;
6
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionInterface;
7
use Symfony\Component\Console\Application;
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Input\ArrayInput;
10
use Symfony\Component\Console\Input\InputArgument;
11
use Symfony\Component\Console\Input\InputOption;
12
13
class CompletionHandler
14
{
15
    /**
16
     * Application to complete for
17
     * @var \Symfony\Component\Console\Application
18
     */
19
    protected $application;
20
21
    /**
22
     * @var Command
23
     */
24
    protected $command;
25
26
    /**
27
     * @var CompletionContext
28
     */
29
    protected $context;
30
31
    /**
32
     * Array of completion helpers.
33
     * @var CompletionInterface[]
34
     */
35
    protected $helpers = array();
36
37
    /**
38
     * Index the command name was detected at
39
     * @var int
40
     */
41
    private $commandWordIndex;
42
43
    public function __construct(Application $application, CompletionContext $context = null)
44
    {
45
        $this->application = $application;
46
        $this->context = $context;
47
48
        // Set up completions for commands that are built-into Application
49
        $this->addHandler(
50
            new Completion(
51
                'help',
52
                'command_name',
53
                Completion::TYPE_ARGUMENT,
54
                $this->getCommandNames()
55
            )
56
        );
57
58
        $this->addHandler(
59
            new Completion(
60
                'list',
61
                'namespace',
62
                Completion::TYPE_ARGUMENT,
63
                $application->getNamespaces()
64
            )
65
        );
66
    }
67
68
    public function setContext(CompletionContext $context)
69
    {
70
        $this->context = $context;
71
    }
72
73
    /**
74
     * @return CompletionContext
75
     */
76
    public function getContext()
77
    {
78
        return $this->context;
79
    }
80
81
    /**
82
     * @param CompletionInterface[] $array
83
     */
84
    public function addHandlers(array $array)
85
    {
86
        $this->helpers = array_merge($this->helpers, $array);
87
    }
88
89
    /**
90
     * @param CompletionInterface $helper
91
     */
92
    public function addHandler(CompletionInterface $helper)
93
    {
94
        $this->helpers[] = $helper;
95
    }
96
97
    /**
98
     * Do the actual completion, returning an array of strings to provide to the parent shell's completion system
99
     *
100
     * @throws \RuntimeException
101
     * @return string[]
102
     */
103
    public function runCompletion()
104
    {
105
        if (!$this->context) {
106
            throw new \RuntimeException('A CompletionContext must be set before requesting completion.');
107
        }
108
109
        // Set the command to query options and arugments from
110
        $this->command = $this->detectCommand();
111
112
        $process = array(
113
            'completeForOptionValues',
114
            'completeForOptionShortcuts',
115
            'completeForOptionShortcutValues',
116
            'completeForOptions',
117
            'completeForCommandName',
118
            'completeForCommandArguments'
119
        );
120
121
        foreach ($process as $methodName) {
122
            $result = $this->{$methodName}();
123
124
            if (false !== $result) {
125
                // Return the result of the first completion mode that matches
126
                return $this->filterResults((array) $result);
127
            }
128
        }
129
130
        return array();
131
    }
132
133
    /**
134
     * Get an InputInterface representation of the completion context
135
     *
136
     * @deprecated Incorrectly uses the ArrayInput API and is no longer needed.
137
     *             This will be removed in the next major version.
138
     *
139
     * @return ArrayInput
140
     */
141
    public function getInput()
142
    {
143
        // Filter the command line content to suit ArrayInput
144
        $words = $this->context->getWords();
145
        array_shift($words);
146
        $words = array_filter($words);
147
148
        return new ArrayInput($words);
149
    }
150
151
    /**
152
     * Attempt to complete the current word as a long-form option (--my-option)
153
     *
154
     * @return array|false
155
     */
156
    protected function completeForOptions()
157
    {
158
        $word = $this->context->getCurrentWord();
159
160
        if (substr($word, 0, 2) === '--') {
161
            $options = array();
162
163
            foreach ($this->getAllOptions() as $opt) {
164
                $options[] = '--'.$opt->getName();
165
            }
166
167
            return $options;
168
        }
169
170
        return false;
171
    }
172
173
    /**
174
     * Attempt to complete the current word as an option shortcut.
175
     *
176
     * If the shortcut exists it will be completed, but a list of possible shortcuts is never returned for completion.
177
     *
178
     * @return array|false
179
     */
180
    protected function completeForOptionShortcuts()
181
    {
182
        $word = $this->context->getCurrentWord();
183
184
        if (strpos($word, '-') === 0 && strlen($word) == 2) {
185
            $definition = $this->command ? $this->command->getNativeDefinition() : $this->application->getDefinition();
186
187
            if ($definition->hasShortcut(substr($word, 1))) {
188
                return array($word);
189
            }
190
        }
191
192
        return false;
193
    }
194
195
    /**
196
     * Attempt to complete the current word as the value of an option shortcut
197
     *
198
     * @return array|false
199
     */
200 View Code Duplication
    protected function completeForOptionShortcutValues()
201
    {
202
        $wordIndex = $this->context->getWordIndex();
203
204
        if ($this->command && $wordIndex > 1) {
205
            $left = $this->context->getWordAtIndex($wordIndex - 1);
206
207
            // Complete short options
208
            if ($left[0] == '-' && strlen($left) == 2) {
209
                $shortcut = substr($left, 1);
210
                $def = $this->command->getNativeDefinition();
211
212
                if (!$def->hasShortcut($shortcut)) {
213
                    return false;
214
                }
215
216
                $opt = $def->getOptionForShortcut($shortcut);
217
                if ($opt->isValueRequired() || $opt->isValueOptional()) {
218
                    return $this->completeOption($opt);
219
                }
220
            }
221
        }
222
223
        return false;
224
    }
225
226
    /**
227
     * Attemp to complete the current word as the value of a long-form option
228
     *
229
     * @return array|false
230
     */
231 View Code Duplication
    protected function completeForOptionValues()
232
    {
233
        $wordIndex = $this->context->getWordIndex();
234
235
        if ($this->command && $wordIndex > 1) {
236
            $left = $this->context->getWordAtIndex($wordIndex - 1);
237
238
            if (strpos($left, '--') === 0) {
239
                $name = substr($left, 2);
240
                $def = $this->command->getNativeDefinition();
241
242
                if (!$def->hasOption($name)) {
243
                    return false;
244
                }
245
246
                $opt = $def->getOption($name);
247
                if ($opt->isValueRequired() || $opt->isValueOptional()) {
248
                    return $this->completeOption($opt);
249
                }
250
            }
251
        }
252
253
        return false;
254
    }
255
256
    /**
257
     * Attempt to complete the current word as a command name
258
     *
259
     * @return array|false
260
     */
261
    protected function completeForCommandName()
262
    {
263
        if (!$this->command || $this->context->getWordIndex() == $this->commandWordIndex) {
264
            return $this->getCommandNames();
265
        }
266
267
        return false;
268
    }
269
270
    /**
271
     * Attempt to complete the current word as a command argument value
272
     *
273
     * @see Symfony\Component\Console\Input\InputArgument
274
     * @return array|false
275
     */
276
    protected function completeForCommandArguments()
277
    {
278
        if (!$this->command || strpos($this->context->getCurrentWord(), '-') === 0) {
279
            return false;
280
        }
281
282
        $definition = $this->command->getNativeDefinition();
283
        $argWords = $this->mapArgumentsToWords($definition->getArguments());
284
        $wordIndex = $this->context->getWordIndex();
285
286
        if (isset($argWords[$wordIndex])) {
287
            $name = $argWords[$wordIndex];
288
        } elseif (!empty($argWords) && $definition->getArgument(end($argWords))->isArray()) {
289
            $name = end($argWords);
290
        } else {
291
            return false;
292
        }
293
294
        if ($helper = $this->getCompletionHelper($name, Completion::TYPE_ARGUMENT)) {
295
            return $helper->run();
296
        }
297
298
        if ($this->command instanceof CompletionAwareInterface) {
299
            return $this->command->completeArgumentValues($name, $this->context);
300
        }
301
302
        return false;
303
    }
304
305
    /**
306
     * Find a CompletionInterface that matches the current command, target name, and target type
307
     *
308
     * @param string $name
309
     * @param string $type
310
     * @return CompletionInterface|null
311
     */
312
    protected function getCompletionHelper($name, $type)
313
    {
314
        foreach ($this->helpers as $helper) {
315
            if ($helper->getType() != $type && $helper->getType() != CompletionInterface::ALL_TYPES) {
316
                continue;
317
            }
318
319
            if ($helper->getCommandName() == CompletionInterface::ALL_COMMANDS || $helper->getCommandName() == $this->command->getName()) {
320
                if ($helper->getTargetName() == $name) {
321
                    return $helper;
322
                }
323
            }
324
        }
325
326
        return null;
327
    }
328
329
    /**
330
     * Complete the value for the given option if a value completion is availble
331
     *
332
     * @param InputOption $option
333
     * @return array|false
334
     */
335
    protected function completeOption(InputOption $option)
336
    {
337
        if ($helper = $this->getCompletionHelper($option->getName(), Completion::TYPE_OPTION)) {
338
            return $helper->run();
339
        }
340
341
        if ($this->command instanceof CompletionAwareInterface) {
342
            return $this->command->completeOptionValues($option->getName(), $this->context);
343
        }
344
345
        return false;
346
    }
347
348
    /**
349
     * Step through the command line to determine which word positions represent which argument values
350
     *
351
     * The word indexes of argument values are found by eliminating words that are known to not be arguments (options,
352
     * option values, and command names). Any word that doesn't match for elimination is assumed to be an argument value,
353
     *
354
     * @param InputArgument[] $argumentDefinitions
355
     * @return array as [argument name => word index on command line]
356
     */
357
    protected function mapArgumentsToWords($argumentDefinitions)
358
    {
359
        $argumentPositions = array();
360
        $argumentNumber = 0;
361
        $previousWord = null;
362
        $argumentNames = array_keys($argumentDefinitions);
363
364
        // Build a list of option values to filter out
365
        $optionsWithArgs = $this->getOptionWordsWithValues();
366
367
        foreach ($this->context->getWords() as $wordIndex => $word) {
368
            // Skip program name, command name, options, and option values
369
            if ($wordIndex == 0
370
                || $wordIndex === $this->commandWordIndex
371
                || ($word && '-' === $word[0])
372
                || in_array($previousWord, $optionsWithArgs)) {
373
                $previousWord = $word;
374
                continue;
375
            } else {
376
                $previousWord = $word;
377
            }
378
379
            // If argument n exists, pair that argument's name with the current word
380
            if (isset($argumentNames[$argumentNumber])) {
381
                $argumentPositions[$wordIndex] = $argumentNames[$argumentNumber];
382
            }
383
384
            $argumentNumber++;
385
        }
386
387
        return $argumentPositions;
388
    }
389
390
    /**
391
     * Build a list of option words/flags that will have a value after them
392
     * Options are returned in the format they appear as on the command line.
393
     *
394
     * @return string[] - eg. ['--myoption', '-m', ... ]
395
     */
396
    protected function getOptionWordsWithValues()
397
    {
398
        $strings = array();
399
400
        foreach ($this->getAllOptions() as $option) {
401
            if ($option->isValueRequired()) {
402
                $strings[] = '--' . $option->getName();
403
404
                if ($option->getShortcut()) {
405
                    $strings[] = '-' . $option->getShortcut();
406
                }
407
            }
408
        }
409
410
        return $strings;
411
    }
412
413
    /**
414
     * Filter out results that don't match the current word on the command line
415
     *
416
     * @param string[] $array
417
     * @return string[]
418
     */
419
    protected function filterResults(array $array)
420
    {
421
        $curWord = $this->context->getCurrentWord();
422
423
        return array_filter($array, function($val) use ($curWord) {
424
            return fnmatch($curWord.'*', $val);
425
        });
426
    }
427
428
    /**
429
     * Get the combined options of the application and entered command
430
     *
431
     * @return InputOption[]
432
     */
433
    protected function getAllOptions()
434
    {
435
        if (!$this->command) {
436
            return $this->application->getDefinition()->getOptions();
437
        }
438
439
        return array_merge(
440
            $this->command->getNativeDefinition()->getOptions(),
441
            $this->application->getDefinition()->getOptions()
442
        );
443
    }
444
445
    /**
446
     * Get command names available for completion
447
     *
448
     * Filters out hidden commands where supported.
449
     *
450
     * @return string[]
451
     */
452
    protected function getCommandNames()
453
    {
454
        // Command::Hidden isn't supported before Symfony Console 3.2.0
455
        // We don't complete hidden command names as these are intended to be private
456
        if (method_exists('\Symfony\Component\Console\Command\Command', 'isHidden')) {
457
            $commands = array();
458
459
            foreach ($this->application->all() as $name => $command) {
460
                if (!$command->isHidden()) {
461
                    $commands[] = $name;
462
                }
463
            }
464
465
            return $commands;
466
467
        } else {
468
469
            // Fallback for compatibility with Symfony Console < 3.2.0
470
            // This was the behaviour prior to pull #75
471
            $commands = $this->application->all();
472
            unset($commands['_completion']);
473
474
            return array_keys($commands);
475
        }
476
    }
477
478
    /**
479
     * Find the current command name in the command-line
480
     *
481
     * Note this only cares about flag-type options. Options with values cannot
482
     * appear before a command name in Symfony Console application.
483
     *
484
     * @return Command|null
485
     */
486
    private function detectCommand()
487
    {
488
        // Always skip the first word (program name)
489
        $skipNext = true;
490
491
        foreach ($this->context->getWords() as $index => $word) {
492
493
            // Skip word if flagged
494
            if ($skipNext) {
495
                $skipNext = false;
496
                continue;
497
            }
498
499
            // Skip empty words and words that look like options
500
            if (strlen($word) == 0 || $word[0] === '-') {
501
                continue;
502
            }
503
504
            // Return the first unambiguous match to argument-like words
505
            try {
506
                $cmd = $this->application->find($word);
507
                $this->commandWordIndex = $index;
0 ignored issues
show
Documentation Bug introduced by
It seems like $index can also be of type string. However, the property $commandWordIndex is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
508
                return $cmd;
509
            } catch (\InvalidArgumentException $e) {
510
                // Exception thrown, when multiple or no commands are found.
511
            }
512
        }
513
514
        // No command found
515
        return null;
516
    }
517
}
518