Add TrimMiddleware to trim all request values
This commit is contained in:
parent
8a3c2eaec5
commit
9feed46d4e
|
@ -50,6 +50,7 @@ return [
|
|||
\Engelsystem\Middleware\SetLocale::class,
|
||||
\Engelsystem\Middleware\ETagHandler::class,
|
||||
\Engelsystem\Middleware\AddHeaders::class,
|
||||
\Engelsystem\Middleware\TrimInput::class,
|
||||
|
||||
// The application code
|
||||
\Engelsystem\Middleware\ErrorHandler::class,
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Engelsystem\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* Request middleware that trims all string values of PUT and POST requests.
|
||||
* Some fields such as passwords are excluded.
|
||||
*/
|
||||
class TrimInput implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string> List of field names to exclude from trim
|
||||
*/
|
||||
private const TRIM_EXCLUDE_LIST = [
|
||||
'password',
|
||||
'password2',
|
||||
'new_password',
|
||||
'new_password2',
|
||||
'new_pw',
|
||||
'new_pw2',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
if (in_array($request->getMethod(), ['PUT', 'POST']) && is_array($request->getParsedBody())) {
|
||||
$trimmedParsedBody = $this->trimArrayValues($request->getParsedBody());
|
||||
$request = $request->withParsedBody($trimmedParsedBody);
|
||||
}
|
||||
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template AK array key type
|
||||
* @template AV array value type
|
||||
* @param array<AK, AV> $array
|
||||
* @return array<AK, AV>
|
||||
*/
|
||||
private function trimArrayValues(array $array): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($array as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
// recurse trim
|
||||
$result[$key] = $this->trimArrayValues($value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($value) && !in_array($key, self::TRIM_EXCLUDE_LIST)) {
|
||||
// trim only non-excluded string values
|
||||
$result[$key] = trim($value);
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[$key] = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Engelsystem\Test\Unit\Middleware;
|
||||
|
||||
use Engelsystem\Http\Request;
|
||||
use Engelsystem\Middleware\TrimInput;
|
||||
use Engelsystem\Test\Unit\TestCase;
|
||||
use Generator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class TrimInputTest extends TestCase
|
||||
{
|
||||
private TrimInput $subject;
|
||||
|
||||
/**
|
||||
* @var MockObject<RequestHandlerInterface>
|
||||
*/
|
||||
private MockObject $handler;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->subject = new TrimInput();
|
||||
$this->handler = $this->getMockForAbstractClass(RequestHandlerInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Generator<string, array>
|
||||
*/
|
||||
public function provideTrimTestData(): Generator
|
||||
{
|
||||
yield 'GET request' => ['GET', [], []];
|
||||
|
||||
foreach (['POST', 'PUT'] as $method) {
|
||||
yield $method . ' request with empty data' => [$method, [], []];
|
||||
yield $method . ' request with mixed data' => [
|
||||
$method,
|
||||
[
|
||||
'fieldA' => 23,
|
||||
'fieldB' => ' bla ',
|
||||
'password' => ' pass1 ',
|
||||
'password2' => ' pass2 ',
|
||||
'new_password' => ' new_password ',
|
||||
'new_password2' => ' new_password2 ',
|
||||
'new_pw' => ' new_pw ',
|
||||
'new_pw2' => ' new_pw2 ',
|
||||
'password_confirmation' => ' password_confirmation ',
|
||||
'fieldC' => ['sub' => ' bla2 '],
|
||||
'fieldD' => null,
|
||||
],
|
||||
[
|
||||
'fieldA' => 23,
|
||||
'fieldB' => 'bla',
|
||||
// password fields should keep their surrounding spaces
|
||||
'password' => ' pass1 ',
|
||||
'password2' => ' pass2 ',
|
||||
'new_password' => ' new_password ',
|
||||
'new_password2' => ' new_password2 ',
|
||||
'new_pw' => ' new_pw ',
|
||||
'new_pw2' => ' new_pw2 ',
|
||||
'password_confirmation' => ' password_confirmation ',
|
||||
'fieldC' => ['sub' => 'bla2'],
|
||||
'fieldD' => null,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Middleware\TrimInput
|
||||
* @dataProvider provideTrimTestData
|
||||
*/
|
||||
public function testTrim(string $method, mixed $body, mixed $expectedBody): void
|
||||
{
|
||||
$request = (new Request())->withMethod($method)->withParsedBody($body);
|
||||
$this->handler->expects(self::once())->method('handle')->with(
|
||||
self::callback(function (ServerRequestInterface $request) use ($expectedBody): bool {
|
||||
self::assertSame($expectedBody, $request->getParsedBody());
|
||||
return true;
|
||||
})
|
||||
);
|
||||
$this->subject->process($request, $this->handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Special test case to cover null value parsed body.
|
||||
*
|
||||
* @covers \Engelsystem\Middleware\TrimInput
|
||||
*/
|
||||
public function testTrimPostNull(): void
|
||||
{
|
||||
$request = $this->getMockForAbstractClass(ServerRequestInterface::class);
|
||||
$request->method('getMethod')->willReturn('POST');
|
||||
$request->method('getParsedBody')->willReturn(null);
|
||||
$this->handler->expects(self::once())->method('handle')->with(
|
||||
self::callback(function (ServerRequestInterface $request): bool {
|
||||
self::assertNull($request->getParsedBody());
|
||||
return true;
|
||||
})
|
||||
);
|
||||
$this->subject->process($request, $this->handler);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue