Passed
Push — master ( f44c48...436048 )
by Vince
01:32
created

limiter::setUnlimited()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 6
c 2
b 0
f 0
nc 4
nop 1
dl 0
loc 13
rs 9.2222
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
     * @var object|null
49
     */
50
    private $bucket;
51
52
    /**
53
     * [$tokenPacker Token packer class object]
54
     * @var object|null
55
     */
56
    private $tokenPacker = null;
57
58
    /**
59
     * [$account User account object]
60
     */
61
    private $account;
62
63
    /**
64
     * [$options Responisble options]
65
     */
66
    private $options;
67
68
    /**
69
     * [$timeframe Durations are in seconds]
70
     * @var array
71
     */
72
    private static $timeframe = [
73
        'SECOND' => 1,
74
        'MINUTE' => 60,
75
        'HOUR' => 3600,
76
        'DAY' => 86400,
77
        'CUSTOM' => 0,
78
    ];
79
80
    /**
81
     * [$window Timeframe window]
82
     * @var integer|string
83
     */
84
    private $window = 'MINUTE';
85
86
    /**
87
     * [$unlimited Rate limiter bypass if true]
88
     * @var boolean
89
     */
90
    private $unlimited = false;
91
92
    /**
93
     * [$scope Set the default scope]
94
     * @var string
95
     */
96
    private $scope = 'private';
97
98
    public function __construct($limit = null, $rate = null)
99
    {
100
        if (!is_null($limit)) {
101
            $this->capacity = $limit;
102
        }
103
104
        if (!is_null($rate)) {
105
            $this->window = $rate;
106
        }
107
108
        $this->tokenPacker = new throttle\tokenPack;
109
        $this->bucket = new throttle\tokenBucket;
110
    }
111
112
    /**
113
     * [setupOptions Set any Responsible API options]
114
     * @return self
115
     */
116
    public function setupOptions()
117
    {
118
        $options = $this->getOptions();
119
120
        $this->setCapacity($options);
121
122
        $this->setTimeframe($options);
123
124
        $this->setLeakRate($options);
125
126
        $this->setUnlimited($options);
127
128
        if (isset($this->account->scope) &&
129
            ($this->account->scope == 'anonymous' || $this->account->scope == 'public')
130
        ) {
131
            $this->scope = $this->account->scope;
132
        }
133
134
        return $this;
135
    }
136
137
    /**
138
     * [throttleRequest Build the Responsible API throttle]
139
     * @return boolean|void
140
     */
141
    public function throttleRequest()
