<?php
namespace Can\RestBundle\EventListener;
use Can\RestBundle\CanRestBundle;
use Can\RestBundle\Cors\CorsConfiguration;
use Can\RestBundle\Cors\CorsConfigurationFactory;
use Can\RestBundle\Cors\CorsHeader;
use Can\RestBundle\Cors\CorsUtil;
use Can\RestBundle\Cors\Matcher\ChainMatcher;
use Can\RestBundle\Cors\OriginMatcherInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
/**
* Handles pre-flighted requests.
*
* @group cors
*
* @package can/rest-bundle
* @author lechecacharro <lechecacharro@gmail.com>
*/
class PreFlightRequestListener extends CorsListener
{
/**
* @var EventDispatcherInterface
*/
private $dispatcher;
/**
* PreFlightRequestListener constructor.
*
* @param CorsConfigurationFactory $factory
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(CorsConfigurationFactory $factory, EventDispatcherInterface $dispatcher)
{
parent::__construct($factory);
$this->dispatcher = $dispatcher;
}
/**
* @param RequestEvent $event
*/
public function onRequest(RequestEvent $event): void
{
if (! $event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
$configuration = $this->getConfiguration($request);
if (! $configuration->isEnabled()) {
return;
}
// Skip if not a CORS request -- this includes requests not
// specifying an "Origin" header
if (! CorsUtil::isCrossOrigin($request)) {
return;
}
$request->attributes->set(CanRestBundle::ATTR_CORS_ALLOWED, true);
$request->attributes->set(CanRestBundle::ATTR_CORS_XORIGIN, true);
// If the "force_allow_origin" option is set, then add a listener
// which will set or override the "Access-Control-Allow-Origin" header
if (! empty($configuration->getForceAllowOrigin())) {
$this->dispatcher->addListener('kernel.response', [CorsForceAllowOriginListener::class, 'onResponse'], -1);
}
// Pre-flight checks
if (CorsUtil::isPreFlight($request)) {
$preflightResponse = $this->getPreFlightResponse($request, $configuration);
$event->setResponse($preflightResponse);
return;
}
if (! $this->isAllowedOrigin($request, $configuration)) {
$request->attributes->set(CanRestBundle::ATTR_CORS_ALLOWED, false);
return;
}
}
/**
* @param CorsConfiguration $configuration
*
* @return OriginMatcherInterface
*/
private function createOriginMatcher(CorsConfiguration $configuration): OriginMatcherInterface
{
return ChainMatcher::create($configuration->getAllowOrigins());
}
/**
* @param Request $request
* @param CorsConfiguration $configuration
*
* @return bool
*/
private function isAllowedOrigin(Request $request, CorsConfiguration $configuration): bool
{
$origin = $request->headers->get('Origin');
return $this->createOriginMatcher($configuration)->matches($origin);
}
/**
* @param Request $request
* @param CorsConfiguration $configuration
*
* @return Response
*/
private function getPreFlightResponse(Request $request, CorsConfiguration $configuration): Response
{
$response = new Response();
if ($configuration->isAllowCredentials()) {
$response->headers->set(CorsHeader::ACCESS_CONTROL_ALLOW_CREDENTIALS, 'true');
}
if ($configuration->getAllowMethods()) {
$response->headers->set(CorsHeader::ACCESS_CONTROL_ALLOW_METHODS, implode(', ', $configuration->getAllowMethods()));
}
if (count($configuration->getAllowHeaders())) {
if ($configuration->isAllHeadersAllowed()) {
$headers = $request->headers->get(CorsHeader::ACCESS_CONTROL_REQUEST_HEADERS);
} else {
$headers = implode(', ', $configuration->getAllowHeaders());
}
if ($headers) {
$response->headers->set(CorsHeader::ACCESS_CONTROL_ALLOW_HEADERS, $headers);
}
}
if ($configuration->getMaxAge()) {
$response->headers->set(CorsHeader::ACCESS_CONTROL_MAX_AGE, $configuration->getMaxAge());
}
if (! $this->isAllowedOrigin($request, $configuration)) {
$response->headers->set(CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN, 'null');
return $response;
}
$response->headers->set(CorsHeader::ACCESS_CONTROL_ALLOW_ORIGIN, $request->headers->get('Origin'));
$method = $request->headers->get(CorsHeader::ACCESS_CONTROL_REQUEST_METHOD);
// If the method is not allowed, then use the HTTP response code
// 405 ("Method Not Allowed")
if (! $configuration->isMethodAllowed($method)) {
$response->setStatusCode(405);
return $response;
}
// We have to allow the header in the case-set as we received it by
// the client. Firefox e.g. sends the LINK method as "Link", and we
// have to allow it like this or the browser will deny the request
if (! in_array($method, $configuration->getAllowMethods(), true)) {
$configuration->allowMethod($method);
$response->headers->set(CorsHeader::ACCESS_CONTROL_ALLOW_METHODS, implode(', ', $configuration->getAllowMethods()));
}
// Check request headers
$headers = $request->headers->get(CorsHeader::ACCESS_CONTROL_REQUEST_HEADERS);
if ($configuration->isAllHeadersAllowed() && $headers) {
$headers = trim(strtolower($headers));
foreach (preg_split('/, */', $headers) as $header) {
if (CorsUtil::isSimpleRequestHeader($header)) {
continue;
}
// If the header is not allowed, then use the HTTP response
// code 400 ("Bad Request")
if (! $configuration->isHeaderAllowed($header)) {
$response->setStatusCode(400);
$response->setContent('Unauthorized header '. $header);
break;
}
}
}
return $response;
}
}