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;
}
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 |
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.