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\SetLocale::class,
|
||||||
\Engelsystem\Middleware\ETagHandler::class,
|
\Engelsystem\Middleware\ETagHandler::class,
|
||||||
\Engelsystem\Middleware\AddHeaders::class,
|
\Engelsystem\Middleware\AddHeaders::class,
|
||||||
|
\Engelsystem\Middleware\TrimInput::class,
|
||||||
|
|
||||||
// The application code
|
// The application code
|
||||||
\Engelsystem\Middleware\ErrorHandler::class,
|
\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