DAY_IN_SECONDS; } /** * Count one attempt and report whether the caller is over the limit. * * @param string $form_id Form id. * @param string $ip Client IP. * @param int $limit Max attempts per window (0 disables). * @param int $window Window in seconds. * @return bool True when over the limit (this attempt is rejected). */ public static function hit_rate_limit( $form_id, $ip, $limit = self::DEFAULT_LIMIT, $window = self::DEFAULT_WINDOW ) { $limit = (int) $limit; if ( $limit <= 0 ) { return false; } $suffix = substr( self::hmac( 'rl|' . $form_id . '|' . $ip ), 0, 32 ); $key = 'reactwoo_atomic_msf_rl_' . $suffix; $bucket = get_transient( $key ); $now = time(); if ( ! is_array( $bucket ) || ! isset( $bucket['start'], $bucket['count'] ) || ( $now - (int) $bucket['start'] ) >= $window ) { $bucket = array( 'start' => $now, 'count' => 0 ); } if ( (int) $bucket['count'] >= $limit ) { return true; } $bucket['count'] = (int) $bucket['count'] + 1; set_transient( $key, $bucket, max( 1, $window - ( $now - (int) $bucket['start'] ) ) ); return false; } /** * Privacy-safe hash of the client IP (never stored raw). * * @param string $ip IP. */ public static function ip_hash( $ip ) { return substr( self::hmac( 'ip|' . $ip ), 0, 40 ); } /** * Client IP from REMOTE_ADDR only (proxy headers are spoofable; filterable for trusted proxies). */ public static function client_ip() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; /** Filter: allow sites behind a trusted proxy to supply the visitor IP. */ $ip = (string) apply_filters( 'reactwoo/atomic/forms/client_ip', $ip ); return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '0.0.0.0'; } private static function hmac( $data ) { return hash_hmac( 'sha256', $data, wp_salt( 'nonce' ) ); } }