From 11f71e83a6e4bbb46d27232bd24ecb8c16aed353 Mon Sep 17 00:00:00 2001 From: Steven Renaux Date: Mon, 14 Aug 2023 11:46:33 +0200 Subject: [PATCH] Adding a new Attribute MapRequestHeader class and resolver --- .../FrameworkBundle/Resources/config/web.php | 4 + .../HttpKernel/Attribute/MapRequestHeader.php | 32 +++ src/Symfony/Component/HttpKernel/CHANGELOG.md | 1 + .../RequestHeaderValueResolver.php | 66 ++++++ .../RequestHeaderValueResolverTest.php | 214 ++++++++++++++++++ 5 files changed, 317 insertions(+) create mode 100644 src/Symfony/Component/HttpKernel/Attribute/MapRequestHeader.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestHeaderValueResolver.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestHeaderValueResolverTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php index a4e975dac8749..0e39565fa918d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestHeaderValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver; @@ -101,6 +102,9 @@ ->set('argument_resolver.query_parameter_value_resolver', QueryParameterValueResolver::class) ->tag('controller.targeted_value_resolver', ['name' => QueryParameterValueResolver::class]) + ->set('argument_resolver.header_value_resolver', RequestHeaderValueResolver::class) + ->tag('controller.targeted_value_resolver', ['name' => RequestHeaderValueResolver::class]) + ->set('response_listener', ResponseListener::class) ->args([ param('kernel.charset'), diff --git a/src/Symfony/Component/HttpKernel/Attribute/MapRequestHeader.php b/src/Symfony/Component/HttpKernel/Attribute/MapRequestHeader.php new file mode 100644 index 0000000000000..d7b2cd8555548 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Attribute/MapRequestHeader.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Attribute; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestHeaderValueResolver; + +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class MapRequestHeader extends ValueResolver +{ + /** + * @param string|null $name The name of the header parameter; if null, the name of the argument in the controller will be used + * @param string $resolver The class name of the resolver to use + * @param int $validationFailedStatusCode The HTTP code to return if the validation fails + */ + public function __construct( + public readonly ?string $name = null, + string $resolver = RequestHeaderValueResolver::class, + public readonly int $validationFailedStatusCode = Response::HTTP_BAD_REQUEST, + ) { + parent::__construct($resolver); + } +} diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 5df71549449f3..f12478dce0b7f 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -65,6 +65,7 @@ CHANGELOG * Add argument `$buildDir` to `WarmableInterface` * Add argument `$filter` to `Profiler::find()` and `FileProfilerStorage::find()` * Add `ControllerResolver::allowControllers()` to define which callables are legit controllers when the `_check_controller_is_allowed` request attribute is set + * Add `#[MapRequestHeader]` to map header from `Request::$headers` 6.3 --- diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestHeaderValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestHeaderValueResolver.php new file mode 100644 index 0000000000000..17fef55294c14 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestHeaderValueResolver.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; + +use Symfony\Component\HttpFoundation\AcceptHeader; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Attribute\MapRequestHeader; +use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\HttpException; + +class RequestHeaderValueResolver implements ValueResolverInterface +{ + public function resolve(Request $request, ArgumentMetadata $argument): iterable + { + if (!$attribute = $argument->getAttributesOfType(MapRequestHeader::class)[0] ?? null) { + return []; + } + + $type = $argument->getType(); + + if (!\in_array($type, ['string', 'array', AcceptHeader::class])) { + throw new \LogicException(\sprintf('Could not resolve the argument typed "%s". Valid values types are "array", "string" or "%s".', $type, AcceptHeader::class)); + } + + $name = $attribute->name ?? $argument->getName(); + $value = null; + + if ($request->headers->has($name)) { + $value = match ($type) { + 'string' => $request->headers->get($name), + 'array' => match (strtolower($name)) { + 'accept' => $request->getAcceptableContentTypes(), + 'accept-charset' => $request->getCharsets(), + 'accept-language' => $request->getLanguages(), + 'accept-encoding' => $request->getEncodings(), + default => $request->headers->all($name), + }, + default => AcceptHeader::fromString($request->headers->get($name)), + }; + } + + if (null === $value && $argument->hasDefaultValue()) { + $value = $argument->getDefaultValue(); + } + + if (null === $value && $type === 'array') { + $value = []; + } + + if (null === $value && !$argument->isNullable()) { + throw new HttpException($attribute->validationFailedStatusCode, \sprintf('Missing header "%s".', $name)); + } + + return [$value]; + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestHeaderValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestHeaderValueResolverTest.php new file mode 100644 index 0000000000000..6843021ef7d40 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestHeaderValueResolverTest.php @@ -0,0 +1,214 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\AcceptHeader; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Attribute\MapRequestHeader; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestHeaderValueResolver; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\Exception\HttpException; + +class RequestHeaderValueResolverTest extends TestCase +{ + public static function provideHeaderValueWithStringType(): iterable + { + yield 'with accept' => ['accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8']; + yield 'with accept-language' => ['accept-language', 'en-us,en;q=0.5']; + yield 'with host' => ['host', 'localhost']; + yield 'with user-agent' => ['user-agent', 'Symfony']; + } + + public static function provideHeaderValueWithArrayType(): iterable + { + yield 'with accept' => [ + 'accept', + 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + [ + [ + 'text/html', + 'application/xhtml+xml', + 'application/xml', + '*/*', + ], + ], + ]; + yield 'with accept-language' => [ + 'accept-language', + 'en-us,en;q=0.5', + [ + [ + 'en_US', + 'en', + ], + ], + ]; + yield 'with host' => [ + 'host', + 'localhost', + [ + ['localhost'], + ], + ]; + yield 'with user-agent' => [ + 'user-agent', + 'Symfony', + [ + ['Symfony'], + ], + ]; + } + + public static function provideHeaderValueWithAcceptHeaderType(): iterable + { + yield 'with accept' => [ + 'accept', + 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + [AcceptHeader::fromString('text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8')], + ]; + yield 'with accept-language' => [ + 'accept-language', + 'en-us,en;q=0.5', + [AcceptHeader::fromString('en-us,en;q=0.5')], + ]; + yield 'with host' => [ + 'host', + 'localhost', + [AcceptHeader::fromString('localhost')], + ]; + yield 'with user-agent' => [ + 'user-agent', + 'Symfony', + [AcceptHeader::fromString('Symfony')], + ]; + } + + public static function provideHeaderValueWithDefaultAndNull(): iterable + { + yield 'with hasDefaultValue' => [true, 'foo', false, 'foo']; + yield 'with no isNullable' => [false, null, true, null]; + } + + public function testWrongType() + { + $this->expectException(\LogicException::class); + + $metadata = new ArgumentMetadata('accept', 'int', false, false, null, false, [ + MapRequestHeader::class => new MapRequestHeader(), + ]); + + $request = Request::create('/'); + + $resolver = new RequestHeaderValueResolver(); + $resolver->resolve($request, $metadata); + } + + /** + * @dataProvider provideHeaderValueWithStringType + */ + public function testWithStringType(string $parameter, string $value) + { + $resolver = new RequestHeaderValueResolver(); + + $metadata = new ArgumentMetadata('variableName', 'string', false, false, null, false, [ + MapRequestHeader::class => new MapRequestHeader($parameter), + ]); + + $request = Request::create('/'); + $request->headers->set($parameter, $value); + + $arguments = $resolver->resolve($request, $metadata); + + self::assertEquals([$value], $arguments); + } + + /** + * @dataProvider provideHeaderValueWithArrayType + */ + public function testWithArrayType(string $parameter, string $value, array $expected) + { + $resolver = new RequestHeaderValueResolver(); + + $metadata = new ArgumentMetadata('variableName', 'array', false, false, null, false, [ + MapRequestHeader::class => new MapRequestHeader($parameter), + ]); + + $request = Request::create('/'); + $request->headers->set($parameter, $value); + + $arguments = $resolver->resolve($request, $metadata); + + self::assertEquals($expected, $arguments); + } + + /** + * @dataProvider provideHeaderValueWithAcceptHeaderType + */ + public function testWithAcceptHeaderType(string $parameter, string $value, array $expected) + { + $resolver = new RequestHeaderValueResolver(); + + $metadata = new ArgumentMetadata('variableName', AcceptHeader::class, false, false, null, false, [ + MapRequestHeader::class => new MapRequestHeader($parameter), + ]); + + $request = Request::create('/'); + $request->headers->set($parameter, $value); + + $arguments = $resolver->resolve($request, $metadata); + + self::assertEquals($expected, $arguments); + } + + /** + * @dataProvider provideHeaderValueWithDefaultAndNull + */ + public function testWithDefaultValueAndNull(bool $hasDefaultValue, ?string $defaultValue, bool $isNullable, ?string $expected) + { + $metadata = new ArgumentMetadata('wrong-header', 'string', false, $hasDefaultValue, $defaultValue, $isNullable, [ + MapRequestHeader::class => new MapRequestHeader(), + ]); + + $request = Request::create('/'); + + $resolver = new RequestHeaderValueResolver(); + $arguments = $resolver->resolve($request, $metadata); + + self::assertEquals([$expected], $arguments); + } + + public function testWithNoDefaultAndNotNullable() + { + $this->expectException(HttpException::class); + $this->expectExceptionMessage('Missing header "variableName".'); + + $metadata = new ArgumentMetadata('variableName', 'string', false, false, null, false, [ + MapRequestHeader::class => new MapRequestHeader(), + ]); + + $resolver = new RequestHeaderValueResolver(); + $resolver->resolve(Request::create('/'), $metadata); + } + + public function testWithNoDefaultAndNotNullableArray() + { + $metadata = new ArgumentMetadata('variableName', 'array', false, false, null, false, [ + MapRequestHeader::class => new MapRequestHeader(), + ]); + + $resolver = new RequestHeaderValueResolver(); + $arguments = $resolver->resolve(Request::create('/'), $metadata); + + self::assertEquals([[]], $arguments); + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy