Skip to content

Commit

Permalink
Merge branch '2.x'
Browse files Browse the repository at this point in the history
* 2.x:
  fix access denied listener, add tests
  add a SerializerErrorRenderer
  • Loading branch information
xabbuh committed May 7, 2020
2 parents 4d453b2 + 8a79940 commit dbe76a0
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 58 deletions.
1 change: 1 addition & 0 deletions DependencyInjection/Configuration.php
Expand Up @@ -448,6 +448,7 @@ private function addExceptionSection(ArrayNodeDefinition $rootNode): void
->defaultValue('legacy')
->values(['legacy', 'rfc7807'])
->end()
->booleanNode('serializer_error_renderer')->defaultValue(false)->end()
->arrayNode('codes')
->useAttributeAsKey('name')
->beforeNormalization()
Expand Down
25 changes: 25 additions & 0 deletions DependencyInjection/FOSRestExtension.php
Expand Up @@ -11,13 +11,16 @@

namespace FOS\RestBundle\DependencyInjection;

use FOS\RestBundle\ErrorRenderer\SerializerErrorRenderer;
use FOS\RestBundle\EventListener\ResponseStatusCodeListener;
use FOS\RestBundle\View\ViewHandler;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Form\Extension\Core\Type\FormType;
Expand Down Expand Up @@ -329,6 +332,28 @@ private function loadException(array $config, XmlFileLoader $loader, ContainerBu
->replaceArgument(1, $config['exception']['debug']);
$container->getDefinition('fos_rest.serializer.flatten_exception_normalizer')
->replaceArgument(2, 'rfc7807' === $config['exception']['flatten_exception_format']);

if ($config['exception']['serializer_error_renderer']) {
$format = new Definition();
$format->setFactory([SerializerErrorRenderer::class, 'getPreferredFormat']);
$format->setArguments([
new Reference('request_stack'),
]);
$debug = new Definition();
$debug->setFactory([SerializerErrorRenderer::class, 'isDebug']);
$debug->setArguments([
new Reference('request_stack'),
'%kernel.debug%',
]);
$container->register('fos_rest.error_renderer.serializer', SerializerErrorRenderer::class)
->setArguments([
new Reference('fos_rest.serializer'),
$format,
new Reference('error_renderer.html', ContainerInterface::NULL_ON_INVALID_REFERENCE),
$debug,
]);
$container->setAlias('error_renderer.serializer', 'fos_rest.error_renderer.serializer');
}
}
}

Expand Down
95 changes: 95 additions & 0 deletions ErrorRenderer/SerializerErrorRenderer.php
@@ -0,0 +1,95 @@
<?php

/*
* This file is part of the FOSRestBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\RestBundle\ErrorRenderer;

use FOS\RestBundle\Context\Context;
use FOS\RestBundle\Serializer\Serializer;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;

/**
* @internal
*/
final class SerializerErrorRenderer implements ErrorRendererInterface
{
private $serializer;
private $format;
private $fallbackErrorRenderer;
private $debug;

/**
* @param string|callable(FlattenException) $format
* @param string|bool $debug
*/
public function __construct(Serializer $serializer, $format, ErrorRendererInterface $fallbackErrorRenderer = null, $debug = false)
{
if (!is_string($format) && !is_callable($format)) {
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)));
}

if (!is_bool($debug) && !is_callable($debug)) {
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)));
}

$this->serializer = $serializer;
$this->format = $format;
$this->fallbackErrorRenderer = $fallbackErrorRenderer;
$this->debug = $debug;
}

public function render(\Throwable $exception): FlattenException
{
$flattenException = FlattenException::createFromThrowable($exception);

try {
$format = is_callable($this->format) ? ($this->format)($flattenException) : $this->format;

$context = new Context();
$context->setAttribute('exception', $exception);
$context->setAttribute('debug', is_callable($this->debug) ? ($this->debug)($exception) : $this->debug);

return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, $context));
} catch (NotEncodableValueException $e) {
return $this->fallbackErrorRenderer->render($exception);
}
}

/**
* @see \Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer::getPreferredFormat
*/
public static function getPreferredFormat(RequestStack $requestStack): \Closure
{
return static function () use ($requestStack) {
if (!$request = $requestStack->getCurrentRequest()) {
throw new NotEncodableValueException();
}

return $request->getPreferredFormat();
};
}

/**
* @see \Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer::isDebug
*/
public static function isDebug(RequestStack $requestStack, bool $debug): \Closure
{
return static function () use ($requestStack, $debug): bool {
if (!$request = $requestStack->getCurrentRequest()) {
return $debug;
}

return $debug && $request->attributes->getBoolean('showException', true);
};
}
}
6 changes: 3 additions & 3 deletions EventListener/AccessDeniedListener.php
Expand Up @@ -67,12 +67,12 @@ public function onKernelException(ExceptionEvent $event): void
$exception = $event->getThrowable();

if ($exception instanceof AccessDeniedException) {
$exception = new AccessDeniedHttpException('You do not have the necessary permissions', $exception);
$exception = new AccessDeniedHttpException('You do not have the necessary permissions');
} elseif ($exception instanceof AuthenticationException) {
if ($this->challenge) {
$exception = new UnauthorizedHttpException($this->challenge, 'You are not authenticated', $exception);
$exception = new UnauthorizedHttpException($this->challenge, 'You are not authenticated');
} else {
$exception = new HttpException(401, 'You are not authenticated', $exception);
$exception = new HttpException(401, 'You are not authenticated');
}
}

