Skip to content

Commit dbe76a0

Browse files
committed
Merge branch '2.x'
* 2.x: fix access denied listener, add tests add a SerializerErrorRenderer
2 parents 4d453b2 + 8a79940 commit dbe76a0

File tree

9 files changed

+293
-58
lines changed

9 files changed

+293
-58
lines changed

DependencyInjection/Configuration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ private function addExceptionSection(ArrayNodeDefinition $rootNode): void
448448
->defaultValue('legacy')
449449
->values(['legacy', 'rfc7807'])
450450
->end()
451+
->booleanNode('serializer_error_renderer')->defaultValue(false)->end()
451452
->arrayNode('codes')
452453
->useAttributeAsKey('name')
453454
->beforeNormalization()

DependencyInjection/FOSRestExtension.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111

1212
namespace FOS\RestBundle\DependencyInjection;
1313

14+
use FOS\RestBundle\ErrorRenderer\SerializerErrorRenderer;
1415
use FOS\RestBundle\EventListener\ResponseStatusCodeListener;
1516
use FOS\RestBundle\View\ViewHandler;
1617
use Symfony\Component\Config\FileLocator;
1718
use Symfony\Component\DependencyInjection\Alias;
1819
use Symfony\Component\DependencyInjection\ChildDefinition;
1920
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
2021
use Symfony\Component\DependencyInjection\ContainerBuilder;
22+
use Symfony\Component\DependencyInjection\ContainerInterface;
23+
use Symfony\Component\DependencyInjection\Definition;
2124
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
2225
use Symfony\Component\DependencyInjection\Reference;
2326
use Symfony\Component\Form\Extension\Core\Type\FormType;
@@ -329,6 +332,28 @@ private function loadException(array $config, XmlFileLoader $loader, ContainerBu
329332
->replaceArgument(1, $config['exception']['debug']);
330333
$container->getDefinition('fos_rest.serializer.flatten_exception_normalizer')
331334
->replaceArgument(2, 'rfc7807' === $config['exception']['flatten_exception_format']);
335+
336+
if ($config['exception']['serializer_error_renderer']) {
337+
$format = new Definition();
338+
$format->setFactory([SerializerErrorRenderer::class, 'getPreferredFormat']);
339+
$format->setArguments([
340+
new Reference('request_stack'),
341+
]);
342+
$debug = new Definition();
343+
$debug->setFactory([SerializerErrorRenderer::class, 'isDebug']);
344+
$debug->setArguments([
345+
new Reference('request_stack'),
346+
'%kernel.debug%',
347+
]);
348+
$container->register('fos_rest.error_renderer.serializer', SerializerErrorRenderer::class)
349+
->setArguments([
350+
new Reference('fos_rest.serializer'),
351+
$format,
352+
new Reference('error_renderer.html', ContainerInterface::NULL_ON_INVALID_REFERENCE),
353+
$debug,
354+
]);
355+
$container->setAlias('error_renderer.serializer', 'fos_rest.error_renderer.serializer');
356+
}
332357
}
333358
}
334359

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSRestBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\RestBundle\ErrorRenderer;
13+
14+
use FOS\RestBundle\Context\Context;
15+
use FOS\RestBundle\Serializer\Serializer;
16+
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
17+
use Symfony\Component\ErrorHandler\Exception\FlattenException;
18+
use Symfony\Component\HttpFoundation\RequestStack;
19+
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
20+
21+
/**
22+
* @internal
23+
*/
24+
final class SerializerErrorRenderer implements ErrorRendererInterface
25+
{
26+
private $serializer;
27+
private $format;
28+
private $fallbackErrorRenderer;
29+
private $debug;
30+
31+
/**
32+
* @param string|callable(FlattenException) $format
33+
* @param string|bool $debug
34+
*/
35+
public function __construct(Serializer $serializer, $format, ErrorRendererInterface $fallbackErrorRenderer = null, $debug = false)
36+
{
37+
if (!is_string($format) && !is_callable($format)) {
38+
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be a string or a callable, "%s" given.', __METHOD__, \is_object($format) ? \get_class($format) : \gettype($format)));
39+
}
40+
41+
if (!is_bool($debug) && !is_callable($debug)) {
42+
throw new \TypeError(sprintf('Argument 4 passed to "%s()" must be a boolean or a callable, "%s" given.', __METHOD__, \is_object($debug) ? \get_class($debug) : \gettype($debug)));
43+
}
44+
45+
$this->serializer = $serializer;
46+
$this->format = $format;
47+
$this->fallbackErrorRenderer = $fallbackErrorRenderer;
48+
$this->debug = $debug;
49+
}
50+
51+
public function render(\Throwable $exception): FlattenException
52+
{
53+
$flattenException = FlattenException::createFromThrowable($exception);
54+
55+
try {
56+
$format = is_callable($this->format) ? ($this->format)($flattenException) : $this->format;
57+
58+
$context = new Context();
59+
$context->setAttribute('exception', $exception);
60+
$context->setAttribute('debug', is_callable($this->debug) ? ($this->debug)($exception) : $this->debug);
61+
62+
return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, $context));
63+
} catch (NotEncodableValueException $e) {
64+
return $this->fallbackErrorRenderer->render($exception);
65+
}
66+
}
67+
68+
/**
69+
* @see \Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer::getPreferredFormat
70+
*/
71+
public static function getPreferredFormat(RequestStack $requestStack): \Closure
72+
{
73+
return static function () use ($requestStack) {
74+
if (!$request = $requestStack->getCurrentRequest()) {
75+
throw new NotEncodableValueException();
76+
}
77+
78+
return $request->getPreferredFormat();
79+
};
80+
}
81+
82+
/**
83+
* @see \Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer::isDebug
84+
*/
85+
public static function isDebug(RequestStack $requestStack, bool $debug): \Closure
86+
{
87+
return static function () use ($requestStack, $debug): bool {
88+
if (!$request = $requestStack->getCurrentRequest()) {
89+
return $debug;
90+
}
91+
92+
return $debug && $request->attributes->getBoolean('showException', true);
93+
};
94+
}
95+
}

