Skip to content

Commit 975b49a

Browse files
authored
fix(console): prevent unknown console arguments (#1238)
1 parent 0e78895 commit 975b49a

File tree

10 files changed

+132
-7
lines changed

10 files changed

+132
-7
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Tempest\Console\Exceptions;
4+
5+
use Tempest\Console\Console;
6+
use Tempest\Console\ConsoleCommand;
7+
use Tempest\Console\Input\ConsoleInputArgument;
8+
use Tempest\Support\Arr\ImmutableArray;
9+
10+
final class UnknownArgumentsException extends ConsoleException
11+
{
12+
public function __construct(
13+
private readonly ConsoleCommand $consoleCommand,
14+
/** @var \Tempest\Console\Input\ConsoleInputArgument[] $invalidArguments */
15+
private readonly ImmutableArray $invalidArguments,
16+
) {}
17+
18+
public function render(Console $console): void
19+
{
20+
$console->error(sprintf(
21+
'Unknown arguments: %s',
22+
$this->invalidArguments
23+
->map(fn (ConsoleInputArgument $argument) => sprintf(
24+
'<code>%s</code>',
25+
$argument->name,
26+
))
27+
->implode(', '),
28+
));
29+
}
30+
}

packages/console/src/GenericConsole.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ public function supportsPrompting(): bool
310310
return false;
311311
}
312312

