settings = $settings; $this->rate_limiter = $rate_limiter; } /** * Verifies the REST nonce. * * Visitors who are not logged in still get a valid nonce, which limits requests coming from other sites. * * @param WP_REST_Request $request Request. * @return true|WP_Error */ public function verify_rest( $request ) { $nonce = $request->get_header( 'X-WP-Nonce' ); if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { return new WP_Error( 'mybot_invalid_nonce', __( 'Your session has expired. Please reload the page.', 'aihouse' ), array( 'status' => 403 ) ); } if ( ! $this->settings->is_enabled() ) { return new WP_Error( 'mybot_disabled', __( 'The chatbot is currently disabled.', 'aihouse' ), array( 'status' => 503 ) ); } return true; } /** * Administrators only. * * @return true|WP_Error */ public function verify_admin() { if ( ! current_user_can( 'manage_options' ) ) { return new WP_Error( 'mybot_forbidden', __( 'You do not have permission to do this.', 'aihouse' ), array( 'status' => 403 ) ); } return true; } /** * Visitor identification key used for rate limiting. * * Logged-in users are counted by ID and visitors by hashed IP — the raw IP is never stored * in the database, to avoid privacy issues. * * @return string */ public function bucket_key() { $user_id = get_current_user_id(); if ( $user_id > 0 ) { return 'u' . $user_id; } return 'i' . substr( hash( 'sha256', $this->client_ip() . '|' . wp_salt( 'nonce' ) ), 0, 40 ); } /** * Applies the rate limit to the current request. * * @return true|WP_Error */ public function enforce_rate_limit() { $result = $this->rate_limiter->hit( $this->bucket_key() ); if ( $result['allowed'] ) { return true; } $error = new WP_Error( 'mybot_rate_limited', __( 'You are sending questions too fast. Please try again in a few seconds.', 'aihouse' ), array( 'status' => 429, 'retry_after' => $result['retry_after'], ) ); return $error; } /** * Visitor IP address. * * By default only REMOTE_ADDR is trusted. Forwarded headers are read only when a filter enables it, * because headers are sent by the client and can therefore be forged. * * @return string */ public function client_ip() { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '0.0.0.0'; /** * Enables reading forwarded headers when the site sits behind a trusted proxy. * * @param bool $trust Whether to trust the X-Forwarded-For header. */ if ( apply_filters( 'mybot_trust_proxy_headers', false ) ) { foreach ( array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP' ) as $header ) { if ( empty( $_SERVER[ $header ] ) ) { continue; } $candidate = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ); $candidate = trim( explode( ',', $candidate )[0] ); if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) { $ip = $candidate; break; } } } return $ip; } /** * Generates a new session ID. * * @return string */ public function new_session_id() { return wp_generate_password( 32, false, false ); } /** * Sanitizes the session ID submitted by the client. * * @param string $session_id Raw session ID. * @return string */ public function clean_session_id( $session_id ) { $session_id = preg_replace( '/[^a-zA-Z0-9\-_]/', '', (string) $session_id ); return substr( (string) $session_id, 0, 64 ); } }