EventListener/AccessDeniedListener.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,12 @@ public function onKernelException(ExceptionEvent $event): void
6767
$exception = $event->getThrowable();
6868

6969
if ($exception instanceof AccessDeniedException) {
70-
$exception = new AccessDeniedHttpException('You do not have the necessary permissions', $exception);
70+
$exception = new AccessDeniedHttpException('You do not have the necessary permissions');
7171
} elseif ($exception instanceof AuthenticationException) {
7272
if ($this->challenge) {
73-
$exception = new UnauthorizedHttpException($this->challenge, 'You are not authenticated', $exception);
73+
$exception = new UnauthorizedHttpException($this->challenge, 'You are not authenticated');
7474
} else {
75-
$exception = new HttpException(401, 'You are not authenticated', $exception);
75+
$exception = new HttpException(401, 'You are not authenticated');
7676
}
7777
}
7878

Tests/DependencyInjection/FOSRestExtensionTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\DependencyInjection\ContainerBuilder;
2121
use Symfony\Component\DependencyInjection\Definition;
2222
use Symfony\Component\DependencyInjection\Reference;
23+
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
2324

2425
/**
2526
* FOSRestExtension test.
@@ -590,4 +591,58 @@ public function testMimeTypesArePassedArrays()
590591
$this->container->getDefinition('fos_rest.mime_type_listener')->getArgument(0)
591592
);
592593
}
594+
595+
public function testSerializerErrorRendererNotRegisteredByDefault()
596+
{
597+
$config = array(
598+
'fos_rest' => array(
599+
'exception' => [
600+
'exception_listener' => false,
601+
'serialize_exceptions' => false,
602+
],
603+
'routing_loader' => false,
604+
'service' => [
605+
'templating' => null,
606+
],
607+
'view' => [
608+
'default_engine' => null,
609+
'force_redirects' => [],
610+
],
611+
),
612+
);
613+
$this->extension->load($config, $this->container);
614+
615+
$this->assertFalse($this->container->hasDefinition('fos_rest.error_renderer.serializer'));
616+
$this->assertFalse($this->container->hasAlias('error_renderer.serializer'));
617+
}
618+
619+
public function testRegisterSerializerErrorRenderer()
620+
{
621+
if (!interface_exists(ErrorRendererInterface::class)) {
622+
$this->markTestSkipped();
623+
}
624+
625+
$config = array(
626+
'fos_rest' => array(
627+
'exception' => [
628+
'exception_listener' => false,
629+
'serialize_exceptions' => false,
630+
'serializer_error_renderer' => true,
631+
],
632+
'routing_loader' => false,
633+
'service' => [
634+
'templating' => null,
635+
],
636+
'view' => [
637+
'default_engine' => null,
638+
'force_redirects' => [],
639+
],
640+
),
641+
);
642+
$this->extension->load($config, $this->container);
643+
644+
$this->assertTrue($this->container->hasDefinition('fos_rest.error_renderer.serializer'));
645+
$this->assertTrue($this->container->hasAlias('error_renderer.serializer'));
646+
$this->assertSame('fos_rest.error_renderer.serializer', (string) $this->container->getAlias('error_renderer.serializer'));
647+
}
593648
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSRestBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\RestBundle\Tests\ErrorRenderer;
13+
14+
use FOS\RestBundle\Context\Context;
15+
use FOS\RestBundle\ErrorRenderer\SerializerErrorRenderer;
16+
use FOS\RestBundle\Serializer\Serializer;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
19+
use Symfony\Component\ErrorHandler\Exception\FlattenException;
20+
use Symfony\Component\HttpFoundation\Request;
21+
use Symfony\Component\HttpFoundation\RequestStack;
22+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
23+
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
24+
25+
class SerializerErrorRendererTest extends TestCase
26+
{
27+
protected function setUp()
28+
{
29+
if (!interface_exists(ErrorRendererInterface::class)) {
30+
$this->markTestSkipped();
31+
}
32+
}
33+
34+
public function testSerializeFlattenExceptionWithStringFormat()
35+
{
36+
$serializer = $this->createMock(Serializer::class);
37+
$serializer
38+
->expects($this->once())
39+
->method('serialize')
40+
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
41+
->willReturn('serialized FlattenException');
42+
43+
$errorRenderer = new SerializerErrorRenderer($serializer, 'json');
44+
$flattenException = $errorRenderer->render(new NotFoundHttpException());
45+
46+
$this->assertSame('serialized FlattenException', $flattenException->getAsString());
47+
}
48+
49+
public function testSerializeFlattenExceptionWithCallableFormat()
50+
{
51+
$serializer = $this->createMock(Serializer::class);
52+
$serializer
53+
->expects($this->once())
54+
->method('serialize')
55+
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
56+
->willReturn('serialized FlattenException');
57+
58+
$format = function (FlattenException $flattenException) {
59+
return 'json';
60+
};
61+
62+
$errorRenderer = new SerializerErrorRenderer($serializer, $format);
63+
$flattenException = $errorRenderer->render(new NotFoundHttpException());
64+
65+
$this->assertSame('serialized FlattenException', $flattenException->getAsString());
66+
}
67+
68+
public function testSerializeFlattenExceptionUsingGetPreferredFormatMethod()
69+
{
70+
$serializer = $this->createMock(Serializer::class);
71+
$serializer
72+
->expects($this->once())
73+
->method('serialize')
74+
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
75+
->willReturn('serialized FlattenException');
76+
77+
$request = new Request();
78+
$request->attributes->set('_format', 'json');
79+
80+
$requestStack = new RequestStack();
81+
$requestStack->push($request);
82+
$format = SerializerErrorRenderer::getPreferredFormat($requestStack);
83+
84+
$errorRenderer = new SerializerErrorRenderer($serializer, $format);
85+
$flattenException = $errorRenderer->render(new NotFoundHttpException());
86+
87+
$this->assertSame('serialized FlattenException', $flattenException->getAsString());
88+
}
89+
90+
public function testFallbackErrorRendererIsUsedWhenFormatCannotBeDetected()
91+
{
92+
$exception = new NotFoundHttpException();
93+
$flattenException = new FlattenException();
94+
95+
$fallbackErrorRenderer = $this->createMock(ErrorRendererInterface::class);
96+
$fallbackErrorRenderer
97+
->expects($this->once())
98+
->method('render')
99+
->with($exception)
100+
->willReturn($flattenException);
101+
102+
$serializer = $this->createMock(Serializer::class);
103+
$serializer->expects($this->once())
104+
->method('serialize')
105+
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
106+
->willThrowException(new NotEncodableValueException());
107+
108+
$errorRenderer = new SerializerErrorRenderer($serializer, 'json', $fallbackErrorRenderer);
109+
110+
$this->assertSame($flattenException, $errorRenderer->render($exception));
111+
}
112+
}

Tests/Functional/Bundle/TestBundle/ErrorRenderer/JmsSerializerErrorRenderer.php

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)