db = $db; } /** * Check if we can make a request (and wait if needed) * * @param bool $wait Whether to wait if rate limited * @return bool True if request can proceed */ public function check_and_wait($wait = true) { // Check per-second limit (most restrictive) $this->enforce_per_second_limit(); // Check hourly limit if (!$this->check_limit(self::WINDOW_HOUR, self::LIMIT_PER_HOUR)) { if ($wait) { $this->wait_for_window(self::WINDOW_HOUR); } else { return false; } } // Check daily limit if (!$this->check_limit(self::WINDOW_DAY, self::LIMIT_PER_DAY)) { if ($wait) { $this->wait_for_window(self::WINDOW_DAY); } else { return false; } } return true; } /** * Enforce per-second rate limit */ private function enforce_per_second_limit() { $now = microtime(true); $min_interval = 1.0 / self::LIMIT_PER_SECOND; // 0.5 seconds if ($this->last_request_time > 0) { $elapsed = $now - $this->last_request_time; if ($elapsed < $min_interval) { $sleep_time = ($min_interval - $elapsed) * 1000000; // microseconds usleep((int) $sleep_time); } } $this->last_request_time = microtime(true); } /** * Check if under the limit for a window type * * @param string $window_type Window type * @param int $limit Limit for this window * @return bool True if under limit */ private function check_limit($window_type, $limit) { $count = $this->get_window_count($window_type); return $count < $limit; } /** * Get current count for a window * * @param string $window_type Window type * @return int Current request count */ private function get_window_count($window_type) { global $wpdb; $window_start = $this->get_window_start($window_type); $count = $wpdb->get_var($wpdb->prepare( "SELECT request_count FROM {$this->db->rate_limits_table()} WHERE window_type = %s AND window_start = %s", $window_type, $window_start )); return $count ? (int) $count : 0; } /** * Get window start time * * @param string $window_type Window type * @return string MySQL datetime */ private function get_window_start($window_type) { $now = current_time('timestamp'); switch ($window_type) { case self::WINDOW_SECOND: return gmdate('Y-m-d H:i:s', $now); case self::WINDOW_HOUR: return gmdate('Y-m-d H:00:00', $now); case self::WINDOW_DAY: return gmdate('Y-m-d 00:00:00', $now); default: return gmdate('Y-m-d H:i:s', $now); } } /** * Record a request * * @param int $bytes_transferred Optional bytes transferred */ public function record_request($bytes_transferred = 0) { global $wpdb; // Record for hourly window $this->increment_window(self::WINDOW_HOUR, $bytes_transferred); // Record for daily window $this->increment_window(self::WINDOW_DAY, $bytes_transferred); // Clean up old records $this->cleanup_old_records(); } /** * Increment count for a window * * @param string $window_type Window type * @param int $bytes_transferred Bytes transferred */ private function increment_window($window_type, $bytes_transferred = 0) { global $wpdb; $window_start = $this->get_window_start($window_type); // Try to update existing record $updated = $wpdb->query($wpdb->prepare( "UPDATE {$this->db->rate_limits_table()} SET request_count = request_count + 1, bytes_transferred = bytes_transferred + %d WHERE window_type = %s AND window_start = %s", $bytes_transferred, $window_type, $window_start )); // If no record existed, insert new one if (0 === $updated) { $wpdb->insert( $this->db->rate_limits_table(), array( 'window_type' => $window_type, 'window_start' => $window_start, 'request_count' => 1, 'bytes_transferred' => $bytes_transferred, ), array('%s', '%s', '%d', '%d') ); } } /** * Wait for a rate limit window to reset * * @param string $window_type Window type */ private function wait_for_window($window_type) { $now = current_time('timestamp'); switch ($window_type) { case self::WINDOW_HOUR: // Wait until next hour $next_hour = strtotime('+1 hour', strtotime(gmdate('Y-m-d H:00:00', $now))); $wait_seconds = $next_hour - $now; break; case self::WINDOW_DAY: // Wait until next day $next_day = strtotime('+1 day', strtotime(gmdate('Y-m-d 00:00:00', $now))); $wait_seconds = $next_day - $now; break; default: $wait_seconds = 1; } if ($wait_seconds > 0) { sleep(min($wait_seconds, 60)); // Max 60 second wait per call } } /** * Clean up old rate limit records */ private function cleanup_old_records() { global $wpdb; // Delete records older than 48 hours $cutoff = gmdate('Y-m-d H:i:s', strtotime('-48 hours')); $wpdb->query($wpdb->prepare( "DELETE FROM {$this->db->rate_limits_table()} WHERE window_start < %s", $cutoff )); } /** * Get current rate limit status * * @return array Rate limit status */ public function get_status() { return array( 'hourly' => array( 'used' => $this->get_window_count(self::WINDOW_HOUR), 'limit' => self::LIMIT_PER_HOUR, 'remaining' => max(0, self::LIMIT_PER_HOUR - $this->get_window_count(self::WINDOW_HOUR)), ), 'daily' => array( 'used' => $this->get_window_count(self::WINDOW_DAY), 'limit' => self::LIMIT_PER_DAY, 'remaining' => max(0, self::LIMIT_PER_DAY - $this->get_window_count(self::WINDOW_DAY)), ), 'bytes_this_hour' => $this->get_bytes_this_hour(), 'bytes_limit' => self::LIMIT_BYTES_PER_HOUR, ); } /** * Get bytes transferred this hour * * @return int Bytes */ private function get_bytes_this_hour() { global $wpdb; $window_start = $this->get_window_start(self::WINDOW_HOUR); $bytes = $wpdb->get_var($wpdb->prepare( "SELECT bytes_transferred FROM {$this->db->rate_limits_table()} WHERE window_type = %s AND window_start = %s", self::WINDOW_HOUR, $window_start )); return $bytes ? (int) $bytes : 0; } /** * Check if we're approaching rate limits * * @param float $threshold Percentage threshold (0.0 - 1.0) * @return bool True if approaching limits */ public function is_approaching_limit($threshold = 0.9) { $status = $this->get_status(); $hourly_pct = $status['hourly']['used'] / $status['hourly']['limit']; $daily_pct = $status['daily']['used'] / $status['daily']['limit']; return $hourly_pct >= $threshold || $daily_pct >= $threshold; } /** * Reset all rate limit counters (for testing) */ public function reset() { global $wpdb; $wpdb->query("TRUNCATE TABLE {$this->db->rate_limits_table()}"); $this->last_request_time = 0; } }