Expand Down
55 changes: 55 additions & 0 deletions Tests/DependencyInjection/FOSRestExtensionTest.php
Expand Up @@ -20,6 +20,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;

/**
* FOSRestExtension test.
Expand Down Expand Up @@ -590,4 +591,58 @@ public function testMimeTypesArePassedArrays()
$this->container->getDefinition('fos_rest.mime_type_listener')->getArgument(0)
);
}

public function testSerializerErrorRendererNotRegisteredByDefault()
{
$config = array(
'fos_rest' => array(
'exception' => [
'exception_listener' => false,
'serialize_exceptions' => false,
],
'routing_loader' => false,
'service' => [
'templating' => null,
],
'view' => [
'default_engine' => null,
'force_redirects' => [],
],
),
);
$this->extension->load($config, $this->container);

$this->assertFalse($this->container->hasDefinition('fos_rest.error_renderer.serializer'));
$this->assertFalse($this->container->hasAlias('error_renderer.serializer'));
}

public function testRegisterSerializerErrorRenderer()
{
if (!interface_exists(ErrorRendererInterface::class)) {
$this->markTestSkipped();
}

$config = array(
'fos_rest' => array(
'exception' => [
'exception_listener' => false,
'serialize_exceptions' => false,
'serializer_error_renderer' => true,
],
'routing_loader' => false,
'service' => [
'templating' => null,
],
'view' => [
'default_engine' => null,
'force_redirects' => [],
],
),
);
$this->extension->load($config, $this->container);

$this->assertTrue($this->container->hasDefinition('fos_rest.error_renderer.serializer'));
$this->assertTrue($this->container->hasAlias('error_renderer.serializer'));
$this->assertSame('fos_rest.error_renderer.serializer', (string) $this->container->getAlias('error_renderer.serializer'));
}
}
112 changes: 112 additions & 0 deletions Tests/ErrorRenderer/SerializerErrorRendererTest.php
@@ -0,0 +1,112 @@
<?php

/*
* This file is part of the FOSRestBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\RestBundle\Tests\ErrorRenderer;

use FOS\RestBundle\Context\Context;
use FOS\RestBundle\ErrorRenderer\SerializerErrorRenderer;
use FOS\RestBundle\Serializer\Serializer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;

class SerializerErrorRendererTest extends TestCase
{
protected function setUp()
{
if (!interface_exists(ErrorRendererInterface::class)) {
$this->markTestSkipped();
}
}

public function testSerializeFlattenExceptionWithStringFormat()
{
$serializer = $this->createMock(Serializer::class);
$serializer
->expects($this->once())
->method('serialize')
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
->willReturn('serialized FlattenException');

$errorRenderer = new SerializerErrorRenderer($serializer, 'json');
$flattenException = $errorRenderer->render(new NotFoundHttpException());

$this->assertSame('serialized FlattenException', $flattenException->getAsString());
}

public function testSerializeFlattenExceptionWithCallableFormat()
{
$serializer = $this->createMock(Serializer::class);
$serializer
->expects($this->once())
->method('serialize')
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
->willReturn('serialized FlattenException');

$format = function (FlattenException $flattenException) {
return 'json';
};

$errorRenderer = new SerializerErrorRenderer($serializer, $format);
$flattenException = $errorRenderer->render(new NotFoundHttpException());

$this->assertSame('serialized FlattenException', $flattenException->getAsString());
}

public function testSerializeFlattenExceptionUsingGetPreferredFormatMethod()
{
$serializer = $this->createMock(Serializer::class);
$serializer
->expects($this->once())
->method('serialize')
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
->willReturn('serialized FlattenException');

$request = new Request();
$request->attributes->set('_format', 'json');

$requestStack = new RequestStack();
$requestStack->push($request);
$format = SerializerErrorRenderer::getPreferredFormat($requestStack);

$errorRenderer = new SerializerErrorRenderer($serializer, $format);
$flattenException = $errorRenderer->render(new NotFoundHttpException());

$this->assertSame('serialized FlattenException', $flattenException->getAsString());
}

public function testFallbackErrorRendererIsUsedWhenFormatCannotBeDetected()
{
$exception = new NotFoundHttpException();
$flattenException = new FlattenException();

$fallbackErrorRenderer = $this->createMock(ErrorRendererInterface::class);
$fallbackErrorRenderer
->expects($this->once())
->method('render')
->with($exception)
->willReturn($flattenException);

$serializer = $this->createMock(Serializer::class);
$serializer->expects($this->once())
->method('serialize')
->with($this->isInstanceOf(FlattenException::class), 'json', $this->isInstanceOf(Context::class))
->willThrowException(new NotEncodableValueException());

$errorRenderer = new SerializerErrorRenderer($serializer, 'json', $fallbackErrorRenderer);

$this->assertSame($flattenException, $errorRenderer->render($exception));
}
}

This file was deleted.

0 comments on commit dbe76a0

Please sign in to comment.