WP_REST_Server::READABLE, 'callback' => 'avca_rest_search', 'permission_callback' => '__return_true', 'args' => array( 'q' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'avca_rest_validate_text', ), ), ) ); register_rest_route( AVCA_REST_NAMESPACE, '/lead', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => 'avca_rest_lead', 'permission_callback' => '__return_true', 'args' => array( 'type' => array( 'type' => 'string', 'default' => 'lead', 'enum' => array( 'lead', 'callback' ), ), 'preferred_time' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ), 'name' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'email' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_email', ), 'phone' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ), 'question' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', ), 'message' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_textarea_field', ), 'page' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'esc_url_raw', ), 'website' => array( 'type' => 'string', 'default' => '', ), ), ) ); register_rest_route( AVCA_REST_NAMESPACE, '/unanswered', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => 'avca_rest_unanswered', 'permission_callback' => '__return_true', 'args' => array( 'question' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'avca_rest_validate_text', ), ), ) ); } add_action( 'rest_api_init', 'avca_register_rest_routes' ); /** * Validate a short visitor text (1–300 characters). * * @param mixed $value Value. * @return bool */ function avca_rest_validate_text( $value ) { if ( ! is_string( $value ) ) { return false; } $length = function_exists( 'mb_strlen' ) ? mb_strlen( trim( $value ) ) : strlen( trim( $value ) ); return $length >= 1 && $length <= 300; } /** * Simple per-visitor rate limit using transients. * * @param string $action Action name. * @param int $limit Max requests per window. * @param int $window Window in seconds. * @return bool True when the visitor is over the limit. */ function avca_rate_limited( $action, $limit, $window ) { $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown'; $key = 'avca_rl_' . $action . '_' . md5( $ip ); $count = (int) get_transient( $key ); if ( $count >= $limit ) { return true; } set_transient( $key, $count + 1, $window ); return false; } /** * Error response helper. * * @param string $code Error code. * @param string $message Message. * @param int $status HTTP status. * @return WP_Error */ function avca_rest_error( $code, $message, $status ) { return new WP_Error( $code, $message, array( 'status' => $status ) ); } /** * Site search: returns up to 3 published items from the enabled post types. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ function avca_rest_search( $request ) { $settings = avca_get_settings(); if ( empty( $settings['site_search'] ) || empty( $settings['search_types'] ) ) { return rest_ensure_response( array() ); } if ( avca_rate_limited( 'search', 60, 10 * MINUTE_IN_SECONDS ) ) { return avca_rest_error( 'avca_rate_limited', __( 'Too many requests. Please try again later.', 'avi-support-assistant' ), 429 ); } $query = new WP_Query( array( 's' => $request['q'], 'post_type' => $settings['search_types'], 'post_status' => 'publish', 'posts_per_page' => 3, 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'has_password' => false, ) ); $results = array(); foreach ( $query->posts as $post ) { $results[] = array( 'title' => wp_strip_all_tags( get_the_title( $post ) ), 'url' => get_permalink( $post ), 'excerpt' => wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post->post_excerpt ? $post->post_excerpt : $post->post_content ) ), 18 ), ); } return rest_ensure_response( $results ); } /** * Save a lead from the chat. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ function avca_rest_lead( $request ) { $is_callback = 'callback' === $request['type']; if ( ! avca_get_setting( $is_callback ? 'callback' : 'lead_form' ) ) { return avca_rest_error( 'avca_disabled', __( 'The contact form is turned off.', 'avi-support-assistant' ), 403 ); } // Honeypot: real visitors never fill this hidden field. if ( '' !== trim( (string) $request['website'] ) ) { return rest_ensure_response( array( 'success' => true ) ); } $data = array( 'type' => $is_callback ? 'callback' : 'lead', 'preferred_time' => mb_substr( trim( (string) $request['preferred_time'] ), 0, 100 ), 'name' => mb_substr( trim( (string) $request['name'] ), 0, 100 ), 'email' => (string) $request['email'], 'phone' => mb_substr( trim( (string) $request['phone'] ), 0, 30 ), 'question' => mb_substr( trim( (string) $request['question'] ), 0, 300 ), 'message' => mb_substr( trim( (string) $request['message'] ), 0, 1000 ), 'page' => (string) $request['page'], ); if ( '' === $data['name'] ) { return avca_rest_error( 'avca_invalid_name', __( 'Please enter your name.', 'avi-support-assistant' ), 400 ); } // Email is required for leads; callbacks need a phone number instead. if ( ( ! $is_callback || '' !== $data['email'] ) && ! is_email( $data['email'] ) ) { return avca_rest_error( 'avca_invalid_email', __( 'Please enter a valid email address.', 'avi-support-assistant' ), 400 ); } if ( $is_callback && '' === $data['phone'] ) { return avca_rest_error( 'avca_invalid_phone', __( 'Please enter your phone number.', 'avi-support-assistant' ), 400 ); } if ( '' !== $data['phone'] && ! preg_match( '/^[0-9+()\-.\s]{6,30}$/', $data['phone'] ) ) { return avca_rest_error( 'avca_invalid_phone', __( 'Please enter a valid phone number.', 'avi-support-assistant' ), 400 ); } // Only keep page URLs from this site. if ( '' !== $data['page'] && wp_parse_url( $data['page'], PHP_URL_HOST ) !== wp_parse_url( home_url(), PHP_URL_HOST ) ) { $data['page'] = ''; } if ( avca_rate_limited( 'lead', 5, HOUR_IN_SECONDS ) ) { return avca_rest_error( 'avca_rate_limited', __( 'You have already sent your details. Our team will contact you soon.', 'avi-support-assistant' ), 429 ); } $lead_id = avca_create_lead( $data ); if ( is_wp_error( $lead_id ) ) { return avca_rest_error( 'avca_save_failed', __( 'Sorry, something went wrong. Please try again.', 'avi-support-assistant' ), 500 ); } return rest_ensure_response( array( 'success' => true ) ); } /** * Log a question the bot could not answer. * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ function avca_rest_unanswered( $request ) { if ( avca_get_setting( 'log_unanswered' ) && ! avca_rate_limited( 'unanswered', 30, HOUR_IN_SECONDS ) ) { avca_log_unanswered( (string) $request['question'] ); } return rest_ensure_response( array( 'success' => true ) ); }