<?php
namespace Can\RestBundle\EventListener;
use Can\RestBundle\CanRestBundle;
use Can\RestBundle\Http\Exception\UnsupportedRequestFormatHttpException;
use Can\RestBundle\Http\Method;
use Can\RestBundle\Ingest\IngestConfiguration;
use Can\RestBundle\Negotiation\Format;
use Can\RestBundle\Negotiation\FormatGuesserInterface;
use Can\RestBundle\Negotiation\NegotiationConfiguration;
use Can\RestBundle\Negotiation\NoContentTypeException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
/**
* A content-negotiation listener which returns a 400 (Bad Request) error
* response if the request format is not supported.
*
* @group http
*
* @package can/rest-bundle
* @author lechecacharro <lechecacharro@gmail.com>
*/
class RequestFormatListener
{
/**
* @var IngestConfiguration
*/
protected $ingestConfiguration;
/**
* @var NegotiationConfiguration
*/
protected $negotiationConfiguration;
/**
* @var FormatGuesserInterface
*/
protected $formatGuesser;
/**
* RequestFormatListener constructor.
*
* @param IngestConfiguration $ingestConfiguration
* @param NegotiationConfiguration $negotiationConfiguration
* @param FormatGuesserInterface $formatGuesser
*/
public function __construct(IngestConfiguration $ingestConfiguration, NegotiationConfiguration $negotiationConfiguration, FormatGuesserInterface $formatGuesser)
{
$this->ingestConfiguration = $ingestConfiguration;
$this->negotiationConfiguration = $negotiationConfiguration;
$this->formatGuesser = $formatGuesser;
}
/**
* @param RequestEvent $event
*
* @throws BadRequestHttpException on malformed Content-Type
*/
public function onRequest(RequestEvent $event): void
{
if (! $event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
// Skip if not in the REST zone, or if the request has no content
if (! $request->attributes->get(CanRestBundle::ATTR_SERVICE_ZONE) ||
! $request->attributes->get(CanRestBundle::ATTR_REQUEST_CONTENT)) {
return;
}
$format = null;
try {
// Try to guess the request format from the request (most probably
// based on the Content-Type header, but maybe the guesser supports
// sniffing the request payload)
$format = $this->formatGuesser->guess($request);
} catch (NoContentTypeException $e) {
// If the request format could not be figured out for some reason
// (e.g., no Content-Type header),either fail (if the no Content-Type
// policy is STRICT) or fall-through and use the default media type
if ($this->negotiationConfiguration->isStrictNoContentTypePolicy()) {
throw new UnsupportedMediaTypeHttpException();
}
// Fall-through
}
if (null === $format) {
$format = $this->formatGuesser->getDefaultFormat();
}
$request->attributes->set(CanRestBundle::ATTR_REQUEST_FORMAT, $format);
$request->attributes->set(CanRestBundle::ATTR_REQUEST_MEDIATYPE, $format->getMediaType());
if (! $this->isSupportedFormat($request, $format)) {
$request->attributes->set(CanRestBundle::ATTR_REQUEST_SUPPORTED, false);
throw new UnsupportedRequestFormatHttpException($format->getName(), $format->getMediaType());
}
$request->attributes->set(CanRestBundle::ATTR_REQUEST_SUPPORTED, true);
}
/**
* Figures out whether the specified format is supported. Note that the
* support for some formats (e.g. support for patch formats, or support
* for ingest formats may be allowed only for some particular kinds of
* requests).
*
* @param Request $request
* @param Format $format
*
* @return bool
*/
protected function isSupportedFormat(Request $request, Format $format): bool
{
$name = $format->getName();
if ($this->negotiationConfiguration->isSupportedFormat($name)) {
return true;
}
$method = $request->getMethod();
if (Method::PATCH === $method && $this->negotiationConfiguration->isSupportedPatchFormat($name)) {
return true;
}
if ($this->ingestConfiguration->isMethodAllowed($method) &&
$this->ingestConfiguration->isMediaTypeAllowed($format->getMediaType())) {
return true;
}
return false;
}
}