vendor/noxlogic/ratelimit-bundle/EventListener/RateLimitAnnotationListener.php line 54

Open in your IDE?
  1. <?php
  2. namespace Noxlogic\RateLimitBundle\EventListener;
  3. use Noxlogic\RateLimitBundle\Annotation\RateLimit;
  4. use Noxlogic\RateLimitBundle\Events\CheckedRateLimitEvent;
  5. use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;
  6. use Noxlogic\RateLimitBundle\Events\RateLimitEvents;
  7. use Noxlogic\RateLimitBundle\Exception\RateLimitExceptionInterface;
  8. use Noxlogic\RateLimitBundle\Service\RateLimitService;
  9. use Noxlogic\RateLimitBundle\Util\PathLimitProcessor;
  10. use Symfony\Component\EventDispatcher\EventDispatcherInterface as LegacyEventDispatcherInterface;
  11. use Symfony\Component\HttpFoundation\Request;
  12. use Symfony\Component\HttpFoundation\Response;
  13. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  14. use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
  15. use Symfony\Component\HttpKernel\HttpKernelInterface;
  16. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  17. class RateLimitAnnotationListener extends BaseListener
  18. {
  19.     /**
  20.      * @var EventDispatcherInterface | LegacyEventDispatcherInterface
  21.      */
  22.     protected $eventDispatcher;
  23.     /**
  24.      * @var \Noxlogic\RateLimitBundle\Service\RateLimitService
  25.      */
  26.     protected $rateLimitService;
  27.     /**
  28.      * @var \Noxlogic\RateLimitBundle\Util\PathLimitProcessor
  29.      */
  30.     protected $pathLimitProcessor;
  31.     /**
  32.      * @param RateLimitService                    $rateLimitService
  33.      */
  34.     public function __construct(
  35.         $eventDispatcher,
  36.         RateLimitService $rateLimitService,
  37.         PathLimitProcessor $pathLimitProcessor
  38.     ) {
  39.         $this->eventDispatcher $eventDispatcher;
  40.         $this->rateLimitService $rateLimitService;
  41.         $this->pathLimitProcessor $pathLimitProcessor;
  42.     }
  43.     /**
  44.      * @param ControllerEvent|FilterControllerEvent $event
  45.      */
  46.     public function onKernelController($event)
  47.     {
  48.         // Skip if the bundle isn't enabled (for instance in test environment)
  49.         if( ! $this->getParameter('enabled'true)) {
  50.             return;
  51.         }
  52.         // Skip if we aren't the main request
  53.         if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
  54.             return;
  55.         }
  56.         // Find the best match
  57.         $annotations $event->getRequest()->attributes->get('_x-rate-limit', array());
  58.         $rateLimit $this->findBestMethodMatch($event->getRequest(), $annotations);
  59.         // Another treatment before applying RateLimit ?
  60.         $checkedRateLimitEvent = new CheckedRateLimitEvent($event->getRequest(), $rateLimit);
  61.         $this->dispatch(RateLimitEvents::CHECKED_RATE_LIMIT$checkedRateLimitEvent);
  62.         $rateLimit $checkedRateLimitEvent->getRateLimit();
  63.         // No matching annotation found
  64.         if (! $rateLimit) {
  65.             return;
  66.         }
  67.         $key $this->getKey($event$rateLimit$annotations);
  68.         // Ratelimit the call
  69.         $rateLimitInfo $this->rateLimitService->limitRate($key);
  70.         if (! $rateLimitInfo) {
  71.             // Create new rate limit entry for this call
  72.             $rateLimitInfo $this->rateLimitService->createRate($key$rateLimit->getLimit(), $rateLimit->getPeriod());
  73.             if (! $rateLimitInfo) {
  74.                 // @codeCoverageIgnoreStart
  75.                 return;
  76.                 // @codeCoverageIgnoreEnd
  77.             }
  78.         }
  79.         // Store the current rating info in the request attributes
  80.         $request $event->getRequest();
  81.         $request->attributes->set('rate_limit_info'$rateLimitInfo);
  82.         // Reset the rate limits
  83.         if(time() >= $rateLimitInfo->getResetTimestamp()) {
  84.             $this->rateLimitService->resetRate($key);
  85.             $rateLimitInfo $this->rateLimitService->createRate($key$rateLimit->getLimit(), $rateLimit->getPeriod());
  86.             if (! $rateLimitInfo) {
  87.                 // @codeCoverageIgnoreStart
  88.                 return;
  89.                 // @codeCoverageIgnoreEnd
  90.             }
  91.         }
  92.         // When we exceeded our limit, return a custom error response
  93.         if ($rateLimitInfo->getCalls() > $rateLimitInfo->getLimit()) {
  94.             // Throw an exception if configured.
  95.             if ($this->getParameter('rate_response_exception')) {
  96.                 $class $this->getParameter('rate_response_exception');
  97.                 $e = new $class($this->getParameter('rate_response_message'), $this->getParameter('rate_response_code'));
  98.                 if ($e instanceof RateLimitExceptionInterface) {
  99.                     $e->setPayload($rateLimit->getPayload());
  100.                 }
  101.                 throw $e;
  102.             }
  103.             $message $this->getParameter('rate_response_message');
  104.             $code $this->getParameter('rate_response_code');
  105.             $event->setController(function () use ($message$code) {
  106.                 // @codeCoverageIgnoreStart
  107.                 return new Response($message$code);
  108.                 // @codeCoverageIgnoreEnd
  109.             });
  110.             $event->stopPropagation();
  111.         }
  112.     }
  113.     /**
  114.      * @param RateLimit[] $annotations
  115.      */
  116.     protected function findBestMethodMatch(Request $request, array $annotations)
  117.     {
  118.         // Empty array, check the path limits
  119.         if (count($annotations) == 0) {
  120.             return $this->pathLimitProcessor->getRateLimit($request);
  121.         }
  122.         $best_match null;
  123.         foreach ($annotations as $annotation) {
  124.             // cast methods to array, even method holds a string
  125.             $methods is_array($annotation->getMethods()) ? $annotation->getMethods() : array($annotation->getMethods());
  126.             if (in_array($request->getMethod(), $methods)) {
  127.                 $best_match $annotation;
  128.             }
  129.             // Only match "default" annotation when we don't have a best match
  130.             if (count($annotation->getMethods()) == && $best_match == null) {
  131.                 $best_match $annotation;
  132.             }
  133.         }
  134.         return $best_match;
  135.     }
  136.     /**
  137.      * @param ControllerEvent|FilterControllerEvent $event
  138.      * @param RateLimit $rateLimit
  139.      * @param array $annotations
  140.      * @return string
  141.      */
  142.     private function getKey($eventRateLimit $rateLimit, array $annotations)
  143.     {
  144.         // Let listeners manipulate the key
  145.         $keyEvent = new GenerateKeyEvent($event->getRequest(), ''$rateLimit->getPayload());
  146.         $rateLimitMethods implode('.'$rateLimit->getMethods());
  147.         $keyEvent->addToKey($rateLimitMethods);
  148.         $rateLimitAlias count($annotations) === 0
  149.             str_replace('/''.'$this->pathLimitProcessor->getMatchedPath($event->getRequest()))
  150.             : $this->getAliasForRequest($event);
  151.         $keyEvent->addToKey($rateLimitAlias);
  152.         $this->dispatch(RateLimitEvents::GENERATE_KEY$keyEvent);
  153.         return $keyEvent->getKey();
  154.     }
  155.     /**
  156.      * @param string $route
  157.      * @param ControllerEvent|FilterControllerEvent $controller
  158.      * @return mixed|string
  159.      */
  160.     private function getAliasForRequest($event)
  161.     {
  162.         if (($route $event->getRequest()->attributes->get('_route'))) {
  163.             return $route;
  164.         }
  165.         $controller $event->getController();
  166.         if (is_string($controller) && false !== strpos($controller'::')) {
  167.             $controller explode('::'$controller);
  168.         }
  169.         if (is_array($controller)) {
  170.             return str_replace('\\''.'is_string($controller[0]) ? $controller[0] : get_class($controller[0])) . '.' $controller[1];
  171.         }
  172.         if ($controller instanceof \Closure) {
  173.             return 'closure';
  174.         }
  175.         if (is_object($controller)) {
  176.             return str_replace('\\''.'get_class($controller[0]));
  177.         }
  178.         return 'other';
  179.     }
  180.     private function dispatch($eventName$event)
  181.     {
  182.         if ($this->eventDispatcher instanceof EventDispatcherInterface) {
  183.             // Symfony >= 4.3
  184.             $this->eventDispatcher->dispatch($event$eventName);
  185.         } else {
  186.             // Symfony 3.4
  187.             $this->eventDispatcher->dispatch($eventName$event);
  188.         }
  189.     }
  190. }