Add TrimMiddleware to trim all request values

This commit is contained in:
Michael Weimann 2023-06-13 00:11:32 +02:00 committed by Igor Scheller
parent 8a3c2eaec5
commit 9feed46d4e
3 changed files with 177 additions and 0 deletions

View File

@ -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,

View File

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

View File

@ -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);
}
}