Completed
Push — master ( eed00d...bda091 )
by Dmitry
10:44
created

RateLimiter::init()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 5
nc 4
nop 0
crap 3
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\filters;
9
10
use Yii;
11
use yii\base\ActionFilter;
12
use yii\web\Request;
13
use yii\web\Response;
14
use yii\web\TooManyRequestsHttpException;
15
16
/**
17
 * RateLimiter implements a rate limiting algorithm based on the [leaky bucket algorithm](http://en.wikipedia.org/wiki/Leaky_bucket).
18
 *
19
 * You may use RateLimiter by attaching it as a behavior to a controller or module, like the following,
20
 *
21
 * ```php
22
 * public function behaviors()
23
 * {
24
 *     return [
25
 *         'rateLimiter' => [
26
 *             'class' => \yii\filters\RateLimiter::className(),
27
 *         ],
28
 *     ];
29
 * }
30
 * ```
31
 *
32
 * When the user has exceeded his rate limit, RateLimiter will throw a [[TooManyRequestsHttpException]] exception.
33
 *
34
 * Note that RateLimiter requires [[user]] to implement the [[RateLimitInterface]]. RateLimiter will
35
 * do nothing if [[user]] is not set or does not implement [[RateLimitInterface]].
36
 *
37
 * @author Qiang Xue <[email protected]>
38
 * @since 2.0
39
 */
40
class RateLimiter extends ActionFilter
41
{
42
    /**
43
     * @var bool whether to include rate limit headers in the response
44
     */
45
    public $enableRateLimitHeaders = true;
46
    /**
47
     * @var string the message to be displayed when rate limit exceeds
48
     */
49
    public $errorMessage = 'Rate limit exceeded.';
50
    /**
51
     * @var RateLimitInterface the user object that implements the RateLimitInterface.
52
     * If not set, it will take the value of `Yii::$app->user->getIdentity(false)`.
53
     */
54
    public $user;
55
    /**
56
     * @var Request the current request. If not set, the `request` application component will be used.
57
     */
58
    public $request;
59
    /**
60
     * @var Response the response to be sent. If not set, the `response` application component will be used.
61
     */
62
    public $response;
63
64
65
    /**
66
     * @inheritdoc
67
     */
68 12
    public function init()
69
    {
70 12
        if ($this->request === null) {
71 11
            $this->request = Yii::$app->getRequest();
72
        }
73 12
        if ($this->response === null) {
74 11
            $this->response = Yii::$app->getResponse();
75
        }
76 12
    }
77
78
    /**
79
     * @inheritdoc
80
     */
81 3
    public function beforeAction($action)
82
    {
83 3
        if ($this->user === null && Yii::$app->getUser()) {
84 1
            $this->user = Yii::$app->getUser()->getIdentity(false);
0 ignored issues
show
Bug introduced by
The method getUser does only exist in yii\web\Application, but not in yii\console\Application.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
85
        }
86
87 3
        if ($this->user instanceof RateLimitInterface) {
88 1
            Yii::trace('Check rate limit', __METHOD__);
89 1
            $this->checkRateLimit($this->user, $this->request, $this->response, $action);
90 2
        } elseif ($this->user) {
91 1
            Yii::info('Rate limit skipped: "user" does not implement RateLimitInterface.', __METHOD__);
92
        } else {
93 1
            Yii::info('Rate limit skipped: user not logged in.', __METHOD__);
94
        }
95
96 3
        return true;
97
    }
98
99
    /**
100
     * Checks whether the rate limit exceeds.
101
     * @param RateLimitInterface $user the current user
102
     * @param Request $request
103
     * @param Response $response
104
     * @param \yii\base\Action $action the action to be executed
105
     * @throws TooManyRequestsHttpException if rate limit exceeds
106
     */
107 3
    public function checkRateLimit($user, $request, $response, $action)
108
    {
109 3
        $current = time();
110
111 3
        list ($limit, $window) = $user->getRateLimit($request, $action);
112 3
        list ($allowance, $timestamp) = $user->loadAllowance($request, $action);
113
114 3
        $allowance += (int) (($current - $timestamp) * $limit / $window);
115 3
        if ($allowance > $limit) {
116
            $allowance = $limit;
117
        }
118
119 3
        if ($allowance < 1) {
120 1
            $user->saveAllowance($request, $action, 0, $current);
121 1
            $this->addRateLimitHeaders($response, $limit, 0, $window);
122 1
            throw new TooManyRequestsHttpException($this->errorMessage);
123
        } else {
124 2
            $user->saveAllowance($request, $action, $allowance - 1, $current);
125 2
            $this->addRateLimitHeaders($response, $limit, $allowance - 1, (int) (($limit - $allowance) * $window / $limit));
126
        }
127 2
    }
128
129
    /**
130
     * Adds the rate limit headers to the response
131
     * @param Response $response
132
     * @param int $limit the maximum number of allowed requests during a period
133
     * @param int $remaining the remaining number of allowed requests within the current period
134
     * @param int $reset the number of seconds to wait before having maximum number of allowed requests again
135
     */
136 4
    public function addRateLimitHeaders($response, $limit, $remaining, $reset)
137
    {
138 4
        if ($this->enableRateLimitHeaders) {
139 3
            $response->getHeaders()
140 3
                ->set('X-Rate-Limit-Limit', $limit)
141 3
                ->set('X-Rate-Limit-Remaining', $remaining)
142 3
                ->set('X-Rate-Limit-Reset', $reset);
143
        }
144 4
    }
145
}
146