0 ? "user_{$user_id}" : $this->get_client_ip(); // Check minute-based rate limit $minute_key = "thinkrank_rate_limit_{$endpoint_name}_{$identifier}_" . floor(time() / 60); $minute_attempts = get_transient($minute_key) ?: 0; if ($minute_attempts >= $this->rate_limit_per_minute) { return new WP_Error( 'rate_limit_exceeded', 'Too many requests per minute. Please slow down.', ['status' => 429] ); } // Check hour-based rate limit $hour_key = "thinkrank_rate_limit_{$endpoint_name}_{$identifier}_" . floor(time() / 3600); $hour_attempts = get_transient($hour_key) ?: 0; if ($hour_attempts >= $this->rate_limit_per_hour) { return new WP_Error( 'rate_limit_exceeded', 'Too many requests per hour. Please try again later.', ['status' => 429] ); } // Increment counters set_transient($minute_key, $minute_attempts + 1, 60); set_transient($hour_key, $hour_attempts + 1, 3600); return true; } /** * Get client IP address * * @since 1.0.0 * * @return string Client IP address */ private function get_client_ip(): string { // Check for various headers that might contain the real IP $headers = [ 'HTTP_CF_CONNECTING_IP', // Cloudflare 'HTTP_CLIENT_IP', // Proxy 'HTTP_X_FORWARDED_FOR', // Load balancer/proxy 'HTTP_X_FORWARDED', // Proxy 'HTTP_X_CLUSTER_CLIENT_IP', // Cluster 'HTTP_FORWARDED_FOR', // Proxy 'HTTP_FORWARDED', // Proxy 'REMOTE_ADDR' // Standard ]; foreach ($headers as $header) { if (!empty($_SERVER[$header])) { $ip = $_SERVER[$header]; // Handle comma-separated IPs (X-Forwarded-For can contain multiple IPs) if (strpos($ip, ',') !== false) { $ip = trim(explode(',', $ip)[0]); } // Validate IP address if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } // Fallback to REMOTE_ADDR even if it's a private IP return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } /** * Get rate limit status for a user/endpoint * * @since 1.0.0 * * @param string $endpoint_name Endpoint identifier * @param int $user_id User ID (0 for anonymous) * @return array Rate limit status */ protected function get_rate_limit_status(string $endpoint_name, int $user_id = 0): array { $identifier = $user_id > 0 ? "user_{$user_id}" : $this->get_client_ip(); $minute_key = "thinkrank_rate_limit_{$endpoint_name}_{$identifier}_" . floor(time() / 60); $hour_key = "thinkrank_rate_limit_{$endpoint_name}_{$identifier}_" . floor(time() / 3600); $minute_attempts = get_transient($minute_key) ?: 0; $hour_attempts = get_transient($hour_key) ?: 0; return [ 'minute_attempts' => $minute_attempts, 'minute_limit' => $this->rate_limit_per_minute, 'minute_remaining' => max(0, $this->rate_limit_per_minute - $minute_attempts), 'hour_attempts' => $hour_attempts, 'hour_limit' => $this->rate_limit_per_hour, 'hour_remaining' => max(0, $this->rate_limit_per_hour - $hour_attempts), 'reset_time' => (floor(time() / 60) + 1) * 60 // Next minute ]; } }