<?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\EventListener; 
 
use FOS\RestBundle\Decoder\DecoderProviderInterface; 
use FOS\RestBundle\FOSRestBundle; 
use FOS\RestBundle\Normalizer\ArrayNormalizerInterface; 
use FOS\RestBundle\Normalizer\Exception\NormalizationException; 
use Symfony\Component\HttpFoundation\ParameterBag; 
use Symfony\Component\HttpFoundation\Request; 
use Symfony\Component\HttpKernel\Event\RequestEvent; 
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; 
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; 
 
/** 
 * This listener handles Request body decoding. 
 * 
 * @author Lukas Kahwe Smith <smith@pooteeweet.org> 
 * 
 * @internal 
 */ 
class BodyListener 
{ 
    private $decoderProvider; 
    private $throwExceptionOnUnsupportedContentType; 
    private $defaultFormat; 
    private $arrayNormalizer; 
    private $normalizeForms; 
 
    public function __construct( 
        DecoderProviderInterface $decoderProvider, 
        bool $throwExceptionOnUnsupportedContentType = false, 
        ArrayNormalizerInterface $arrayNormalizer = null, 
        bool $normalizeForms = false 
    ) { 
        $this->decoderProvider = $decoderProvider; 
        $this->throwExceptionOnUnsupportedContentType = $throwExceptionOnUnsupportedContentType; 
        $this->arrayNormalizer = $arrayNormalizer; 
        $this->normalizeForms = $normalizeForms; 
    } 
 
    public function setDefaultFormat(?string $defaultFormat) 
    { 
        $this->defaultFormat = $defaultFormat; 
    } 
 
    /** 
     * @param RequestEvent $event 
     */ 
    public function onKernelRequest($event) 
    { 
        $request = $event->getRequest(); 
 
        if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) { 
            return; 
        } 
 
        $method = $request->getMethod(); 
        $contentType = $request->headers->get('Content-Type'); 
        $normalizeRequest = $this->normalizeForms && $this->isFormRequest($request); 
 
        if ($this->isDecodeable($request)) { 
            $format = null === $contentType 
                ? $request->getRequestFormat() 
                : $request->getFormat($contentType); 
 
            $format = $format ?: $this->defaultFormat; 
 
            $content = $request->getContent(); 
 
            if (null === $format || !$this->decoderProvider->supports($format)) { 
                if ($this->throwExceptionOnUnsupportedContentType 
                    && $this->isNotAnEmptyDeleteRequestWithNoSetContentType($method, $content, $contentType) 
                ) { 
                    throw new UnsupportedMediaTypeHttpException("Request body format '$format' not supported"); 
                } 
 
                return; 
            } 
 
            if (!empty($content)) { 
                $decoder = $this->decoderProvider->getDecoder($format); 
                $data = $decoder->decode($content); 
                if (is_array($data)) { 
                    $request->request = new ParameterBag($data); 
                    $normalizeRequest = true; 
                } else { 
                    throw new BadRequestHttpException('Invalid '.$format.' message received'); 
                } 
            } 
        } 
 
        if (null !== $this->arrayNormalizer && $normalizeRequest) { 
            $data = $request->request->all(); 
 
            try { 
                $data = $this->arrayNormalizer->normalize($data); 
            } catch (NormalizationException $e) { 
                throw new BadRequestHttpException($e->getMessage()); 
            } 
 
            $request->request = new ParameterBag($data); 
        } 
    } 
 
    private function isNotAnEmptyDeleteRequestWithNoSetContentType(string $method, $content, $contentType): bool 
    { 
        return false === ('DELETE' === $method && empty($content) && empty($contentType)); 
    } 
 
    /** 
     * Check if we should try to decode the body. 
     * 
     * @return bool 
     */ 
    protected function isDecodeable(Request $request) 
    { 
        if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH', 'DELETE'])) { 
            return false; 
        } 
 
        return !$this->isFormRequest($request); 
    } 
 
    private function isFormRequest(Request $request): bool 
    { 
        $contentTypeParts = explode(';', $request->headers->get('Content-Type')); 
 
        if (isset($contentTypeParts[0])) { 
            return in_array($contentTypeParts[0], ['multipart/form-data', 'application/x-www-form-urlencoded']); 
        } 
 
        return false; 
    } 
}