limit = absint($limit); $this->window = absint($window); } /** * Check if the current user/IP is rate limited. * * @param string $action The action being rate limited. * @return bool True if rate limited (should block), false if allowed. */ public function is_limited($action) { $key = $this->get_rate_limit_key($action); $data = get_transient($key); if ($data === false) { // First request, not limited return false; } return $data['count'] >= $this->limit; } /** * Record a request for rate limiting purposes. * * @param string $action The action being recorded. * @return void */ public function record_request($action) { $key = $this->get_rate_limit_key($action); $data = get_transient($key); if ($data === false) { // First request in this window $data = [ 'count' => 1, 'start' => time(), ]; } else { $data['count']++; } // Set/update transient with remaining window time $elapsed = time() - $data['start']; $remaining = max(1, $this->window - $elapsed); set_transient($key, $data, $remaining); } /** * Check rate limit and record request in one call. * * @param string $action The action being checked. * @return bool True if request is allowed, false if rate limited. */ public function check_and_record($action) { if ($this->is_limited($action)) { return false; } $this->record_request($action); return true; } /** * Get the number of remaining requests in the current window. * * @param string $action The action to check. * @return int Number of remaining requests. */ public function get_remaining($action) { $key = $this->get_rate_limit_key($action); $data = get_transient($key); if ($data === false) { return $this->limit; } return max(0, $this->limit - $data['count']); } /** * Get the rate limit key for the current user/IP. * * @param string $action The action being rate limited. * @return string Transient key. */ private function get_rate_limit_key($action) { // Use user ID if logged in, otherwise IP $user_id = get_current_user_id(); if ($user_id > 0) { $identifier = 'user_' . $user_id; } else { // Sanitize and hash IP for privacy $ip = $this->get_client_ip(); $identifier = 'ip_' . md5($ip); } return 'mlf_rate_' . sanitize_key($action) . '_' . $identifier; } /** * Get the client IP address. * * @return string Client IP address. */ private function get_client_ip() { $ip = ''; // Check for various headers (in order of reliability) $headers = [ 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR', ]; foreach ($headers as $header) { if (!empty($_SERVER[$header])) { // X-Forwarded-For can contain multiple IPs, get the first one $ips = explode(',', sanitize_text_field(wp_unslash($_SERVER[$header]))); $ip = trim($ips[0]); // Validate IP if (filter_var($ip, FILTER_VALIDATE_IP)) { break; } } } // Fallback to localhost if no valid IP found return $ip ?: '127.0.0.1'; } /** * Clear rate limit for a specific action and user/IP. * * @param string $action The action to clear. * @return void */ public function clear($action) { $key = $this->get_rate_limit_key($action); delete_transient($key); } }