Completed
Push — redis-bugfix ( cdc9a6...7beae9 )
by Joshua
19:32 queued 09:30
created

RateLimitAnnotationListener::dispatch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 0
cts 0
cp 0
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 6
1
<?php
2
3
namespace Noxlogic\RateLimitBundle\EventListener;
4
5
use Noxlogic\RateLimitBundle\Annotation\RateLimit;
6
use Noxlogic\RateLimitBundle\Events\ProxyFilterControllerEvent;
7
use Noxlogic\RateLimitBundle\Events\CheckedRateLimitEvent;
8
use Noxlogic\RateLimitBundle\Events\GenerateKeyEvent;
9
use Noxlogic\RateLimitBundle\Events\RateLimitEvents;
10
use Noxlogic\RateLimitBundle\Exception\RateLimitExceptionInterface;
11
use Noxlogic\RateLimitBundle\Service\RateLimitService;
12
use Noxlogic\RateLimitBundle\Util\PathLimitProcessor;
13
use Symfony\Component\EventDispatcher\EventDispatcherInterface as LegacyEventDispatcherInterface;
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\HttpFoundation\Response;
16
use Symfony\Component\HttpKernel\HttpKernelInterface;
17
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
18
19
class RateLimitAnnotationListener extends BaseListener
20
{
21
22
    /**
23
     * @var EventDispatcherInterface | LegacyEventDispatcherInterface
24
     */
25
    protected $eventDispatcher;
26
27
    /**
28
     * @var \Noxlogic\RateLimitBundle\Service\RateLimitService
29
     */
30
    protected $rateLimitService;
31
32
    /**
33
     * @var \Noxlogic\RateLimitBundle\Util\PathLimitProcessor
34
     */
35
    protected $pathLimitProcessor;
36
37
    /**
38
     * @param RateLimitService                    $rateLimitService
39
     */
40 19
    public function __construct(
41
        $eventDispatcher,
42
        RateLimitService $rateLimitService,
43
        PathLimitProcessor $pathLimitProcessor
44
    ) {
45 19
        $this->eventDispatcher = $eventDispatcher;
46 19
        $this->rateLimitService = $rateLimitService;
47 19
        $this->pathLimitProcessor = $pathLimitProcessor;
48 19
    }
49
50
    /**
51
     * @param ProxyFilterControllerEvent $event
52
     */
53 15
    public function onKernelController(ProxyFilterControllerEvent $event)
54
    {
55
        // Skip if the bundle isn't enabled (for instance in test environment)
56 15
        if( ! $this->getParameter('enabled', true)) {
57 1
            return;
58
        }
59
60
        // Skip if we aren't the main request
61 14
        if ($event->getRequestType() != HttpKernelInterface::MASTER_REQUEST) {
62 1
            return;
63
        }
64
65
        // Find the best match
66 13
        $annotations = $event->getRequest()->attributes->get('_x-rate-limit', array());
67 13
        $rateLimit = $this->findBestMethodMatch($event->getRequest(), $annotations);
68
69
        // Another treatment before applying RateLimit ?
70 13
        $checkedRateLimitEvent = new CheckedRateLimitEvent($event->getRequest(), $rateLimit);
71 13
        $this->dispatch(RateLimitEvents::CHECKED_RATE_LIMIT, $checkedRateLimitEvent);
72 13
        $rateLimit = $checkedRateLimitEvent->getRateLimit();
73
74
        // No matching annotation found
75 13
        if (! $rateLimit) {
76 3
            return;
77
        }
78
79 10
        $key = $this->getKey($event, $rateLimit, $annotations);
80
81
        // Ratelimit the call
82 10
        $rateLimitInfo = $this->rateLimitService->limitRate($key);
83 10
        if (! $rateLimitInfo) {
84
            // Create new rate limit entry for this call
85 5
            $rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
86 5
            if (! $rateLimitInfo) {
87
                // @codeCoverageIgnoreStart
88
                return;
89
                // @codeCoverageIgnoreEnd
90
            }
91
        }
92
93
94
        // Store the current rating info in the request attributes
95 9
        $request = $event->getRequest();
96 9
        $request->attributes->set('rate_limit_info', $rateLimitInfo);
97
98
        // Reset the rate limits
99 9
        if(time() >= $rateLimitInfo->getResetTimestamp()) {
100 1
            $this->rateLimitService->resetRate($key);
101 1
            $rateLimitInfo = $this->rateLimitService->createRate($key, $rateLimit->getLimit(), $rateLimit->getPeriod());
102 1
            if (! $rateLimitInfo) {
103
                // @codeCoverageIgnoreStart
104
                return;
105
                // @codeCoverageIgnoreEnd
106
            }
107
        }
108
109
        // When we exceeded our limit, return a custom error response
110 9
        if ($rateLimitInfo->getCalls() > $rateLimitInfo->getLimit()) {
111
112
            // Throw an exception if configured.
113 5
            if ($this->getParameter('rate_response_exception')) {
114 2
                $class = $this->getParameter('rate_response_exception');
115
116 2
                $e = new $class($this->getParameter('rate_response_message'), $this->getParameter('rate_response_code'));
117
118 2
                if ($e instanceof RateLimitExceptionInterface) {
119 1
                    $e->setPayload($rateLimit->getPayload());
120
                }
121
122 2
                throw $e;
123
            }
124
125 3
            $message = $this->getParameter('rate_response_message');
126 3
            $code = $this->getParameter('rate_response_code');
127 3
            $event->setController(function () use ($message, $code) {
128
                // @codeCoverageIgnoreStart
129
                return new Response($message, $code);
130
                // @codeCoverageIgnoreEnd
131 3
            });
132 3
            $event->stopPropagation();
0 ignored issues
show
Deprecated Code introduced by
The method Symfony\Component\EventD...vent::stopPropagation() has been deprecated with message: since Symfony 4.3, use "Symfony\Contracts\EventDispatcher\Event" instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
133
        }
134
135 7
    }
136
137
138
    /**
139
     * @param RateLimit[] $annotations
140
     */
141 17
    protected function findBestMethodMatch(Request $request, array $annotations)
142
    {
143
        // Empty array, check the path limits
144 17
        if (count($annotations) == 0) {
145 5
            return $this->pathLimitProcessor->getRateLimit($request);
146
        }
147
148 12
        $best_match = null;
149 12
        foreach ($annotations as $annotation) {
150
            // cast methods to array, even method holds a string
151 12
            $methods = is_array($annotation->getMethods()) ? $annotation->getMethods() : array($annotation->getMethods());
152
153 12
            if (in_array($request->getMethod(), $methods)) {
154 3
                $best_match = $annotation;
155
            }
156
157
            // Only match "default" annotation when we don't have a best match
158 12
            if (count($annotation->getMethods()) == 0 && $best_match == null) {
159 12
                $best_match = $annotation;
160
            }
161
        }
162
163 12
        return $best_match;
164
    }
165
166 10
    private function getKey(ProxyFilterControllerEvent $event, RateLimit $rateLimit, array $annotations)
167
    {
168
        // Let listeners manipulate the key
169 10
        $keyEvent = new GenerateKeyEvent($event->getRequest(), '', $rateLimit->getPayload());
170
171 10
        $rateLimitMethods = implode('.', $rateLimit->getMethods());
172 10
        $keyEvent->addToKey($rateLimitMethods);
173
174 10
        $rateLimitAlias = count($annotations) === 0
175 1
            ? str_replace('/', '.', $this->pathLimitProcessor->getMatchedPath($event->getRequest()))
176 10
            : $this->getAliasForRequest($event);
177 10
        $keyEvent->addToKey($rateLimitAlias);
178
179 10
        $this->dispatch(RateLimitEvents::GENERATE_KEY, $keyEvent);
180
181 10
        return $keyEvent->getKey();
182
    }
183
184 9
    private function getAliasForRequest(ProxyFilterControllerEvent $event)
185
    {
186 9
        if (($route = $event->getRequest()->attributes->get('_route'))) {
187
            return $route;
188
        }
189
190 9
        $controller = $event->getController();
191
192 9
        if (is_string($controller) && false !== strpos($controller, '::')) {
193
            $controller = explode('::', $controller);
194
        }
195
196 9
        if (is_array($controller)) {
197 9
            return str_replace('\\', '.', is_string($controller[0]) ? $controller[0] : get_class($controller[0])) . '.' . $controller[1];
198
        }
199
200 1
        if ($controller instanceof \Closure) {
201 1
            return 'closure';
202
        }
203
204
        if (is_object($controller)) {
205
            return str_replace('\\', '.', get_class($controller[0]));
206
        }
207
208
        return 'other';
209
    }
210
211
    private function dispatch($eventName, $event)
212
    {
213
        if ($this->eventDispatcher instanceof EventDispatcherInterface) {
214
            // Symfony >= 4.3
215
            $this->eventDispatcher->dispatch($event, $eventName);
216
        } else {
217
            // Symfony 3.4
218
            $this->eventDispatcher->dispatch($eventName, $event);
219
        }
220
    }
221
222
}
223