142
    {
143
        if ($this->isUnlimited() || $this->scope !== 'private') {
144
            return true;
145
        }
146
147
        $account = $this->getAccount();
148
149
        $this->unpackBucket();
150
        
151
        if ($this->bucket->capacity()) {
0 ignored issues
show
Bug introduced by
The method capacity() does not exist on null. ( Ignorable by Annotation )

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

151
        if ($this->bucket->/** @scrutinizer ignore-call */ capacity()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
152
            $this->bucket->pause(false);
153
            $this->bucket->fill();
154
        } else {
155
            if ($this->getLeakRate() <= 0) {
156
                if ($this->unpacked['pauseAccess'] == false) {
157
                    $this->bucket->pause(true);
158
                    $this->save();
159
                }
160
161
                if ($this->bucket->refill($account->access)) {
162
                    $this->save();
163
                }
164
            }
165
166
            (new exception\errorException)->error('TOO_MANY_REQUESTS');
167
        }
168
169
        $this->save();
170
    }
171
172
    /**
173
     * Unpack the account bucket data
174
     */
175
    private function unpackBucket()
176
    {
177
        $account = $this->getAccount();
178
179
        $this->unpacked = $this->tokenPacker->unpack(
0 ignored issues
show
Bug introduced by
The method unpack() does not exist on null. ( Ignorable by Annotation )

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

179
        /** @scrutinizer ignore-call */ 
180
        $this->unpacked = $this->tokenPacker->unpack(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
180
            $account->bucket
181
        );
182
        if (empty($this->unpacked)) {
183
            $this->unpacked = array(
184
                'drops' => 1,
185
                'time' => $account->access,
186
            );
187
        }
188
189
        $this->bucket->setTimeframe($this->getTimeframe())
190
            ->setCapacity($this->getCapacity())
191
            ->setLeakRate($this->getLeakRate())
192
            ->pour($this->unpacked['drops'], $this->unpacked['time'])
193
        ;
194
    }
195
196
    /**
197
     * [updateBucket Store the buckets token data and user access time]
198
     * @return void
199
     */
200
    private function save()
201
    {
202
        $this->packed = $this->tokenPacker->pack(
203
            $this->bucket->getTokenData()
204
        );
205
206
        /**
207
         * [Update account access]
208
         */
209
        (new user\user)
210
            ->setAccountID($this->getAccount()->account_id)
211
            ->setBucketToken($this->packed)
212
            ->updateAccountAccess()
213
        ;
214
    }
215
216
    /**
217
     * [getThrottle Return a list of the throttled results]
218
     * @return array
219
     */
220
    public function getThrottle()
221
    {
222
        if ($this->isUnlimited() || $this->scope !== 'private') {
223
            return array(
224
                'unlimited' => true,
225
            );
226
        }
227
228
        $windowFrame = (is_string($this->getTimeframe()))
229
        ? $this->getTimeframe()
230
        : $this->getTimeframe() . 'secs'
231
        ;
232
233
        if (is_null($this->bucket)) {
234
            return;
235
        }
236
237
        return array(
238
            'limit' => $this->getCapacity(),
239
            'leakRate' => $this->getLeakRate(),
240
            'leak' => $this->bucket->getLeakage(),
241
            'lastAccess' => $this->getLastAccessDate(),
242
            'description' => $this->getCapacity() . ' requests per ' . $windowFrame,
243
            'bucket' => $this->bucket->getTokenData(),
244
        );
245
    }
246
247
    /**
248
     * [getLastAccessDate Get the last recorded access in date format]
249
     * @return string
250
     */
251
    private function getLastAccessDate()
252
    {
253
        if (isset($this->bucket->getTokenData()['time'])) {
254
            return date('m/d/y h:i:sa', $this->bucket->getTokenData()['time']);
255
        }
256
257
        return 'Can\'t be converted';
258
    }
259
260
    /**
261
     * [setAccount Set the requests account]
262
     * @return self
263
     */
264
    public function setAccount($account)
265
    {
266
        $this->account = $account;
267
        return $this;
268
    }
269
270
    /**
271
     * [getAccount Get the requests account]
272
     */
273
    public function getAccount()
274
    {
275
        if (is_null($this->account)||empty($this->account)) {
276
            (new exception\errorException)
277
                ->setOptions($this->getOptions())
278
                ->error('UNAUTHORIZED');
279
            return;
280
        }
281
282
        return $this->account;
283
    }
284
285
    /**
286
     * [options Responsible API options]
287
     * @param array $options
288
     */
289
    public function options($options)
290
    {
291
        $this->options = $options;
292
        return $this;
293
    }
294
295
    /**
296
     * [getOptions Get the stored Responsible API options]
297
     * @return array
298
     */
299
    private function getOptions()
300
    {
301
        return $this->options;
302
    }
303
304
    /**
305
     * [hasOptionProperty Check if an option property is set]
306
     * @param  array  $options
307
     * @param  string  $property
308
     * @return string|integer|boolean
309
     */
310
    private function hasOptionProperty(array $options, $property, $default = false)
311
    {
312
        $val = isset($options[$property]) ? $options[$property] : $default;
313
314
        if ($val && empty($options[$property])) {
315
            $val = $default;
316
        }
317
318
        return $val;
319
    }
320
321
    /**
322
     * [setCapacity Set the buckets capacity]
323
     * @param array $options
324
     */
325
    public function setCapacity($options)
326
    {
327
        $hasCapacityOption = $this->hasOptionProperty($options, 'rateLimit');
328
329
        if ($hasCapacityOption) {
330
            if (!is_integer($hasCapacityOption) || empty($hasCapacityOption)) {
331
                $hasCapacityOption = false;
332
            }
333
        }
334
335
        $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...
336
    }
337
338
    /**
339
     * [getCapacity Get the buckets capacity]
340
     * @return integer
341
     */
342
    public function getCapacity()
343
    {
344
        return $this->capacity;
345
    }
346
347
    /**
348
     * [setTimeframe Set the window timeframe]
349
     * @param array $options
350
     */
351
    public function setTimeframe($options)
352
    {
353
        $timeframe = $this->hasOptionProperty($options, 'rateWindow');
354
355
        if (!is_string($timeframe) && !is_numeric($timeframe)) {
356
            $timeframe = $this->window;
357
        }
358
359
        if (!$timeframe) {
360
            $timeframe = $this->window;
361
        }
362
363
        if (is_numeric($timeframe)) {
364
            self::$timeframe['CUSTOM'] = $timeframe;
365
            $this->window = self::$timeframe['CUSTOM'];
366
            return;
367
        }
368
369
        if (isset(self::$timeframe[$timeframe])) {
370
            $this->window = self::$timeframe[$timeframe];
371
            return;
372
        }
373
374
        $this->window = self::$timeframe['MINUTE'];
375
    }
376
377
    /**
378
     * [getTimeframe Get the timeframe window]
379
     * @return integer|string
380
     */
381
    public function getTimeframe()
382
    {
383
        return $this->window;
384
    }
385
386
    /**
387
     * [setLeakRate Set the buckets leak rate]
388
     * Options: slow, medium, normal, default, fast or custom positive integer
389
     * @param array $options
390
     */
391
    private function setLeakRate($options)
392
    {
393
        if (isset($options['leak']) && !$options['leak']) {
394
            $options['leakRate'] = 'default';
395
        }
396
397
        $leakRate = $this->hasOptionProperty($options, 'leakRate');
398
399
        if (empty($leakRate) || !is_string($leakRate)) {
400
            $leakRate = 'default';
401
        }
402
403
        $this->leakRate = $leakRate;
404
    }
405
406
    /**
407
     * [getLeakRate Get the buckets leak rate]
408
     * @return string|integer
409
     */
410
    private function getLeakRate()
411
    {
412
        return $this->leakRate;
413
    }
414
415
    /**
416
     * [setUnlimited Rate limiter bypass]
417
     * @param array $options
418
     */
419
    private function setUnlimited($options)
420
    {
421
        $unlimited = false;
422
423
        if (isset($options['unlimited']) && ($options['unlimited'] == 1 || $options['unlimited'] == true)) {
424
            $unlimited = true;
425
        }
426
427
        if (isset($options['requestType']) && $options['requestType'] === 'debug') {
428
            $unlimited = true;
429
        }
430
431
        $this->unlimited = $unlimited;
432
    }
433
434
    /**
435
     * [isUnlimited Check if the Responsible API is set to unlimited]
436
     * @return boolean
437
     */
438
    private function isUnlimited()
439
    {
440
        return $this->unlimited;
441
    }
442
}
443