Skip to content

Commit e5b8bcb

Browse files
author
Mokhtar Tlili
committed
#10: throw UnsupportedMediaTypeHttpException in case Content-Type header is missing or unsupported.
1 parent d695f67 commit e5b8bcb

File tree

12 files changed

+147
-172
lines changed

12 files changed

+147
-172
lines changed

Diff for: CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## 1.2.5
4+
- Fix [#10](https://github.com/sfmok/request-input-bundle/issues/10) issue - throw `UnsupportedMediaTypeHttpException` in case `Content-Type` header is missing or unsupported.
5+
6+
## 1.2.4
7+
- Update workflow to support dependencies checking
8+
- Update readme file
9+
- Update .gitignore file
10+
- Fix dependencies issue
11+
312
## 1.2.3
413
- Register services in bundle extension file
514
- Fix exceptions issues

Diff for: README.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
**RequestInputBundle** converts request data into DTO inputs objects with validation.
88

9-
- Request data supported: `json`, `xml` and `form` based on header content type.
9+
- Request data supported: `json`, `xml` and `form` based on `Content-Type` header.
1010
- Resolve inputs arguments for controllers actions.
1111
- Create DTO inputs outside controllers
1212
- Validate DTO inputs objects.
@@ -94,6 +94,11 @@ Whether the request data contains invalid syntax or invalid attributes types a c
9494

9595
- Data Error
9696

97+
```json
98+
{
99+
"title": 12
100+
}
101+
```
97102
```json
98103
{
99104
"title": "Deserialization Failed",
@@ -109,6 +114,11 @@ Whether the request data contains invalid syntax or invalid attributes types a c
109114
```
110115
- Syntax error:
111116
```json
117+
{
118+
"title": 12,
119+
}
120+
```
121+
```json
112122
{
113123
"title": "Deserialization Failed",
114124
"detail": "Syntax error",
@@ -151,7 +161,7 @@ class PostManager
151161
152162
public function getInput(Request $request): InputInterface
153163
{
154-
return $this->inputFactory->createFromRequest($request, PostInput::class, 'json');
164+
return $this->inputFactory->createFromRequest($request, PostInput::class);
155165
}
156166
}
157167
```

Diff for: src/ArgumentResolver/InputArgumentResolver.php

+3-13
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,24 @@
55
namespace Sfmok\RequestInput\ArgumentResolver;
66

77
use Sfmok\RequestInput\InputInterface;
8-
use Sfmok\RequestInput\Attribute\Input;
98
use Symfony\Component\HttpFoundation\Request;
109
use Sfmok\RequestInput\Factory\InputFactoryInterface;
1110
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
1211
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
1312

1413
class InputArgumentResolver implements ArgumentValueResolverInterface
1514
{
16-
public function __construct(private InputFactoryInterface $inputFactory, private array $inputFormats)
15+
public function __construct(private InputFactoryInterface $inputFactory)
1716
{
1817
}
1918

2019
public function supports(Request $request, ArgumentMetadata $argument): bool
2120
{
22-
if (!is_subclass_of($argument->getType(), InputInterface::class)) {
23-
return false;
24-
}
25-
26-
/** @var Input|null $inputAttribute */
27-
if ($inputAttribute = $request->attributes->get('_input')) {
28-
$this->inputFormats = [$inputAttribute->getFormat()];
29-
}
30-
31-
return \in_array($request->getContentType(), $this->inputFormats);
21+
return is_subclass_of($argument->getType(), InputInterface::class);
3222
}
3323

3424
public function resolve(Request $request, ArgumentMetadata $argument): iterable
3525
{
36-
yield $this->inputFactory->createFromRequest($request, $argument->getType(), $request->getContentType());
26+
yield $this->inputFactory->createFromRequest($request, $argument->getType());
3727
}
3828
}

Diff for: src/Attribute/Input.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public function __construct(
2121

2222
public function getFormat(): string
2323
{
24-
return $this->format;
24+
return mb_strtolower($this->format);
2525
}
2626

2727
public function getGroups(): array

Diff for: src/DependencyInjection/RequestInputExtension.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function load(array $configs, ContainerBuilder $container): void
3636
'$serializer' => new Reference(SerializerInterface::class),
3737
'$validator' => new Reference(ValidatorInterface::class),
3838
'$skipValidation' => $config['skip_validation'],
39+
'$inputFormats' => $config['formats'],
3940
])
4041
->setPublic(false)
4142
;
@@ -48,7 +49,6 @@ public function load(array $configs, ContainerBuilder $container): void
4849
$container->register(InputArgumentResolver::class)
4950
->setArguments([
5051
'$inputFactory' => new Reference(InputFactoryInterface::class),
51-
'$inputFormats' => $config['formats'],
5252
])
5353
->addTag('controller.argument_value_resolver', ['priority' => 40])
5454
->setPublic(false)

