vendor/nelmio/cors-bundle/EventListener/CorsListener.php line 59

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the NelmioCorsBundle.
  4.  *
  5.  * (c) Nelmio <hello@nelm.io>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Nelmio\CorsBundle\EventListener;
  11. use Nelmio\CorsBundle\Options\ResolverInterface;
  12. use Psr\Log\LoggerInterface;
  13. use Psr\Log\NullLogger;
  14. use Symfony\Component\HttpFoundation\Request;
  15. use Symfony\Component\HttpFoundation\Response;
  16. use Symfony\Component\HttpKernel\Event\RequestEvent;
  17. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  18. use Symfony\Component\HttpKernel\HttpKernelInterface;
  19. /**
  20.  * Adds CORS headers and handles pre-flight requests
  21.  *
  22.  * @author Jordi Boggiano <j.boggiano@seld.be>
  23.  */
  24. class CorsListener
  25. {
  26.     const SHOULD_ALLOW_ORIGIN_ATTR '_nelmio_cors_should_allow_origin';
  27.     const SHOULD_FORCE_ORIGIN_ATTR '_nelmio_cors_should_force_origin';
  28.     /**
  29.      * Simple headers as defined in the spec should always be accepted
  30.      */
  31.     protected static $simpleHeaders = [
  32.         'accept',
  33.         'accept-language',
  34.         'content-language',
  35.         'origin',
  36.     ];
  37.     /** @var ResolverInterface */
  38.     protected $configurationResolver;
  39.     /** @var LoggerInterface */
  40.     private $logger;
  41.     public function __construct(ResolverInterface $configurationResolver, ?LoggerInterface $logger null)
  42.     {
  43.         $this->configurationResolver $configurationResolver;
  44.         if (null === $logger) {
  45.             $logger = new NullLogger();
  46.         }
  47.         $this->logger $logger;
  48.     }
  49.     public function onKernelRequest(RequestEvent $event): void
  50.     {
  51.         if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
  52.             $this->logger->debug('Not a master type request, skipping CORS checks.');
  53.             return;
  54.         }
  55.         $request $event->getRequest();
  56.         if (!$options $this->configurationResolver->getOptions($request)) {
  57.             $this->logger->debug('Could not get options for request, skipping CORS checks.');
  58.             return;
  59.         }
  60.         // if the "forced_allow_origin_value" option is set, add a listener which will set or override the "Access-Control-Allow-Origin" header
  61.         if (!empty($options['forced_allow_origin_value'])) {
  62.             $this->logger->debug(sprintf(
  63.                 "The 'forced_allow_origin_value' option is set to '%s', adding a listener to set or override the 'Access-Control-Allow-Origin' header.",
  64.                 $options['forced_allow_origin_value']
  65.             ));
  66.             $request->attributes->set(self::SHOULD_FORCE_ORIGIN_ATTRtrue);
  67.         }
  68.         // skip if not a CORS request
  69.         if (!$request->headers->has('Origin')) {
  70.             $this->logger->debug("Request does not have 'Origin' header, skipping CORS.");
  71.             return;
  72.         }
  73.         if ($options['skip_same_as_origin'] && $request->headers->get('Origin') === $request->getSchemeAndHttpHost()) {
  74.             $this->logger->debug("The 'Origin' header of the request equals the scheme and host the request was sent to, skipping CORS.");
  75.             return;
  76.         }
  77.         // perform preflight checks
  78.         if ('OPTIONS' === $request->getMethod() && $request->headers->has('Access-Control-Request-Method')) {
  79.             $this->logger->debug("Request is a preflight check, setting event response now.");
  80.             $event->setResponse($this->getPreflightResponse($request$options));
  81.             return;
  82.         }
  83.         if (!$this->checkOrigin($request$options)) {
  84.             $this->logger->debug("Origin check failed.");
  85.             return;
  86.         }
  87.         $this->logger->debug("Origin is allowed, proceed with adding CORS response headers.");
  88.         $request->attributes->set(self::SHOULD_ALLOW_ORIGIN_ATTRtrue);
  89.     }
  90.     public function onKernelResponse(ResponseEvent $event): void
  91.     {
  92.         if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
  93.             $this->logger->debug("Not a master type request, skip adding CORS response headers.");
  94.             return;
  95.         }
  96.         $request $event->getRequest();
  97.         $shouldAllowOrigin $request->attributes->getBoolean(self::SHOULD_ALLOW_ORIGIN_ATTR);
  98.         $shouldForceOrigin $request->attributes->getBoolean(self::SHOULD_FORCE_ORIGIN_ATTR);
  99.         if (!$shouldAllowOrigin && !$shouldForceOrigin) {
  100.             $this->logger->debug("The origin should not be allowed and not be forced, skip adding CORS response headers.");
  101.             return;
  102.         }
  103.         if (!$options $this->configurationResolver->getOptions($request)) {
  104.             $this->logger->debug("Could not resolve options for request, skip adding CORS response headers.");
  105.             return;
  106.         }
  107.         if ($shouldAllowOrigin) {
  108.             $response $event->getResponse();
  109.             // add CORS response headers
  110.             $origin $request->headers->get('Origin');
  111.             $this->logger->debug(sprintf("Setting 'Access-Control-Allow-Origin' response header to '%s'."$origin));
  112.             $response->headers->set('Access-Control-Allow-Origin'$origin);
  113.             if ($options['allow_credentials']) {
  114.                 $this->logger->debug("Setting 'Access-Control-Allow-Credentials' to 'true'.");
  115.                 $response->headers->set('Access-Control-Allow-Credentials''true');
  116.             }
  117.             if ($options['expose_headers']) {
  118.                 $headers strtolower(implode(', '$options['expose_headers']));
  119.                 $this->logger->debug(sprintf("Setting 'Access-Control-Expose-Headers' response header to '%s'."$headers));
  120.                 $response->headers->set('Access-Control-Expose-Headers'$headers);
  121.             }
  122.         }
  123.         if ($shouldForceOrigin) {
  124.             $this->logger->debug(sprintf("Setting 'Access-Control-Allow-Origin' response header to '%s'."$options['forced_allow_origin_value']));
  125.             $event->getResponse()->headers->set('Access-Control-Allow-Origin'$options['forced_allow_origin_value']);
  126.         }
  127.     }
  128.     protected function getPreflightResponse(Request $request, array $options): Response
  129.     {
  130.         $response = new Response();
  131.         $response->setVary(['Origin']);
  132.         if ($options['allow_credentials']) {
  133.             $this->logger->debug("Setting 'Access-Control-Allow-Credentials' response header to 'true'.");
  134.             $response->headers->set('Access-Control-Allow-Credentials''true');
  135.         }
  136.         if ($options['allow_methods']) {
  137.             $methods implode(', '$options['allow_methods']);
  138.             $this->logger->debug(sprintf("Setting 'Access-Control-Allow-Methods' response header to '%s'."$methods));
  139.             $response->headers->set('Access-Control-Allow-Methods'$methods);
  140.         }
  141.         if ($options['allow_headers']) {
  142.             $headers $this->isWildcard($options'allow_headers')
  143.                 ? $request->headers->get('Access-Control-Request-Headers')
  144.                 : implode(', '$options['allow_headers']);
  145.             if ($headers) {
  146.                 $this->logger->debug(sprintf("Setting 'Access-Control-Allow-Headers' response header to '%s'."$headers));
  147.                 $response->headers->set('Access-Control-Allow-Headers'$headers);
  148.             }
  149.         }
  150.         if ($options['max_age']) {
  151.             $this->logger->debug(sprintf("Setting 'Access-Control-Max-Age' response header to '%d'."$options['max_age']));
  152.             $response->headers->set('Access-Control-Max-Age'$options['max_age']);
  153.         }
  154.         if (!$this->checkOrigin($request$options)) {
  155.             $this->logger->debug("Removing 'Access-Control-Allow-Origin' response header.");
  156.             $response->headers->remove('Access-Control-Allow-Origin');
  157.             return $response;
  158.         }
  159.         $origin $request->headers->get('Origin');
  160.         $this->logger->debug(sprintf("Setting 'Access-Control-Allow-Origin' response header to '%s'"$origin));
  161.         $response->headers->set('Access-Control-Allow-Origin'$origin);
  162.         // check request method
  163.         $method strtoupper($request->headers->get('Access-Control-Request-Method'));
  164.         if (!in_array($method$options['allow_methods'], true)) {
  165.             $this->logger->debug(sprintf("Method '%s' is not allowed."$method));
  166.             $response->setStatusCode(405);
  167.             return $response;
  168.         }
  169.         /**
  170.          * We have to allow the header in the case-set as we received it by the client.
  171.          * Firefox f.e. sends the LINK method as "Link", and we have to allow it like this or the browser will deny the
  172.          * request.
  173.          */
  174.         if (!in_array($request->headers->get('Access-Control-Request-Method'), $options['allow_methods'], true)) {
  175.             $options['allow_methods'][] = $request->headers->get('Access-Control-Request-Method');
  176.             $response->headers->set('Access-Control-Allow-Methods'implode(', '$options['allow_methods']));
  177.         }
  178.         // check request headers
  179.         $headers $request->headers->get('Access-Control-Request-Headers');
  180.         if ($headers && !$this->isWildcard($options'allow_headers')) {
  181.             $headers strtolower(trim($headers));
  182.             foreach (preg_split('{, *}'$headers) as $header) {
  183.                 if (in_array($headerself::$simpleHeaderstrue)) {
  184.                     continue;
  185.                 }
  186.                 if (!in_array($header$options['allow_headers'], true)) {
  187.                     $sanitizedMessage htmlentities('Unauthorized header '.$headerENT_QUOTES'UTF-8');
  188.                     $response->setStatusCode(400);
  189.                     $response->setContent($sanitizedMessage);
  190.                     break;
  191.                 }
  192.             }
  193.         }
  194.         return $response;
  195.     }
  196.     protected function checkOrigin(Request $request, array $options): bool
  197.     {
  198.         // check origin
  199.         $origin $request->headers->get('Origin');
  200.         if ($this->isWildcard($options'allow_origin')) {
  201.             return true;
  202.         }
  203.         if ($options['origin_regex'] === true) {
  204.             // origin regex matching
  205.             foreach ($options['allow_origin'] as $originRegexp) {
  206.                 $this->logger->debug(sprintf("Matching origin regex '%s' to origin '%s'."$originRegexp$origin));
  207.                 if (preg_match('{'.$originRegexp.'}i'$origin)) {
  208.                     $this->logger->debug(sprintf("Origin regex '%s' matches origin '%s'."$originRegexp$origin));
  209.                     return true;
  210.                 }
  211.             }
  212.         } else {
  213.             // old origin matching
  214.             if (in_array($origin$options['allow_origin'])) {
  215.                 $this->logger->debug(sprintf("Origin '%s' is allowed."$origin));
  216.                 return true;
  217.             }
  218.         }
  219.         $this->logger->debug(sprintf("Origin '%s' is not allowed."$origin));
  220.         return false;
  221.     }
  222.     private function isWildcard(array $optionsstring $option): bool
  223.     {
  224.         $result $options[$option] === true || (is_array($options[$option]) && in_array('*'$options[$option]));
  225.         $this->logger->debug(sprintf("Option '%s' is %s a wildcard."$option$result '' 'not'));
  226.         return $result;
  227.     }
  228. }