Skip to content

Commit fb3e879

Browse files
authored
IBX-10144: Implemented supported_media_types flag for REST requests (#179)
* IBX-10144: Implemented `xml_disabled` flag for REST requests * reworked the solution to have a whitelist of formats instead * added additional test for unknown header/media-type * cr remarks * cr remarks 2 * cr remarks 3 * regenerated baseline * regenerated baseline * regenerated baseline
1 parent 12976d6 commit fb3e879

File tree

4 files changed

+232
-6
lines changed

4 files changed

+232
-6
lines changed

phpstan-baseline.neon

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3084,12 +3084,6 @@ parameters:
30843084
count: 1
30853085
path: src/lib/Server/Input/Parser/FacetBuilder/FieldParser.php
30863086

3087-
-
3088-
message: '#^Parameter \#1 \$properties of class Ibexa\\Contracts\\Core\\Repository\\Values\\Content\\Query\\FacetBuilder\\FieldFacetBuilder constructor expects array\<string, mixed\>, hasOffsetValue\(string, int\) given\.$#'
3089-
identifier: argument.type
3090-
count: 1
3091-
path: src/lib/Server/Input/Parser/FacetBuilder/FieldParser.php
3092-
30933087
-
30943088
message: '#^Method Ibexa\\Rest\\Server\\Input\\Parser\\FacetBuilder\\FieldRangeParser\:\:parse\(\) has parameter \$data with no value type specified in iterable type array\.$#'
30953089
identifier: missingType.iterableValue
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Bundle\Rest\EventListener;
10+
11+
use RuntimeException;
12+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
13+
use Symfony\Component\HttpFoundation\Response;
14+
use Symfony\Component\HttpKernel\Event\RequestEvent;
15+
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
16+
use Symfony\Component\HttpKernel\KernelEvents;
17+
18+
final class SupportedMediaTypesSubscriber implements EventSubscriberInterface
19+
{
20+
private const SUPPORTED_MEDIA_TYPES_REGEX = '/(?<=\+)[A-Za-z0-9]+/';
21+
22+
public static function getSubscribedEvents(): array
23+
{
24+
return [
25+
KernelEvents::REQUEST => [
26+
['allowOnlySupportedMediaTypes', 0],
27+
],
28+
];
29+
}
30+
31+
public function allowOnlySupportedMediaTypes(RequestEvent $event): void
32+
{
33+
$request = $event->getRequest();
34+
if (!$request->attributes->has('supported_media_types')) {
35+
return;
36+
}
37+
38+
$supportedMediaTypes = $request->attributes->get('supported_media_types');
39+
if (empty($supportedMediaTypes)) {
40+
return;
41+
}
42+
43+
$contentTypeHeader = $request->headers->get('Content-Type') ?? '';
44+
$acceptHeader = $request->headers->get('Accept') ?? '';
45+
46+
try {
47+
$isContentTypeHeaderSupported = $this->isMediaTypeSupported(
48+
$contentTypeHeader,
49+
$supportedMediaTypes
50+
);
51+
52+
$isAcceptHeaderSupported = $this->isMediaTypeSupported(
53+
$acceptHeader,
54+
$supportedMediaTypes
55+
);
56+
57+
if ($isContentTypeHeaderSupported && $isAcceptHeaderSupported) {
58+
return;
59+
}
60+
} catch (RuntimeException $e) {
61+
return;
62+
}
63+
64+
throw new UnsupportedMediaTypeHttpException(
65+
sprintf(
66+
'Unsupported media type was used. Available ones are: %s',
67+
implode(', ', $supportedMediaTypes)
68+
),
69+
null,
70+
Response::HTTP_UNSUPPORTED_MEDIA_TYPE
71+
);
72+
}
73+
74+
/**
75+
* @param string[] $supportedMediaTypes
76+
*/
77+
private function isMediaTypeSupported(
78+
string $header,
79+
array $supportedMediaTypes
80+
): bool {
81+
preg_match(self::SUPPORTED_MEDIA_TYPES_REGEX, $header, $matches);
82+
83+
$match = reset($matches);
84+
if ($match === false) {
85+
throw new RuntimeException(sprintf(
86+
'Failed to extract media type from header: %s',
87+
$header
88+
));
89+
}
90+
91+
return in_array($match, $supportedMediaTypes, true);
92+
}
93+
}

src/bundle/Resources/config/services.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ services:
235235
- { name: kernel.event_subscriber }
236236
- { name: monolog.logger, channel: request }
237237

238+
Ibexa\Bundle\Rest\EventListener\SupportedMediaTypesSubscriber:
239+
tags:
240+
- { name: kernel.event_subscriber }
241+
238242
Ibexa\Rest\Server\Controller\Options:
239243
parent: Ibexa\Rest\Server\Controller
240244
tags: [controller.service_arguments]
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Tests\Bundle\Rest\EventListener;
10+
11+
use Ibexa\Bundle\Rest\EventListener\SupportedMediaTypesSubscriber;
12+
use PHPUnit\Framework\TestCase;
13+
use Symfony\Component\HttpFoundation\HeaderBag;
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Event\RequestEvent;
16+
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
17+
use Symfony\Component\HttpKernel\HttpKernelInterface;
18+
19+
final class SupportedMediaTypesSubscriberTest extends TestCase
20+
{
21+
/** @var \Symfony\Component\HttpKernel\HttpKernelInterface&\PHPUnit\Framework\MockObject\MockObject */
22+
private HttpKernelInterface $kernel;
23+
24+
protected function setUp(): void
25+
{
26+
parent::setUp();
27+
28+
$this->kernel = $this->createMock(HttpKernelInterface::class);
29+
}
30+
31+
public function testDoesNothingWhenSupportedMediaTypesParameterIsNotSet(): void
32+
{
33+
$request = new Request();
34+
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
35+
36+
$subscriber = new SupportedMediaTypesSubscriber();
37+
$subscriber->allowOnlySupportedMediaTypes($event);
38+
39+
self::expectNotToPerformAssertions();
40+
}
41+
42+
public function testDoesNothingWhenSupportedMediaTypesParameterIsEmpty(): void
43+
{
44+
$request = new Request();
45+
$request->attributes->set('supported_media_types', []);
46+
47+
$subscriber = new SupportedMediaTypesSubscriber();
48+
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
49+
50+
$subscriber->allowOnlySupportedMediaTypes($event);
51+
self::expectNotToPerformAssertions();
52+
}
53+
54+
public function testDoesNothingWhenMediaTypeIsSupported(): void
55+
{
56+
$request = new Request();
57+
$request->attributes->set('supported_media_types', ['json', 'xml']);
58+
$request->headers = new HeaderBag([
59+
'Content-Type' => 'application/vnd.ibexa.api.ContentCreate+json',
60+
'Accept' => 'application/vnd.ibexa.api.ContentCreate+json',
61+
]);
62+
63+
$subscriber = new SupportedMediaTypesSubscriber();
64+
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
65+
66+
$subscriber->allowOnlySupportedMediaTypes($event);
67+
68+
self::expectNotToPerformAssertions();
69+
}
70+
71+
public function testThrowsExceptionWhenHeaderTypeIsNotSupported(): void
72+
{
73+
$request = new Request();
74+
$request->attributes->set('supported_media_types', ['json']);
75+
$request->headers = new HeaderBag([
76+
'Content-Type' => 'application/vnd.ibexa.api.ContentCreate+xml',
77+
'Accept' => 'application/vnd.ibexa.api.ContentCreate+xml',
78+
]);
79+
80+
$subscriber = new SupportedMediaTypesSubscriber();
81+
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
82+
83+
$this->expectException(UnsupportedMediaTypeHttpException::class);
84+
$subscriber->allowOnlySupportedMediaTypes($event);
85+
}
86+
87+
public function testThrowsExceptionWhenUnknownMediaTypeIsUsed(): void
88+
{
89+
$request = new Request();
90+
$request->attributes->set('supported_media_types', ['yaml']);
91+
$request->headers = new HeaderBag([
92+
'Content-Type' => 'application/vnd.ibexa.api.ContentCreate+unknown',
93+
'Accept' => 'application/vnd.ibexa.api.ContentCreate+unknown',
94+
]);
95+
96+
$subscriber = new SupportedMediaTypesSubscriber();
97+
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
98+
99+
$this->expectException(UnsupportedMediaTypeHttpException::class);
100+
$subscriber->allowOnlySupportedMediaTypes($event);
101+
}
102+
103+
public function testThrowsExceptionWhenDifferentMediaTypesInHeadersAreUsed(): void
104+
{
105+
$request = new Request();
106+
$request->attributes->set('supported_media_types', ['json']);
107+
$request->headers = new HeaderBag([
108+
'Content-Type' => 'application/vnd.ibexa.api.ContentCreate+json',
109+
'Accept' => 'application/vnd.ibexa.api.ContentCreate+xml',
110+
]);
111+
112+
$subscriber = new SupportedMediaTypesSubscriber();
113+
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
114+
115+
$this->expectException(UnsupportedMediaTypeHttpException::class);
116+
$subscriber->allowOnlySupportedMediaTypes($event);
117+
}
118+
119+
public function testApplicationJsonHeaderIsSupported(): void
120+
{
121+
$request = new Request();
122+
$request->attributes->set('supported_media_types', ['json']);
123+
$request->headers = new HeaderBag([
124+
'Content-Type' => 'application/json',
125+
'Accept' => 'application/json',
126+
]);
127+
128+
$subscriber = new SupportedMediaTypesSubscriber();
129+
$event = new RequestEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST);
130+
131+
$subscriber->allowOnlySupportedMediaTypes($event);
132+
133+
self::expectNotToPerformAssertions();
134+
}
135+
}

0 commit comments

Comments
 (0)