Passed
Push — master ( 436048...907adf )
by Vince
02:31
created

limiter::setTimeframe()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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