313-
if ($this->argumentBag->get('interaction')?->value === false) {
313+
if ($this->argumentBag->get(GlobalFlags::INTERACTION->value)?->value === false) {
314314
return false;
315315
}
316316

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Tempest\Console;
4+
5+
use Tempest\Support\IsEnumHelper;
6+
7+
enum GlobalFlags: string
8+
{
9+
use IsEnumHelper;
10+
11+
case FORCE = 'force';
12+
case FORCE_SHORTHAND = '-f';
13+
case HELP = 'help';
14+
case HELP_SHORTHAND = '-h';
15+
case INTERACTION = 'interaction';
16+
}

packages/console/src/Input/ConsoleArgumentBag.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
final class ConsoleArgumentBag
1212
{
1313
/** @var ConsoleInputArgument[] */
14-
private array $arguments = [];
14+
private(set) array $arguments = [];
1515

1616
/** @var string[] */
1717
private array $path = [];
@@ -152,14 +152,16 @@ public function addMany(array $arguments): self
152152
}
153153

154154
// Otherwise, $arguments is an array of flags or positional argument.
155-
foreach ($arguments as $argument) {
155+
foreach ($arguments as $key => $argument) {
156156
if (str_starts_with($argument, '-') && ! str_starts_with($argument, '--')) {
157157
$flags = str_split($argument);
158158
unset($flags[0]);
159159

160160
foreach ($flags as $flag) {
161161
$arguments[] = "-{$flag}";
162162
}
163+
164+
unset($arguments[$key]);
163165
}
164166
}
165167

packages/console/src/Middleware/ForceMiddleware.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Tempest\Console\ConsoleMiddlewareCallable;
1010
use Tempest\Console\ExitCode;
1111
use Tempest\Console\GenericConsole;
12+
use Tempest\Console\GlobalFlags;
1213
use Tempest\Console\Initializers\Invocation;
1314
use Tempest\Discovery\SkipDiscovery;
1415

@@ -21,7 +22,7 @@ public function __construct(
2122

2223
public function __invoke(Invocation $invocation, ConsoleMiddlewareCallable $next): ExitCode|int
2324
{
24-
if ($invocation->argumentBag->get('-f') || $invocation->argumentBag->get('force')) {
25+
if ($invocation->argumentBag->get(GlobalFlags::FORCE_SHORTHAND->value) || $invocation->argumentBag->get(GlobalFlags::FORCE->value)) {
2526
if ($this->console instanceof GenericConsole) {
2627
$this->console->setForced();
2728
}

packages/console/src/Middleware/HelpMiddleware.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Tempest\Console\ConsoleMiddleware;
1111
use Tempest\Console\ConsoleMiddlewareCallable;
1212
use Tempest\Console\ExitCode;
13+
use Tempest\Console\GlobalFlags;
1314
use Tempest\Console\Initializers\Invocation;
1415
use Tempest\Core\Priority;
1516

@@ -22,7 +23,7 @@ public function __construct(
2223

2324
public function __invoke(Invocation $invocation, ConsoleMiddlewareCallable $next): ExitCode|int
2425
{
25-
if ($invocation->argumentBag->get('-h') || $invocation->argumentBag->get('help')) {
26+
if ($invocation->argumentBag->get(GlobalFlags::HELP_SHORTHAND->value) || $invocation->argumentBag->get(GlobalFlags::HELP->value)) {
2627
$this->renderHelp($invocation->consoleCommand);
2728

2829
return ExitCode::SUCCESS;

packages/console/src/Middleware/InvalidCommandMiddleware.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ private function retry(Invocation $invocation, InvalidCommandException $exceptio
6161
} else {
6262
$value = $this->console->ask(
6363
question: $name,
64-
default: $argument->default,
65-
hint: $argument->help ?: $argument->description,
6664
options: match (true) {
6765
$isEnum => $argument->type::cases(),
6866
default => null,
6967
},
68+
default: $argument->default,
69+
hint: $argument->help ?: $argument->description,
7070
validation: array_filter([
7171
$isEnum
7272
? new IsEnum($argument->type)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Tempest\Console\Middleware;
4+
5+
use Tempest\Console\ConsoleMiddleware;
6+
use Tempest\Console\ConsoleMiddlewareCallable;
7+
use Tempest\Console\Exceptions\UnknownArgumentsException;
8+
use Tempest\Console\ExitCode;
9+
use Tempest\Console\GlobalFlags;
10+
use Tempest\Console\Initializers\Invocation;
11+
use Tempest\Console\Input\ConsoleArgumentDefinition;
12+
use Tempest\Console\Input\ConsoleInputArgument;
13+
use Tempest\Core\Priority;
14+
15+
use function Tempest\Support\arr;
16+
17+
#[Priority(Priority::FRAMEWORK - 6)]
18+
final class ValidateNamedArgumentsMiddleware implements ConsoleMiddleware
19+
{
20+
public function __invoke(Invocation $invocation, ConsoleMiddlewareCallable $next): ExitCode|int
21+
{
22+
$allowedParameterNames = arr($invocation->consoleCommand->getArgumentDefinitions())
23+
->flatMap(function (ConsoleArgumentDefinition $definition) {
24+
return [$definition->name, ...$definition->aliases];
25+
})
26+
->map(function (string $name) {
27+
return ltrim($name, '-');
28+
});
29+
30+
$invalidInput = arr($invocation->argumentBag->arguments)
31+
->filter(fn (ConsoleInputArgument $argument) => $argument->name !== null)
32+
->filter(fn (ConsoleInputArgument $argument) => ! $allowedParameterNames->contains(ltrim($argument->name, '-')))
33+
->filter(fn (ConsoleInputArgument $argument) => ! in_array($argument->name, GlobalFlags::values(), strict: true));
34+
35+
if ($invalidInput->isNotEmpty()) {
36+
throw new UnknownArgumentsException(
37+
consoleCommand: $invocation->consoleCommand,
38+
invalidArguments: $invalidInput,
39+
);
40+
}
41+
42+
return $next($invocation);
43+
}
44+
}

tests/Integration/Console/Fixtures/TestCommand.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,14 @@ public function test(): void
3434
{
3535
$this->console->confirm('yes or no?');
3636
}
37+
38+
#[ConsoleCommand]
39+
public function flags(bool $flag = false, bool $foo = false): void
40+
{
41+
if ($flag && $foo) {
42+
$this->console->writeln('ok');
43+
}
44+
45+
$this->console->confirm('yes or no?');
46+
}
3747
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Integration\Console\Middleware;
4+
5+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
6+
7+
final class ValidateNamedArgumentsMiddlewareTest extends FrameworkIntegrationTestCase
8+
{
9+
public function test_invalid_parameters_throw_exception(): void
10+
{
11+
$this->console
12+
->call('test:flags --unknown --foo --no-flag --help --force --no-interaction')
13+
->assertError()
14+
->assertContains('unknown')
15+
->assertDoesNotContain('foo')
16+
->assertDoesNotContain('flag')
17+
->assertDoesNotContain('force')
18+
->assertDoesNotContain('help')
19+
->assertDoesNotContain('interaction');
20+
}
21+
}

0 commit comments

Comments
 (0)