<?php
/*
* This file is part of the NelmioCorsBundle.
*
* (c) Nelmio <hello@nelm.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\CorsBundle\EventListener;
use Nelmio\CorsBundle\Options\ResolverInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Adds CORS headers and handles pre-flight requests
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class CorsListener
{
const SHOULD_ALLOW_ORIGIN_ATTR = '_nelmio_cors_should_allow_origin';
const SHOULD_FORCE_ORIGIN_ATTR = '_nelmio_cors_should_force_origin';
/**
* Simple headers as defined in the spec should always be accepted
*/
protected static $simpleHeaders = [
'accept',
'accept-language',
'content-language',
'origin',
];
/** @var ResolverInterface */
protected $configurationResolver;
public function __construct(ResolverInterface $configurationResolver)
{
$this->configurationResolver = $configurationResolver;
}
public function onKernelRequest(RequestEvent $event): void
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$request = $event->getRequest();
if (!$options = $this->configurationResolver->getOptions($request)) {
return;
}
// if the "forced_allow_origin_value" option is set, add a listener which will set or override the "Access-Control-Allow-Origin" header
if (!empty($options['forced_allow_origin_value'])) {
$request->attributes->set(self::SHOULD_FORCE_ORIGIN_ATTR, true);
}
// skip if not a CORS request
if (!$request->headers->has('Origin') || $request->headers->get('Origin') === $request->getSchemeAndHttpHost()) {
return;
}
// perform preflight checks
if ('OPTIONS' === $request->getMethod() && $request->headers->has('Access-Control-Request-Method')) {
$event->setResponse($this->getPreflightResponse($request, $options));
return;
}
if (!$this->checkOrigin($request, $options)) {
return;
}
$request->attributes->set(self::SHOULD_ALLOW_ORIGIN_ATTR, true);
}
public function onKernelResponse(ResponseEvent $event): void
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$request = $event->getRequest();
$shouldAllowOrigin = $request->attributes->getBoolean(self::SHOULD_ALLOW_ORIGIN_ATTR);
$shouldForceOrigin = $request->attributes->getBoolean(self::SHOULD_FORCE_ORIGIN_ATTR);
if (!$shouldAllowOrigin && !$shouldForceOrigin) {
return;
}
if (!$options = $this->configurationResolver->getOptions($request)) {
return;
}
if ($shouldAllowOrigin) {
$response = $event->getResponse();
// add CORS response headers
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
if ($options['allow_credentials']) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}
if ($options['expose_headers']) {
$response->headers->set('Access-Control-Expose-Headers', strtolower(implode(', ', $options['expose_headers'])));
}
}
if ($shouldForceOrigin) {
$event->getResponse()->headers->set('Access-Control-Allow-Origin', $options['forced_allow_origin_value']);
}
}
protected function getPreflightResponse(Request $request, array $options): Response
{
$response = new Response();
$response->setVary(['Origin']);
if ($options['allow_credentials']) {
$response->headers->set('Access-Control-Allow-Credentials', 'true');
}
if ($options['allow_methods']) {
$response->headers->set('Access-Control-Allow-Methods', implode(', ', $options['allow_methods']));
}
if ($options['allow_headers']) {
$headers = $this->isWildcard($options, 'allow_headers')
? $request->headers->get('Access-Control-Request-Headers')
: implode(', ', $options['allow_headers']);
if ($headers) {
$response->headers->set('Access-Control-Allow-Headers', $headers);
}
}
if ($options['max_age']) {
$response->headers->set('Access-Control-Max-Age', $options['max_age']);
}
if (!$this->checkOrigin($request, $options)) {
$response->headers->remove('Access-Control-Allow-Origin');
return $response;
}
$response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin'));
// check request method
if (!in_array(strtoupper($request->headers->get('Access-Control-Request-Method')), $options['allow_methods'], true)) {
$response->setStatusCode(405);
return $response;
}
/**
* We have to allow the header in the case-set as we received it by the client.
* Firefox f.e. sends the LINK method as "Link", and we have to allow it like this or the browser will deny the
* request.
*/
if (!in_array($request->headers->get('Access-Control-Request-Method'), $options['allow_methods'], true)) {
$options['allow_methods'][] = $request->headers->get('Access-Control-Request-Method');
$response->headers->set('Access-Control-Allow-Methods', implode(', ', $options['allow_methods']));
}
// check request headers
$headers = $request->headers->get('Access-Control-Request-Headers');
if ($headers && !$this->isWildcard($options, 'allow_headers')) {
$headers = strtolower(trim($headers));
foreach (preg_split('{, *}', $headers) as $header) {
if (in_array($header, self::$simpleHeaders, true)) {
continue;
}
if (!in_array($header, $options['allow_headers'], true)) {
$sanitizedMessage = htmlentities('Unauthorized header '.$header, ENT_QUOTES, 'UTF-8');
$response->setStatusCode(400);
$response->setContent($sanitizedMessage);
break;
}
}
}
return $response;
}
protected function checkOrigin(Request $request, array $options): bool
{
// check origin
$origin = $request->headers->get('Origin');
if ($this->isWildcard($options, 'allow_origin')) {
return true;
}
if ($options['origin_regex'] === true) {
// origin regex matching
foreach ($options['allow_origin'] as $originRegexp) {
if (preg_match('{'.$originRegexp.'}i', $origin)) {
return true;
}
}
} else {
// old origin matching
if (in_array($origin, $options['allow_origin'])) {
return true;
}
}
return false;
}
private function isWildcard(array $options, string $option): bool
{
return $options[$option] === true || (is_array($options[$option]) && in_array('*', $options[$option]));
}
}