Jake Vanderwerf
9 days ago ed57c386db34d8693ca75311972d0929ebe5f488
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
<?php
namespace JVBase\rest;
 
use WP_REST_Request;
 
if (!defined('ABSPATH')) {
    exit;
}
 
/**
 * Handles rate limiting for REST requests
 */
class RateLimits
{
    protected string $cacheGroup = 'jvb_rate_limits';
 
    protected array $defaults = [
        'GET' => ['count' => 1000, 'window' => 3600],
        'POST' => ['count' => 100, 'window' => 3600],
        'PUT' => ['count' => 100, 'window' => 3600],
        'PATCH' => ['count' => 100, 'window' => 3600],
        'DELETE' => ['count' => 50, 'window' => 3600],
    ];
 
    /**
     * Check if request is within rate limits
     *
     * @param WP_REST_Request $request
     * @param int|null $limit Optional custom limit (overrides defaults)
     * @param int|null $window Optional custom window in seconds (overrides defaults)
     * @return bool True if within limits, false if exceeded
     */
    public function checkLimit(WP_REST_Request $request, ?int $limit = null, ?int $window = null): bool
    {
        $method = $request->get_method();
        $default = $this->defaults[$method] ?? $this->defaults['GET'];
 
        $limit = $limit ?? $default['count'];
        $window = $window ?? $default['window'];
 
        $key = $this->getCacheKey($request, $window);
        $current = (int) wp_cache_get($key, $this->cacheGroup);
 
        if ($current >= $limit) {
            return false;
        }
 
        // Increment or initialize
        if ($current === 0) {
            wp_cache_set($key, 1, $this->cacheGroup, $window);
        } else {
            wp_cache_incr($key, 1, $this->cacheGroup);
        }
 
        return true;
    }
 
    /**
     * Get remaining requests for current window
     */
    public function getRemaining(WP_REST_Request $request, ?int $limit = null, ?int $window = null): int
    {
        $method = $request->get_method();
        $default = $this->defaults[$method] ?? $this->defaults['GET'];
 
        $limit = $limit ?? $default['count'];
        $window = $window ?? $default['window'];
 
        $key = $this->getCacheKey($request, $window);
        $current = (int) wp_cache_get($key, $this->cacheGroup);
 
        return max(0, $limit - $current);
    }
 
    /**
     * Reset rate limit for a request pattern
     */
    public function reset(WP_REST_Request $request, ?int $window = null): void
    {
        $method = $request->get_method();
        $default = $this->defaults[$method] ?? $this->defaults['GET'];
        $window = $window ?? $default['window'];
 
        $key = $this->getCacheKey($request, $window);
        wp_cache_delete($key, $this->cacheGroup);
    }
 
    protected function getCacheKey(WP_REST_Request $request, int $window): string
    {
        $ip = $request->get_header('X-Forwarded-For') ?: ($_SERVER['REMOTE_ADDR'] ?? 'unknown');
        $userId = get_current_user_id();
        $method = $request->get_method();
        $route = $request->get_route();
 
        // Include window in key so different windows don't collide
        return "rate:{$ip}:{$userId}:{$method}:{$route}:{$window}";
    }
}