Jake Vanderwerf
5 days ago 0dfe1d8afafc59c4a5559c498342668d5a58d6ef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<?php
namespace JVBase\integrations;
 
if (!defined('ABSPATH')) {
    exit;
}
 
class RateLimits {
    protected array $history = [];
    protected int $maxHistory = 100;
    protected array $limits = [
        'per_second'    => 2,
        'per_minute'    => 30,
        'per_hour'      => 1000
    ];
 
    protected string $key;
 
    public function __construct(string $key, array $limits = []) {
        $this->key = sanitize_title($key);
        //Allow custom limits
        foreach ($limits as $interval => $limit) {
            $key = match($interval) {
                'second', 'per_second','s'  => 'per_second',
                'minute', 'per_minute', 'm' => 'per_minute',
                'hour', 'per_hour', 'h'     => 'per_hour',
                default => false
            };
            if (!$key) {
                error_log('Attempted to add custom limit: '.$interval.' for key '.$key);
                continue;
            }
            $this->limits[$key] = (int)$limit;
        }
 
        $this->getHistory();
        add_action('shutdown', [$this, 'saveHistory']);
    }
        protected function getHistory():void
        {
            $stored = get_transient(BASE.$this->key.'_limits');
            if (!$stored) {
                $stored = [];
            }
            $this->history = $stored;
        }
        public function recordRequest(string $endpoint = 'base'):void
        {
            if (!array_key_exists($endpoint, $this->history)) {
                $this->history[$endpoint] = [];
            }
 
            $this->history[$endpoint][] = time();
            $this->checkHistorySize();
        }
        public function saveHistory():void
        {
            set_transient(BASE.$this->key.'_limits', $this->history);
        }
        protected function checkHistorySize():void
        {
            foreach ($this->history as $endpoint => $history) {
                if (count($history) > $this->maxHistory) {
                    $this->history[$endpoint] = array_slice($this->history[$endpoint], -$this->maxHistory);
                }
            }
        }
 
    public function checkRateLimit(string $endpoint = 'base'):bool
    {
        if (!array_key_exists($endpoint, $this->history)) {
            return true;
        }
 
        $now = time();
 
        $this->cleanHistory($now);
        $counts = [
            'per_second'    => 0,
            'per_minute'    => 0,
            'per_hour'      => count($this->history)
        ];
 
        foreach ($this->history[$endpoint]??[] as $timestamp) {
            if ($now - $timestamp <=1) $counts['per_second']++;
            if ($now - $timestamp <=60) $counts['per_minute']++;
        }
 
        foreach ($this->limits as $interval => $limit) {
            if ($counts[$interval] >= $limit) return false;
        }
        return true;
    }
 
    protected function cleanHistory($now):void
    {
        $this->history = array_map(function ($endpointHistory) use ($now) {
            return array_filter($endpointHistory, function($timestamp) use ($now) {
                return ($now - $timestamp) < 3600; //Keep last hour
            });
        }, $this->history);
    }
}