Jake Vanderwerf
5 days ago a9b3b28d001941921aa70d37fdc87c758a163a44
inc/rest/RateLimits.php
@@ -0,0 +1,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}";
   }
}