Passed
Push — master ( 00967d...334fde )
by Vince
01:45 queued 11s
created

limiter::save()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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