From 9feed46d4e9149909c513acb57858e1a2acd317a Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 13 Jun 2023 00:11:32 +0200 Subject: [PATCH] Add TrimMiddleware to trim all request values --- config/app.php | 1 + src/Middleware/TrimInput.php | 70 ++++++++++++++++ tests/Unit/Middleware/TrimInputTest.php | 106 ++++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 src/Middleware/TrimInput.php create mode 100644 tests/Unit/Middleware/TrimInputTest.php diff --git a/config/app.php b/config/app.php index 4489ba9c..ff3bed30 100644 --- a/config/app.php +++ b/config/app.php @@ -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, diff --git a/src/Middleware/TrimInput.php b/src/Middleware/TrimInput.php new file mode 100644 index 00000000..f4b7a7e5 --- /dev/null +++ b/src/Middleware/TrimInput.php @@ -0,0 +1,70 @@ + 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 $array + * @return array + */ + 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; + } +} diff --git a/tests/Unit/Middleware/TrimInputTest.php b/tests/Unit/Middleware/TrimInputTest.php new file mode 100644 index 00000000..8a0fedad --- /dev/null +++ b/tests/Unit/Middleware/TrimInputTest.php @@ -0,0 +1,106 @@ + + */ + private MockObject $handler; + + public function setUp(): void + { + $this->subject = new TrimInput(); + $this->handler = $this->getMockForAbstractClass(RequestHandlerInterface::class); + } + + /** + * @return Generator + */ + 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); + } +}