400 ) ); } $message = trim( wp_strip_all_tags( $message ) ); if ( '' === $message ) { return new WP_Error( 'aivexa_empty_message', __( 'Message cannot be empty.', 'aivexa' ), array( 'status' => 400 ) ); } if ( mb_strlen( $message ) > 4000 ) { return new WP_Error( 'aivexa_message_too_long', __( 'Message is too long.', 'aivexa' ), array( 'status' => 400 ) ); } return $message; } /** * Validate conversation ID. * * @param mixed $conversation_id Conversation ID. * @return string */ public static function sanitize_conversation_id( $conversation_id ): string { if ( ! is_string( $conversation_id ) ) { return ''; } $conversation_id = sanitize_key( $conversation_id ); if ( ! preg_match( '/^[a-z0-9_-]{8,64}$/', $conversation_id ) ) { return ''; } return $conversation_id; } /** * Get client IP for rate limiting. */ public static function get_client_ip(): string { $ip = ''; if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { $ip = sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) ); } if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; } return '0.0.0.0'; } /** * Check rate limit. * * @return true|WP_Error */ public static function check_rate_limit() { if ( current_user_can( 'manage_options' ) ) { return true; } $settings = Aivexa_Settings::get_all(); $limit = (int) $settings['rate_limit_requests']; $window = (int) $settings['rate_limit_window']; $key = 'aivexa_rate_' . md5( self::get_client_ip() . '|' . self::get_session_token() ); $data = get_transient( $key ); if ( ! is_array( $data ) ) { $data = array( 'count' => 0, 'start' => time(), ); } if ( ( time() - (int) $data['start'] ) > $window ) { $data = array( 'count' => 0, 'start' => time(), ); } if ( (int) $data['count'] >= $limit ) { return new WP_Error( 'aivexa_rate_limited', __( 'Too many requests. Please try again later.', 'aivexa' ), array( 'status' => 429 ) ); } ++$data['count']; set_transient( $key, $data, $window ); return true; } /** * Session token for anonymous users. */ public static function get_session_token(): string { static $token = null; if ( is_string( $token ) && '' !== $token ) { return $token; } if ( is_user_logged_in() ) { $token = 'user_' . (string) get_current_user_id(); return $token; } if ( ! empty( $_COOKIE[ self::SESSION_COOKIE ] ) ) { $cookie = sanitize_text_field( wp_unslash( (string) $_COOKIE[ self::SESSION_COOKIE ] ) ); if ( preg_match( '/^[a-f0-9]{32}$/', $cookie ) ) { $token = $cookie; return $token; } } try { $token = bin2hex( random_bytes( 16 ) ); } catch ( Exception $exception ) { unset( $exception ); $token = md5( wp_generate_uuid4() . wp_rand() ); } setcookie( self::SESSION_COOKIE, $token, array( 'expires' => time() + DAY_IN_SECONDS, 'path' => COOKIEPATH ? COOKIEPATH : '/', 'domain' => COOKIE_DOMAIN, 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ) ); return $token; } /** * REST chat permission callback. * * @return true|WP_Error */ public static function rest_chat_permission() { if ( ! Aivexa_Settings::is_agent_ready() ) { return new WP_Error( 'aivexa_agent_disabled', __( 'The AI assistant is currently unavailable.', 'aivexa' ), array( 'status' => 503 ) ); } $nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_X_WP_NONCE'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { return new WP_Error( 'aivexa_invalid_nonce', __( 'Invalid security token.', 'aivexa' ), array( 'status' => 403 ) ); } return self::check_rate_limit(); } /** * REST admin permission callback. * * @return true|WP_Error */ public static function rest_admin_permission() { if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'aivexa_forbidden', __( 'You do not have permission to perform this action.', 'aivexa' ), array( 'status' => 403 ) ); } return true; } /** * REST status permission callback. * * @return true */ public static function rest_status_permission(): bool { return true; } /** * Strip sensitive values from debug output. * * @param mixed $data Data. * @return mixed */ public static function redact_sensitive( $data ) { if ( is_string( $data ) ) { return preg_replace( '/sk-[A-Za-z0-9_-]+/', '[REDACTED]', $data ); } if ( is_array( $data ) ) { foreach ( $data as $key => $value ) { if ( in_array( $key, array( 'api_key', 'authorization', 'Authorization' ), true ) ) { $data[ $key ] = '[REDACTED]'; } else { $data[ $key ] = self::redact_sensitive( $value ); } } } return $data; } }