= self::MAX_HITS ) { wp_send_json_error( [ 'message' => __( 'Too many requests. Please wait a moment and try again.', 'b-blocks' ) ], 429 ); } // Increment. First hit also sets the expiry window. if ( 0 === $hits ) { set_transient( $key, 1, self::WINDOW_SECONDS ); } else { // get_transient returned an existing value; update in place. // We intentionally do not refresh the expiry on subsequent // hits so the window is rolling from the *first* request. set_transient( $key, $hits + 1, self::WINDOW_SECONDS ); } } /** * Resolve the client IP address. * * Falls back through common proxy headers to REMOTE_ADDR. Each value * is validated with filter_var so a spoofed X-Forwarded-For cannot * inject an arbitrary string into the transient key. * * @return string A valid IP string, or '0.0.0.0' as a safe fallback. */ private static function get_ip() { $candidates = []; // Trusted proxy headers — only check these when WordPress itself // is behind a known proxy (e.g. a load balancer). Using them // without a trusted proxy allowlist means clients can spoof the // value, but a spoofed IP only affects which rate-limit bucket // they land in — it doesn't bypass the check. if ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { // X-Forwarded-For can be a comma-separated list; the leftmost // IP is the originating client. $xfwd = explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ); $candidates[] = trim( $xfwd[0] ); } if ( isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) { $candidates[] = sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ); } if ( isset( $_SERVER['REMOTE_ADDR'] ) ) { $candidates[] = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); } foreach ( $candidates as $candidate ) { $ip = filter_var( trim( $candidate ), FILTER_VALIDATE_IP ); if ( false !== $ip ) { return $ip; } } return '0.0.0.0'; } } }