Diff for: src/EventListener/ReadInputListener.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function onKernelController(ControllerEvent $event): void
2323
return;
2424
}
2525

26-
if (!\in_array($inputMetadata->getFormat(), Input::INPUT_SUPPORTED_FORMATS)) {
26+
if (!\in_array($inputMetadata->getFormat(), Input::INPUT_SUPPORTED_FORMATS, true)) {
2727
throw new UnexpectedFormatException(sprintf(
2828
'Only the formats [%s] are supported. Got %s.',
2929
implode(', ', Input::INPUT_SUPPORTED_FORMATS),

Diff for: src/Factory/InputFactory.php

+30-13
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66

77
use Sfmok\RequestInput\Attribute\Input;
88
use Sfmok\RequestInput\Exception\DeserializationException;
9-
use Sfmok\RequestInput\Exception\UnexpectedFormatException;
109
use Sfmok\RequestInput\InputInterface;
1110
use Sfmok\RequestInput\Exception\ValidationException;
1211
use Symfony\Component\HttpFoundation\Request;
13-
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
14-
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
12+
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
1513
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
1614
use Symfony\Component\Serializer\SerializerInterface;
1715
use Symfony\Component\Validator\Validator\ValidatorInterface;
@@ -24,28 +22,37 @@ final class InputFactory implements InputFactoryInterface
2422
public function __construct(
2523
private SerializerInterface $serializer,
2624
private ValidatorInterface $validator,
27-
private bool $skipValidation
25+
private bool $skipValidation,
26+
private array $inputFormats
2827
) {
2928
}
3029

31-
public function createFromRequest(Request $request, string $type, string $format): InputInterface
30+
public function createFromRequest(Request $request, string $type): InputInterface
3231
{
33-
if (!\in_array($format, Input::INPUT_SUPPORTED_FORMATS)) {
34-
throw new UnexpectedFormatException(sprintf(
35-
'Only the formats [%s] are supported. Got %s.',
36-
implode(', ', Input::INPUT_SUPPORTED_FORMATS),
37-
$format
38-
));
32+
if (\func_num_args() > 2) {
33+
@trigger_error("Third argument of 'InputFactory::createFromRequest' is not in use and is removed, however the argument in the callers code can be removed without side-effects.", E_USER_DEPRECATED);
34+
}
35+
36+
$contentType = $request->headers->get('CONTENT_TYPE');
37+
if (null === $contentType || '' === $contentType) {
38+
throw new UnsupportedMediaTypeHttpException('The "Content-Type" header must exist and not empty.');
39+
}
40+
41+
$inputMetadata = $request->attributes->get('_input');
42+
$supportedFormats = (array) ($inputMetadata?->getFormat() ?? $this->inputFormats);
43+
$supportedMimeTypes = $this->getSupportedMimeTypes($request, $supportedFormats);
44+
45+
if (!\in_array($contentType, $supportedMimeTypes, true)) {
46+
throw new UnsupportedMediaTypeHttpException(sprintf('The content-type "%s" is not supported. Supported MIME types are "%s".', $contentType, implode('", "', $supportedMimeTypes)));
3947
}
4048

4149
$data = $request->getContent();
50+
$format = $request->getContentType();
4251
if (Input::INPUT_FORM_FORMAT === $format) {
4352
$data = json_encode($request->request->all());
4453
$format = Input::INPUT_JSON_FORMAT;
4554
}
4655

47-
$inputMetadata = $request->attributes->get('_input');
48-
4956
try {
5057
$input = $this->serializer->deserialize($data, $type, $format, $inputMetadata?->getContext() ?? []);
5158
} catch (UnexpectedValueException $exception) {
@@ -63,4 +70,14 @@ public function createFromRequest(Request $request, string $type, string $format
6370

6471
return $input;
6572
}
73+
74+
private function getSupportedMimeTypes(Request $request, array $supportedFormats): array
75+
{
76+
$mimeTypes = [];
77+
foreach ($supportedFormats as $format) {
78+
$mimeTypes = [...$mimeTypes, ...$request->getMimeTypes($format)];
79+
}
80+
81+
return $mimeTypes;
82+
}
6683
}

Diff for: src/Factory/InputFactoryInterface.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99

1010
interface InputFactoryInterface
1111
{
12-
public function createFromRequest(Request $request, string $type, string $format): InputInterface;
12+
public function createFromRequest(Request $request, string $type): InputInterface;
1313
}

Diff for: tests/ArgumentResolver/InputArgumentResolverTest.php

+15-94
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Prophecy\PhpUnit\ProphecyTrait;
99
use Prophecy\Prophecy\ObjectProphecy;
1010
use Sfmok\RequestInput\ArgumentResolver\InputArgumentResolver;
11-
use Sfmok\RequestInput\Attribute\Input;
1211
use Sfmok\RequestInput\Factory\InputFactoryInterface;
1312
use Sfmok\RequestInput\Tests\Fixtures\Input\DummyInput;
1413
use Symfony\Component\HttpFoundation\Request;
@@ -25,119 +24,41 @@ protected function setUp(): void
2524
$this->inputFactory = $this->prophesize(InputFactoryInterface::class);
2625
}
2726

28-
public function testSupportsWithArgumentTypeNotInput(): void
29-
{
30-
$request = new Request();
31-
$request->headers->set('Content-Type', 'application/json');
32-
$argument = new ArgumentMetadata('foo', \stdClass::class, false, false, null);
33-
34-
$resolver = $this->createArgumentResolver(Input::INPUT_SUPPORTED_FORMATS);
35-
$this->assertFalse($resolver->supports($request, $argument));
36-
}
37-
38-
/**
39-
* @dataProvider provideSupportsWithDefaultGlobalFormats
40-
*/
41-
public function testSupportsWithDefaultGlobalFormats(bool $expected, ?string $contentType): void
42-
{
43-
$request = new Request();
44-
$request->headers->set('Content-Type', $contentType);
45-
$argument = new ArgumentMetadata('foo', DummyInput::class, false, false, null);
46-
47-
$resolver = $this->createArgumentResolver(Input::INPUT_SUPPORTED_FORMATS);
48-
$this->assertSame($expected, $resolver->supports($request, $argument));
49-
}
50-
51-
/**
52-
* @dataProvider provideSupportsWithCustomGlobalFormats
53-
*/
54-
public function testSupportsWithCustomGlobalFormats(bool $expected, ?string $contentType): void
55-
{
56-
$request = new Request();
57-
$request->headers->set('Content-Type', $contentType);
58-
59-
$argument = new ArgumentMetadata('foo', DummyInput::class, false, false, null);
60-
61-
$resolver = $this->createArgumentResolver(['json']);
62-
$this->assertSame($expected, $resolver->supports($request, $argument));
63-
}
64-
6527
/**
66-
* @dataProvider provideSupportsWithCustomFormatsInInputAttribute
28+
* @dataProvider provideSupportData
6729
*/
68-
public function testSupportsWithCustomFormatsInInputAttribute(bool $expected, ?string $contentType): void
30+
public function testSupports(mixed $type, bool $expectedResult): void
6931
{
70-
$request = new Request();
71-
$request->headers->set('Content-Type', $contentType);
72-
$request->attributes->set('_input', new Input('xml'));
73-
74-
$argument = new ArgumentMetadata('foo', DummyInput::class, false, false, null);
75-
76-
$resolver = $this->createArgumentResolver(Input::INPUT_SUPPORTED_FORMATS);
77-
$this->assertSame($expected, $resolver->supports($request, $argument));
32+
$argument = new ArgumentMetadata('foo', $type, false, false, null);
33+
$this->assertSame($expectedResult, $this->createArgumentResolver()->supports(new Request(), $argument));
7834
}
7935

80-
public function testResolveSucceeds(): void
36+
public function testResolve(): void
8137
{
8238
$dummyInput = new DummyInput();
83-
8439
$request = new Request();
85-
$request->headers->set('Content-Type', 'application/json');
86-
87-
$argument = new ArgumentMetadata('foo', DummyInput::class, false, false, null);
88-
89-
$resolver = $this->createArgumentResolver([Input::INPUT_SUPPORTED_FORMATS]);
40+
$argument = new ArgumentMetadata('foo', $dummyInput::class, false, false, null);
9041

9142
$this->inputFactory
92-
->createFromRequest($request, $argument->getType(), $request->getContentType())
43+
->createFromRequest($request, $dummyInput::class, $request->getContentType())
9344
->shouldBeCalledOnce()
9445
->willReturn($dummyInput)
9546
;
9647

48+
$resolver = $this->createArgumentResolver();
9749
$this->assertEquals([$dummyInput], iterator_to_array($resolver->resolve($request, $argument)));
9850
}
9951

100-
public function provideSupportsWithDefaultGlobalFormats(): iterable
101-
{
102-
yield [false, null];
103-
yield [false, 'application/rdf+xml'];
104-
yield [false, 'text/html'];
105-
yield [false, 'application/javascript'];
106-
yield [false, 'text/plain'];
107-
yield [false, 'application/ld+json'];
108-
yield [true, 'application/json'];
109-
yield [true, 'application/xml'];
110-
yield [true, 'multipart/form-data'];
111-
}
112-
113-
public function provideSupportsWithCustomGlobalFormats(): iterable
114-
{
115-
yield [false, null];
116-
yield [false, 'application/rdf+xml'];
117-
yield [false, 'text/html'];
118-
yield [false, 'application/javascript'];
119-
yield [false, 'text/plain'];
120-
yield [false, 'application/ld+json'];
121-
yield [true, 'application/json'];
122-
yield [false, 'application/xml'];
123-
yield [false, 'multipart/form-data'];
124-
}
125-
126-
public function provideSupportsWithCustomFormatsInInputAttribute(): iterable
52+
public function provideSupportData(): iterable
12753
{
128-
yield [false, null];
129-
yield [false, 'application/rdf+xml'];
130-
yield [false, 'text/html'];
131-
yield [false, 'application/javascript'];
132-
yield [false, 'text/plain'];
133-
yield [false, 'application/ld+json'];
134-
yield [false, 'application/json'];
135-
yield [true, 'application/xml'];
136-
yield [false, 'multipart/form-data'];
54+
yield [null, false];
55+
yield [\stdClass::class, false];
56+
yield ["Foo", false];
57+
yield [DummyInput::class, true];
13758
}
13859

139-
private function createArgumentResolver(array $formats): InputArgumentResolver
60+
private function createArgumentResolver(): InputArgumentResolver
14061
{
141-
return new InputArgumentResolver($this->inputFactory->reveal(), $formats);
62+
return new InputArgumentResolver($this->inputFactory->reveal());
14263
}
14364
}

Diff for: tests/DependencyInjection/RequestInputExtensionTest.php

+17
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,23 @@ public function testLoadConfiguration(): void
5757
$this->assertServiceHasTags(ReadInputListener::class, ['kernel.event_listener']);
5858
}
5959

60+
public function testLoadConfigurationWithDisabledOption(): void
61+
{
62+
(new RequestInputExtension())->load(['request_input' => ['enabled' => false]], $this->container);
63+
64+
$services = [
65+
InputArgumentResolver::class,
66+
ExceptionListener::class,
67+
ReadInputListener::class,
68+
InputFactory::class,
69+
InputMetadataFactory::class
70+
];
71+
72+
foreach ($services as $service) {
73+
$this->assertFalse($this->container->hasDefinition($service), sprintf('Definition "%s" is found.', $service));
74+
}
75+
}
76+
6077
private function assertContainerHas(array $services, array $aliases = []): void
6178
{
6279
foreach ($services as $service) {

0 commit comments

Comments
 (0)