Passed
Push — master ( 334fde...6a730c )
by Vince
02:11 queued 34s
created

limiter::setUnlimited()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 4
eloc 4
c 3
b 0
f 0
nc 2
nop 1
dl 0
loc 9
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
use responsible\core\server;
21
22
class limiter
23
{
24
    use \responsible\core\traits\optionsTrait;
25
26
    /**
27
     * [$capacity Bucket volume]
28
     * @var integer
29
     */
30
    private $capacity = 100;
31
32
    /**
33
     * [$leakRate Constant rate at which the bucket will leak]
34
     * @var string|integer
35
     */
36
    private $leakRate = 1;
37
38
    /**
39
     * [$unpacked]
40
     */
41
    private $unpacked;
42
43
    /**
44
     * [$packed]
45
     * @var string
46
     */
47
    private $packed;
48
49
    /**
50
     * [$bucket]
51
     * @var object
52
     */
53
    private $bucket;
54
55
    /**
56
     * [$tokenPacker Token packer class object]
57
     * @var object
58
     */
59
    private $tokenPacker;
60
61
    /**
62
     * [$account User account object]
63
     */
64
    private $account;
65
66
    /**
67
     * [$timeframe Durations are in seconds]
68
     * @var array
69
     */
70
    private static $timeframe = [
71
        'SECOND' => 1,
72
        'MINUTE' => 60,
73
        'HOUR' => 3600,
74
        'DAY' => 86400,
75
        'CUSTOM' => 0,
76
    ];
77
78
    /**
79
     * [$window Timeframe window]
80
     * @var integer|string
81
     */
82
    private $window = 'MINUTE';
83
84
    /**
85
     * [$unlimited Rate limiter bypass if true]
86
     * @var boolean
87
     */
88
    private $unlimited = false;
89
90
    /**
91
     * [$isMockTest Set the mock test]
92
     * @var boolean
93
     */
94
    private $isMockTest = false;
95
96
    /**
97
     * [$isMockTest Set the mock account]
98
     * @var array
99
     */
100
    private $mockAccount = [];
101
102
    /**
103
     * [$scope Set the default scope]
104
     * @var string
105
     */
106
    private $scope = 'private';
107
108
    public function __construct($limit = null, $rate = null)
109
    {
110
        if (!is_null($limit)) {
111
            $this->setCapacity(['rateLimit' => $limit]);
112
        }
113
114
        if (!is_null($rate)) {
115
            $this->setTimeframe(['rateWindow' => $rate]);
116
        }
117
118
        $this->bucket = new throttle\tokenBucket;
119
        $this->tokenPacker = new throttle\tokenPack;
120
    }
121
122
    /**
123
     * [setupOptions Set any Responsible API options]
124
     * @return self
125
     */
126
    public function setupOptions()
127
    {
128
        $options = $this->getOptions();
129
130
        $server = new server([], $options);
131
        $this->isMockTest = $server->isMockTest();
132
133
        $this->setCapacity($options);
134
135
        $this->setTimeframe($options);
136
137
        $this->setLeakRate($options);
138
139
        $this->setUnlimited($options);
140
141
        $this->setDebugMode($options);
142
143
        if (isset($this->account->scope) &&
144
            ($this->account->scope == 'anonymous' || $this->account->scope == 'public')
145
        ) {
146
            $this->scope = $this->account->scope;
147
        }
148
149
        return $this;
150
    }
151
152
    /**
153
     * [throttleRequest Build the Responsible API throttle]
154
     * @return boolean|void
155
     */
156
    public function throttleRequest()
157
    {
158
        if ($this->isUnlimited() || $this->scope !== 'private') {
159
            return true;
160
        }
161
162
        $bucket = $this->bucketObj();
163
164
        $this->unpackBucket();
165
        
166
        if ($bucket->capacity()) {
167
            $bucket->pause(false);
168
            $bucket->fill();
169
170
        } else {
171
            $this->throttlePause();
172
        }
173
174
        // print_r($this->unpacked);
175
        // echo PHP_EOL;
176
177
        $this->save();
178
    }
179
180
    /**
181
     * [throttlePause Throttle the limiter when there are too many requests]
182
     * @return void
183
     */
184
    private function throttlePause()
185
    {
186
        $account = $this->getAccount();
187
        $bucket = $this->bucketObj();
188
189
        if ($this->getLeakRate() <= 0) {
190
            if ($this->unpacked['pauseAccess'] == false) {
191
                $bucket->pause(true);
192
                $this->save();
193
            }
194
195
            if ($bucket->refill($account->access)) {
196
                $this->save();
197
            }
198
        }
199
200
        (new exception\errorException)
201
                ->setOptions($this->getOptions())
202
                ->error('TOO_MANY_REQUESTS');
203
    }
204
205
    /**
206
     * Unpack the account bucket data
207
     */
208
    private function unpackBucket()
209
    {
210
        $account = $this->getAccount();
211
        $bucket = $this->bucketObj();
212
        $packer = $this->packerObj();
213
214
        $this->unpacked = $packer->unpack(
215
            $account->bucket
216
        );
217
        if (empty($this->unpacked)) {
218
            $this->unpacked = array(
219
                'drops' => 1,
220
                'time' => $account->access,
221
            );
222
        }
223
224
        $bucket->setTimeframe($this->getTimeframe())
225
            ->setCapacity($this->getCapacity())
226
            ->setLeakRate($this->getLeakRate())
227
            ->pour($this->unpacked['drops'], $this->unpacked['time'])
228
        ;
229
    }
230
231
    /**
232
     * [updateBucket Store the buckets token data and user access time]
233
     * @return void
234
     */
235
    private function save()
236
    {
237
        $bucket = $this->bucketObj();
238
        $packer = $this->packerObj();
239
240
        $this->packed = $packer->pack(
241
            $bucket->getTokenData()
242
        );
243
244
        if($this->isMockTest) {
245
            return;
246
        }
247
248
        /**
249
         * [Update account access]
250
         */
251
        (new user\user)
252
            ->setAccountID($this->getAccount()->account_id)
253
            ->setBucketToken($this->packed)
254
            ->updateAccountAccess()
255
        ;
256
    }
257
258
    /**
259
     * [getThrottle Return a list of the throttled results]
260
     * @return array
261
     */
262
    public function getThrottle()
263
    {
264
        if ($this->isUnlimited() || $this->scope !== 'private') {
265
            return array(
266
                'unlimited' => true,
267
            );
268
        }
269
270
        $bucket = $this->bucketObj();
271
272
        $windowFrame = (is_string($this->getTimeframe()))
273
        ? $this->getTimeframe()
274
        : $this->getTimeframe() . 'secs'
275
        ;
276
277
        if (is_null($bucket)) {
278
            return;
279
        }
280
281
        return array(
282
            'limit' => $this->getCapacity(),
283
            'leakRate' => $this->getLeakRate(),
284
            'leak' => $bucket->getLeakage(),
285
            'lastAccess' => $this->getLastAccessDate(),
286
            'description' => $this->getCapacity() . ' requests per ' . $windowFrame,
287
            'bucket' => $bucket->getTokenData(),
288
        );
289
    }
290
291
    /**
292
     * [getLastAccessDate Get the last recorded access in date format]
293
     * @return string
294
     */
295
    private function getLastAccessDate()
296
    {
297
        $bucket = $this->bucketObj();
298
299
        if (isset($bucket->getTokenData()['time'])) {
300
            return date('m/d/y h:i:sa', $bucket->getTokenData()['time']);
301
        }
302
303
        return 'Can\'t be converted';
304
    }
305
306
    /**
307
     * [setAccount Set the requests account]
308
     * @return self
309
     */
310
    public function setAccount($account)
311
    {
312
        $this->account = $account;
313
        return $this;
314
    }
315
316
    /**
317
     * [getAccount Get the requests account]
318
     */
319
    public function getAccount()
320
    {
321
        if($this->isMockTest) {
322
            $this->getMockAccount();
323
            return $this->mockAccount;
324
        }
325
326
        if (is_null($this->account)||empty($this->account)) {
327
            (new exception\errorException)
328
                ->setOptions($this->getOptions())
329
                ->error('UNAUTHORIZED');
330
            return;
331
        }
332
333
        return $this->account;
334
    }
335
336
    /**
337
     * Build a mock account for testing
338
     * @return void
339
     */
340
    private function getMockAccount()
341
    {
342
        $bucket = $this->bucketObj();
343
        $packer = $this->packerObj();
344
345
        $mockAccount = [];
346
347
        if(!isset($mockAccount['bucket'])) {
348
            $mockAccount['bucket'] = $packer->pack(
349
                $bucket->getTokenData()
350
            );
351
        }
352
353
        $mockAccount['access'] = time();
354
355
        $this->mockAccount = (object)$mockAccount;
0 ignored issues
show
Documentation Bug introduced by
It seems like (object)$mockAccount of type object is incompatible with the declared type array of property $mockAccount.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
356
    }
357
358
    /**
359
     * [bucketObj Get the bucket class object]
360
     * @return object
361
     */
362
    private function bucketObj()
363
    {
364
        return $this->bucket;
365
    }
366
367
    /**
368
     * [packerObj Get the token packer class object]
369
     * @return object
370
     */
371
    private function packerObj()
372
    {
373
        return $this->tokenPacker;
374
    }
375
376
    /**
377
     * [hasOptionProperty Check if an option property is set]
378
     * @param  array  $options
379
     * @param  string  $property
380
     * @return string|integer|boolean
381
     */
382
    private function hasOptionProperty(array $options, $property, $default = false)
383
    {
384
        $val = isset($options[$property]) ? $options[$property] : $default;
385
386
        if ($val && empty($options[$property])) {
387
            $val = $default;
388
        }
389
390
        return $val;
391
    }
392
393
    /**
394
     * [setCapacity Set the buckets capacity]
395
     * @param array $options
396
     */
397
    public function setCapacity($options)
398
    {
399
        $hasCapacityOption = $this->hasOptionProperty($options, 'rateLimit');
400
401
        if (!is_numeric($hasCapacityOption) || empty($hasCapacityOption)) {
402
            $hasCapacityOption = false;
403
        }
404
405
        $this->capacity = ($hasCapacityOption) ? intval($hasCapacityOption) : intval($this->capacity);
406
    }
407
408
    /**
409
     * [getCapacity Get the buckets capacity]
410
     * @return integer
411
     */
412
    public function getCapacity()
413
    {
414
        return $this->capacity;
415
    }
416
417
    /**
418
     * [setTimeframe Set the window timeframe]
419
     * @param array $options
420
     */
421
    public function setTimeframe($options)
422
    {
423
        $timeframe = $this->hasOptionProperty($options, 'rateWindow');
424
425
        if (is_string($timeframe)) {
426
            if (isset(self::$timeframe[$timeframe])) {
427
                $this->window = intval(self::$timeframe[$timeframe]);
428
                return;
429
            }
430
        }
431
432
        if (is_numeric($timeframe)) {
433
            if ($timeframe < 0) {
434
                $timeframe = ($timeframe*-1);
435
            }
436
            self::$timeframe['CUSTOM'] = $timeframe;
437
            $this->window = intval(self::$timeframe['CUSTOM']);
438
            return;
439
        }
440
441
        $this->window = self::$timeframe['MINUTE'];
442
    }
443
444
    /**
445
     * [getTimeframe Get the timeframe window]
446
     * @return integer|string
447
     */
448
    public function getTimeframe()
449
    {
450
        return $this->window;
451
    }
452
453
    /**
454
     * [setLeakRate Set the buckets leak rate]
455
     * Options: slow, medium, normal, default, fast or custom positive integer
456
     * @param array $options
457
     */
458
    private function setLeakRate($options)
459
    {
460
        if (isset($options['leak']) && !$options['leak']) {
461
            $options['leakRate'] = 'default';
462
        }
463
464
        $leakRate = $this->hasOptionProperty($options, 'leakRate');
465
466
        if (empty($leakRate) || !is_string($leakRate)) {
467
            $leakRate = 'default';
468
        }
469
470
        $this->leakRate = $leakRate;
471
    }
472
473
    /**
474
     * [getLeakRate Get the buckets leak rate]
475
     * @return string|integer
476
     */
477
    public function getLeakRate()
478
    {
479
        return $this->leakRate;
480
    }
481
482
    /**
483
     * [setUnlimited Rate limiter bypass]
484
     * @param array $options
485
     */
486
    private function setUnlimited($options)
487
    {
488
        $unlimited = false;
489
490
        if (isset($options['unlimited']) && ($options['unlimited'] == 1 || $options['unlimited'] == true)) {
491
            $unlimited = true;
492
        }
493
494
        $this->unlimited = $unlimited;
495
    }
496
497
    /**
498
     * [setUnlimited Rate limiter bypass in debug mode]
499
     * @param array $options
500
     */
501
    private function setDebugMode($options)
502
    {
503
        $unlimited = false;
504
505
        if (isset($options['requestType']) && $options['requestType'] === 'debug') {
506
            $unlimited = true;
507
        }
508
509
        $this->unlimited = $unlimited;
510
    }
511
512
    /**
513
     * [isUnlimited Check if the Responsible API is set to unlimited]
514
     * @return boolean
515
     */
516
    private function isUnlimited()
517
    {
518
        return $this->unlimited;
519
    }
520
}
521