| | |
| | | <?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}"; |
| | | } |
| | | } |