Passed
Push — master ( 6d1806...1466fb )
by Vince
01:34
created

limiter::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 8
rs 10
1
<?php
2
/**
3
 * ==================================
4
 * Responsible PHP API
5
 * ==================================
6
 *
7
 * @link Git https://github.com/vince-scarpa/responsibleAPI.git
8
 *
9
 * @api Responible API
10
 * @package responsible\core\throttle
11
 *
12
 * @author Vince scarpa <[email protected]>
13
 *
14
 */
15
namespace responsible\core\throttle;
16
17
use responsible\core\exception;
18
use responsible\core\throttle;
19
use responsible\core\user;
20
21
class limiter
22
{
23
    /**
24
     * [$capacity Bucket volume]
25
     * @var integer
26
     */
27
    private $capacity = 100;
28
29
    /**
30
     * [$leakRate Constant rate at which the bucket will leak]
31
     * @var string|integer
32
     */
33
    private $leakRate = 1;
34
35
    /**
36
     * [$unpacked]
37
     */
38
    private $unpacked;
39
40
    /**
41
     * [$packed]
42
     * @var string
43
     */
44
    private $packed;
45
46
    /**
47
     * [$bucket]
48
     */
49
    private $bucket;
50
51
    /**
52
     * [$account User account object]
53
     */
54
    private $account;
55
56
    /**
57
     * [$options Responisble options]
58
     */
59
    private $options;
60
61
    /**
62
     * [$timeframe Durations are in seconds]
63
     * @var array
64
     */
65
    private static $timeframe = [
66
        'SECOND' => 1,
67
        'MINUTE' => 60,
68
        'HOUR' => 3600,
69
        'DAY' => 86400,
70
        'CUSTOM' => 0,
71
    ];
72
73
    /**
74
     * [$window Timeframe window]
75
     * @var integer|string
76
     */
77
    private $window = 'MINUTE';
78
79
    /**
80
     * [$unlimited Rate limiter bypass if true]
81
     * @var boolean
82
     */
83
    private $unlimited = false;
84
85
    /**
86
     * [$scope Set the default scope]
87
     * @var string
88
     */
89
    private $scope = 'private';
90
91
    public function __construct($limit = null, $rate = null)
92
    {
93
        if (!is_null($limit)) {
94
            $this->capacity = $limit;
95
        }
96
97
        if (!is_null($rate)) {
98
            $this->window = $rate;
99
        }
100
    }
101
102
    /**
103
     * [setupOptions Set any Responsible API options]
104
     * @return self
105
     */
106
    public function setupOptions()
107
    {
108
        $options = $this->getOptions();
109
110
        $this->setCapacity($options);
111
112
        $this->setTimeframe($options);
113
114
        $this->setLeakRate($options);
115
116
        $this->setUnlimited($options);
117
118
        if (isset($this->account->scope) &&
119
            ($this->account->scope == 'anonymous' || $this->account->scope == 'public')
120
        ) {
121
            $this->scope = $this->account->scope;
122
        }
123
124
        return $this;
125
    }
126
127
    /**
128
     * [throttleRequest Build the Responsible API throttle]
129
     * @return boolean|void
130
     */
131
    public function throttleRequest()
132
    {
133
        if ($this->isUnlimited() || $this->scope !== 'private') {
134
            return true;
135
        }
136
137
        $account = $this->getAccount();
138
139
        if (empty($account)) {
140
            return false;
141
        }
142
143
        /**
144
         * [$unpack Unpack the account bucket data]
145
         */
146
        $this->unpacked = (new throttle\tokenPack)->unpack(
147
            $account->bucket
148
        );
149
        if (empty($this->unpacked)) {
150
            $this->unpacked = array(
151
                'drops' => 1,
152
                'time' => $account->access,
153
            );
154
        }
155
156
        $this->bucket = (new throttle\tokenBucket())
157
            ->setTimeframe($this->getTimeframe())
158
            ->setCapacity($this->getCapacity())
159
            ->setLeakRate($this->getLeakRate())
160
            ->pour($this->unpacked['drops'], $this->unpacked['time'])
161
        ;
162
163
        /**
164
         * Check if the bucket still has capacity to fill
165
         */
166
        if ($this->bucket->capacity()) {
167
            $this->bucket->pause(false);
168
            $this->bucket->fill();
169
        } else {
170
            if ($this->getLeakRate() <= 0) {
171
                if ($this->unpacked['pauseAccess'] == false) {
172
                    $this->bucket->pause(true);
173
                    $this->save();
174
                }
175
176
                if ($this->bucket->refill($account->access)) {
177
                    $this->save();
178
                }
179
            }
180
181
            (new exception\errorException)->error('TOO_MANY_REQUESTS');
182
        }
183
184
        $this->save();
185
    }
186
187
    /**
188
     * [updateBucket Store the buckets token data and user access time]
189
     * @return void
190
     */
191
    private function save()
192
    {
193
        $this->packed = (new throttle\tokenPack)->pack(
194
            $this->bucket->getTokenData()
195
        );
196
197
        /**
198
         * [Update account access]
199
         */
200
        (new user\user)
201
            ->setAccountID($this->getAccount()->account_id)
202
            ->setBucketToken($this->packed)
203
            ->updateAccountAccess()
204
        ;
205
    }
206
207
    /**
208
     * [getThrottle Return a list of the throttled results]
209
     * @return array
210
     */
211
    public function getThrottle()
212
    {
213
        if ($this->isUnlimited() || $this->scope !== 'private') {
214
            return array(
215
                'unlimited' => true,
216
            );
217
        }
218
219
        $windowFrame = (is_string($this->getTimeframe()))
220
        ? $this->getTimeframe()
221
        : $this->getTimeframe() . 'secs'
222
        ;
223
224
        if (is_null($this->bucket)) {
225
            return;
226
        }
227
228
        return array(
229
            'limit' => $this->getCapacity(),
230
            'leakRate' => $this->getLeakRate(),
231
            'leak' => $this->bucket->getLeakage(),
232
            'lastAccess' => $this->getLastAccessDate(),
233
            'description' => $this->getCapacity() . ' requests per ' . $windowFrame,
234
            'bucket' => $this->bucket->getTokenData(),
235
        );
236
    }
237
238
    /**
239
     * [getLastAccessDate Get the last recorded access in date format]
240
     * @return string
241
     */
242
    private function getLastAccessDate()
243
    {
244
        if (isset($this->bucket->getTokenData()['time'])) {
245
            return date('m/d/y h:i:sa', $this->bucket->getTokenData()['time']);
246
        }
247
248
        return 'Can\'t be converted';
249
    }
250
251
    /**
252
     * [setAccount Set the requests account]
253
     * @return self
254
     */
255
    public function setAccount($account)
256
    {
257
        $this->account = $account;
258
        return $this;
259
    }
260
261
    /**
262
     * [getAccount Get the requests account]
263
     */
264
    public function getAccount()
265
    {
266
        if (is_null($this->account)) {
267
            (new exception\errorException)
268
                ->setOptions($this->getOptions())
269
                ->error('UNAUTHORIZED');
270
            return;
271
        }
272
273
        return $this->account;
274
    }
275
276
    /**
277
     * [options Responsible API options]
278
     * @param array $options
279
     */
280
    public function options($options)
281
    {
282
        $this->options = $options;
283
        return $this;
284
    }
285
286
    /**
287
     * [getOptions Get the stored Responsible API options]
288
     * @return array
289
     */
290
    private function getOptions()
291
    {
292
        return $this->options;
293
    }
294
295
    /**
296
     * [hasOptionProperty Check if an option property is set]
297
     * @param  array  $options
298
     * @param  string  $property
299
     * @return string|integer|boolean
300
     */
301
    private function hasOptionProperty(array $options, $property, $default = false)
302
    {
303
        $val = isset($options[$property]) ? $options[$property] : $default;
304
305
        if ($val && empty($options[$property])) {
306
            $val = $default;
307
        }
308
309
        return $val;
310
    }
311
312
    /**
313
     * [setCapacity Set the buckets capacity]
314
     * @param array $options
315
     */
316
    public function setCapacity($options)
317
    {
318
        $hasCapacityOption = $this->hasOptionProperty($options, 'rateLimit');
319
320
        if ($hasCapacityOption) {
321
            if (!is_integer($hasCapacityOption) || empty($hasCapacityOption)) {
322
                $hasCapacityOption = false;
323
            }
324
        }
325
326
        $this->capacity = ($hasCapacityOption) ? $hasCapacityOption : $this->capacity;
0 ignored issues
show
Documentation Bug introduced by
It seems like $hasCapacityOption ? $ha...ption : $this->capacity can also be of type string. However, the property $capacity is declared as type integer. 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...
327
    }
328
329
    /**
330
     * [getCapacity Get the buckets capacity]
331
     * @return integer
332
     */
333
    public function getCapacity()
334
    {
335
        return $this->capacity;
336
    }
337
338
    /**
339
     * [setTimeframe Set the window timeframe]
340
     * @param array $options
341
     */
342
    public function setTimeframe($options)
343
    {
344
        $timeframe = $this->hasOptionProperty($options, 'rateWindow');
345
346
        if (!is_string($timeframe) && !is_numeric($timeframe)) {
347
            $timeframe = $this->window;
348
        }
349
350
        if (!$timeframe) {
351
            $timeframe = $this->window;
352
        }
353
354
        if (is_numeric($timeframe)) {
355
            self::$timeframe['CUSTOM'] = $timeframe;
356
            $this->window = self::$timeframe['CUSTOM'];
357
            return;
358
        }
359
360
        if (isset(self::$timeframe[$timeframe])) {
361
            $this->window = self::$timeframe[$timeframe];
362
            return;
363
        }
364
365
        $this->window = self::$timeframe['MINUTE'];
366
    }
367
368
    /**
369
     * [getTimeframe Get the timeframe window]
370
     * @return integer|string
371
     */
372
    public function getTimeframe()
373
    {
374
        return $this->window;
375
    }
376
377
    /**
378
     * [setLeakRate Set the buckets leak rate]
379
     * Options: slow, medium, normal, default, fast or custom positive integer
380
     * @param array $options
381
     */
382
    private function setLeakRate($options)
383
    {
384
        if (isset($options['leak']) && !$options['leak']) {
385
            $options['leakRate'] = 'default';
386
        }
387
388
        $leakRate = $this->hasOptionProperty($options, 'leakRate');
389
390
        if (empty($leakRate) || !is_string($leakRate)) {
391
            $leakRate = 'default';
392
        }
393
394
        $this->leakRate = $leakRate;
395
    }
396
397
    /**
398
     * [getLeakRate Get the buckets leak rate]
399
     * @return string|integer
400
     */
401
    private function getLeakRate()
402
    {
403
        return $this->leakRate;
404
    }
405
406
    /**
407
     * [setUnlimited Rate limiter bypass]
408
     * @param array $options
409
     */
410
    private function setUnlimited($options)
411
    {
412
        $unlimited = false;
413
414
        if (isset($options['unlimited']) && ($options['unlimited'] == 1 || $options['unlimited'] == true)) {
415
            $unlimited = true;
416
        }
417
418
        if (isset($options['requestType']) && $options['requestType'] === 'debug') {
419
            $unlimited = true;
420
        }
421
422
        $this->unlimited = $unlimited;
423
    }
424
425
    /**
426
     * [isUnlimited Check if the Responsible API is set to unlimited]
427
     * @return boolean
428
     */
429
    private function isUnlimited()
430
    {
431
        return $this->unlimited;
432
    }
433
}
434