feature #33412 [Console] Do not leak hidden console commands (m-vo)
This PR was merged into the 4.4 branch. Discussion ---------- [Console] Do not leak hidden console commands | Q | A | ------------- | --- | Branch? | 4.4 (updated) | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #33398 | License | MIT This PR attempts to fix hidden console commands to be leaked when interacting with the console. These are the changes: * Hidden commands won't be shown anymore in the list of commands in a namespace as well as the list of suggestions ("Did you mean...") for invalid or ambiguous commands. * Hidden commands therefore now need to be always entered with their full name. * If an abbreviated command is entered that was previously ambiguous with (only) hidden commands, it's now executed directly (not ambiguous anymore). Side note: When implementing the tests & changes I realized that `Application->get()` isn't side effect free (when redirecting to the help command) and behaves differently when called multiple times. It therefore must not be used from inside `find()`. Maybe we should change this? Here are the relevant bits:f71f74b36a/src/Symfony/Component/Console/Application.php (L495-L502)
Commits -------f3406338e6
[Console] Deprecate abbreviating hidden command names using Application->find()
This commit is contained in:
commit
e627989089
@ -6,6 +6,11 @@ Cache
|
|||||||
|
|
||||||
* Added argument `$prefix` to `AdapterInterface::clear()`
|
* Added argument `$prefix` to `AdapterInterface::clear()`
|
||||||
|
|
||||||
|
Console
|
||||||
|
-------
|
||||||
|
|
||||||
|
* Deprecated finding hidden commands using an abbreviation, use the full name instead
|
||||||
|
|
||||||
Debug
|
Debug
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ Config
|
|||||||
Console
|
Console
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
* Removed support for finding hidden commands using an abbreviation, use the full name instead
|
||||||
* Removed the `setCrossingChar()` method in favor of the `setDefaultCrossingChar()` method in `TableStyle`.
|
* Removed the `setCrossingChar()` method in favor of the `setDefaultCrossingChar()` method in `TableStyle`.
|
||||||
* Removed the `setHorizontalBorderChar()` method in favor of the `setDefaultCrossingChars()` method in `TableStyle`.
|
* Removed the `setHorizontalBorderChar()` method in favor of the `setDefaultCrossingChars()` method in `TableStyle`.
|
||||||
* Removed the `getHorizontalBorderChar()` method in favor of the `getBorderChars()` method in `TableStyle`.
|
* Removed the `getHorizontalBorderChar()` method in favor of the `getBorderChars()` method in `TableStyle`.
|
||||||
|
@ -692,12 +692,14 @@ class Application implements ResetInterface
|
|||||||
foreach ($abbrevs as $abbrev) {
|
foreach ($abbrevs as $abbrev) {
|
||||||
$maxLen = max(Helper::strlen($abbrev), $maxLen);
|
$maxLen = max(Helper::strlen($abbrev), $maxLen);
|
||||||
}
|
}
|
||||||
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) {
|
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
|
||||||
if (!$commandList[$cmd] instanceof Command) {
|
if (!$commandList[$cmd] instanceof Command) {
|
||||||
$commandList[$cmd] = $this->commandLoader->get($cmd);
|
$commandList[$cmd] = $this->commandLoader->get($cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($commandList[$cmd]->isHidden()) {
|
if ($commandList[$cmd]->isHidden()) {
|
||||||
|
unset($commands[array_search($cmd, $commands)]);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,12 +707,21 @@ class Application implements ResetInterface
|
|||||||
|
|
||||||
return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
|
return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
|
||||||
}, array_values($commands));
|
}, array_values($commands));
|
||||||
|
|
||||||
|
if (\count($commands) > 1) {
|
||||||
$suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
|
$suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
|
||||||
|
|
||||||
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands));
|
throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s", $name, $suggestions), array_values($commands));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $this->get(reset($commands));
|
$command = $this->get(reset($commands));
|
||||||
|
|
||||||
|
if ($command->isHidden()) {
|
||||||
|
@trigger_error(sprintf('Command "%s" is hidden, finding it using an abbreviation is deprecated since Symfony 4.4, use its full name instead.', $command->getName()), E_USER_DEPRECATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $command;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,6 +4,7 @@ CHANGELOG
|
|||||||
4.4.0
|
4.4.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
* deprecated finding hidden commands using an abbreviation, use the full name instead
|
||||||
* added `Question::setTrimmable` default to true to allow the answer to be trimmed
|
* added `Question::setTrimmable` default to true to allow the answer to be trimmed
|
||||||
* added method `preventRedrawFasterThan()` and `forceRedrawSlowerThan()` on `ProgressBar`
|
* added method `preventRedrawFasterThan()` and `forceRedrawSlowerThan()` on `ProgressBar`
|
||||||
* `Application` implements `ResetInterface`
|
* `Application` implements `ResetInterface`
|
||||||
|
@ -76,6 +76,7 @@ class ApplicationTest extends TestCase
|
|||||||
require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering.php';
|
require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering.php';
|
||||||
require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering2.php';
|
require_once self::$fixturesPath.'/TestAmbiguousCommandRegistering2.php';
|
||||||
require_once self::$fixturesPath.'/FooHiddenCommand.php';
|
require_once self::$fixturesPath.'/FooHiddenCommand.php';
|
||||||
|
require_once self::$fixturesPath.'/BarHiddenCommand.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function normalizeLineBreaks($text)
|
protected function normalizeLineBreaks($text)
|
||||||
@ -441,6 +442,16 @@ class ApplicationTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFindWithAmbiguousAbbreviationsFindsCommandIfAlternativesAreHidden()
|
||||||
|
{
|
||||||
|
$application = new Application();
|
||||||
|
|
||||||
|
$application->add(new \FooCommand());
|
||||||
|
$application->add(new \FooHiddenCommand());
|
||||||
|
|
||||||
|
$this->assertInstanceOf('FooCommand', $application->find('foo:'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testFindCommandEqualNamespace()
|
public function testFindCommandEqualNamespace()
|
||||||
{
|
{
|
||||||
$application = new Application();
|
$application = new Application();
|
||||||
@ -708,6 +719,49 @@ class ApplicationTest extends TestCase
|
|||||||
$application->find('foo::bar');
|
$application->find('foo::bar');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFindHiddenWithExactName()
|
||||||
|
{
|
||||||
|
$application = new Application();
|
||||||
|
$application->add(new \FooHiddenCommand());
|
||||||
|
|
||||||
|
$this->assertInstanceOf('FooHiddenCommand', $application->find('foo:hidden'));
|
||||||
|
$this->assertInstanceOf('FooHiddenCommand', $application->find('afoohidden'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group legacy
|
||||||
|
* @expectedDeprecation Command "%s:hidden" is hidden, finding it using an abbreviation is deprecated since Symfony 4.4, use its full name instead.
|
||||||
|
* @dataProvider provideAbbreviationsForHiddenCommands
|
||||||
|
*/
|
||||||
|
public function testFindHiddenWithAbbreviatedName($name)
|
||||||
|
{
|
||||||
|
$application = new Application();
|
||||||
|
|
||||||
|
$application->add(new \FooHiddenCommand());
|
||||||
|
$application->add(new \BarHiddenCommand());
|
||||||
|
|
||||||
|
$application->find($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideAbbreviationsForHiddenCommands()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['foo:hidde'],
|
||||||
|
['afoohidd'],
|
||||||
|
['bar:hidde'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindAmbiguousCommandsIfAllAlternativesAreHidden()
|
||||||
|
{
|
||||||
|
$application = new Application();
|
||||||
|
|
||||||
|
$application->add(new \FooCommand());
|
||||||
|
$application->add(new \FooHiddenCommand());
|
||||||
|
|
||||||
|
$this->assertInstanceOf('FooCommand', $application->find('foo:'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testSetCatchExceptions()
|
public function testSetCatchExceptions()
|
||||||
{
|
{
|
||||||
$application = new Application();
|
$application = new Application();
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
class BarHiddenCommand extends Command
|
||||||
|
{
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName('bar:hidden')
|
||||||
|
->setAliases(['abarhidden'])
|
||||||
|
->setHidden(true)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user