This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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) { |
||
0 ignored issues
–
show
|
|||
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
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 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;
}
![]() |
|||
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 |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.