0, 'window_start' => $now]; if (file_exists($filePath)) { $content = @file_get_contents($filePath); if ($content !== false) { $decoded = json_decode($content, true); if (is_array($decoded)) { $rateData = $decoded; } } } // Check if window has expired if ($now - $rateData['window_start'] >= self::WINDOW_SECONDS) { $rateData = ['count' => 0, 'window_start' => $now]; } // Increment count $rateData['count']++; // Save updated data @file_put_contents($filePath, json_encode($rateData), LOCK_EX); // Check if over limit return $rateData['count'] <= $limit; } /** * Clean up old rate limit files (call periodically) */ public static function cleanupOldFiles() { $dir = self::getRateLimitDir(); $files = glob($dir . '/*.json'); $now = time(); $maxAge = self::WINDOW_SECONDS * 2; // Files older than 2 windows foreach ($files as $file) { if ($now - filemtime($file) > $maxAge) { @unlink($file); } } } /** * Check rate limit for current request (both session and IP) * * @param string $type 'default' or 'api' * @return bool True if request is allowed, false if rate limited */ public static function check($type = 'default') { // First check IP-based rate limit (prevents session bypass) if (!self::checkIpRateLimit($type)) { return false; } // Then check session-based rate limit if (session_status() === PHP_SESSION_NONE) { session_start(); } $limit = $type === 'api' ? self::API_LIMIT : self::DEFAULT_LIMIT; $key = 'rate_limit_' . $type; $now = time(); // Initialize rate limit tracking if (!isset($_SESSION[$key])) { $_SESSION[$key] = [ 'count' => 0, 'window_start' => $now ]; } $rateData = &$_SESSION[$key]; // Check if window has expired if ($now - $rateData['window_start'] >= self::WINDOW_SECONDS) { // Reset for new window $rateData['count'] = 0; $rateData['window_start'] = $now; } // Increment request count $rateData['count']++; // Check if over limit if ($rateData['count'] > $limit) { return false; } return true; } /** * Apply rate limiting and send error response if exceeded * * @param string $type 'default' or 'api' */ public static function apply($type = 'default') { // Periodically clean up old rate limit files (1% chance per request) if (mt_rand(1, 100) === 1) { self::cleanupOldFiles(); } if (!self::check($type)) { http_response_code(429); header('Content-Type: application/json'); header('Retry-After: ' . self::WINDOW_SECONDS); echo json_encode([ 'success' => false, 'error' => 'Rate limit exceeded. Please try again later.', 'retry_after' => self::WINDOW_SECONDS ]); exit; } } /** * Get current rate limit status * * @param string $type 'default' or 'api' * @return array Rate limit status */ public static function getStatus($type = 'default') { if (session_status() === PHP_SESSION_NONE) { session_start(); } $limit = $type === 'api' ? self::API_LIMIT : self::DEFAULT_LIMIT; $key = 'rate_limit_' . $type; $now = time(); if (!isset($_SESSION[$key])) { return [ 'limit' => $limit, 'remaining' => $limit, 'reset' => $now + self::WINDOW_SECONDS ]; } $rateData = $_SESSION[$key]; // Check if window has expired if ($now - $rateData['window_start'] >= self::WINDOW_SECONDS) { return [ 'limit' => $limit, 'remaining' => $limit, 'reset' => $now + self::WINDOW_SECONDS ]; } return [ 'limit' => $limit, 'remaining' => max(0, $limit - $rateData['count']), 'reset' => $rateData['window_start'] + self::WINDOW_SECONDS ]; } /** * Add rate limit headers to response * * @param string $type 'default' or 'api' */ public static function addHeaders($type = 'default') { $status = self::getStatus($type); header('X-RateLimit-Limit: ' . $status['limit']); header('X-RateLimit-Remaining: ' . $status['remaining']); header('X-RateLimit-Reset: ' . $status['reset']); } }