RateLimiter::beforeAction()   A
last analyzed

Complexity

Conditions 6
Paths 12

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

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

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
79
        }
80 14
        if ($this->response === null) {
81 13
            $this->response = Yii::$app->getResponse();
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::app->getResponse() can also be of type yii\console\Response. However, the property $response is declared as type null|yii\web\Response. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
82
        }
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88 5
    public function beforeAction($action)
89
    {
90 5
        if ($this->user === null && Yii::$app->getUser()) {
0 ignored issues
show
Bug introduced by
The method getUser() does not exist on yii\console\Application. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

90
        if ($this->user === null && Yii::$app->/** @scrutinizer ignore-call */ getUser()) {
Loading history...
91 2
            $this->user = Yii::$app->getUser()->getIdentity(false);
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::app->getUser()->getIdentity(false) can also be of type yii\web\IdentityInterface. However, the property $user is declared as type Closure|null|yii\filters\RateLimitInterface. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

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