namespace = 'ai-support-assistant/v1'; add_action( 'rest_api_init', array( $this, 'register_routes' ) ); } /** * Register API routes. * * @return void */ public function register_routes(): void { // Admin: Index Media Attachment. register_rest_route( $this->namespace, '/admin/documents/upload-attachment', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'upload_attachment' ), 'permission_callback' => array( $this, 'check_admin_permission' ), 'args' => array( 'attachment_id' => array( 'required' => true, 'sanitize_callback' => 'absint', ), ), ), ) ); // Admin: Crawl and Index Webpage URL. register_rest_route( $this->namespace, '/admin/documents/crawl-url', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'crawl_url' ), 'permission_callback' => array( $this, 'check_admin_permission' ), 'args' => array( 'url' => array( 'required' => true, 'sanitize_callback' => 'esc_url_raw', ), ), ), ) ); // Admin: Reindex Document. register_rest_route( $this->namespace, '/admin/documents/(?P\d+)/reindex', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'reindex_document' ), 'permission_callback' => array( $this, 'check_admin_permission' ), ), ) ); // Admin: Delete Document. register_rest_route( $this->namespace, '/admin/documents/(?P\d+)', array( array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_document' ), 'permission_callback' => array( $this, 'check_admin_permission' ), ), ) ); // Admin: Get Document Chunks Preview. register_rest_route( $this->namespace, '/admin/documents/(?P\d+)/chunks', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_document_chunks' ), 'permission_callback' => array( $this, 'check_admin_permission' ), ), ) ); // Admin: Export Logs. register_rest_route( $this->namespace, '/history/export', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'export_history' ), 'permission_callback' => array( $this, 'check_admin_permission' ), ), ) ); // Admin: Get & Delete Chat History Log — both methods on same URI in a single registration. register_rest_route( $this->namespace, '/admin/history/(?P\d+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_history_log' ), 'permission_callback' => array( $this, 'check_admin_permission' ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_history_log' ), 'permission_callback' => array( $this, 'check_admin_permission' ), ), ) ); // Public: Chat Query Endpoint. register_rest_route( $this->namespace, '/chat', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'handle_chat' ), 'permission_callback' => '__return_true', // Nonce checked in callback for fine-grained visitor filtering. 'args' => array( 'question' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), 'session_id' => array( 'required' => false, 'sanitize_callback' => 'sanitize_text_field', ), 'nonce' => array( 'required' => false, 'sanitize_callback' => 'sanitize_text_field', ), ), ), ) ); } /** * Check if current user is admin. * * @return bool */ public function check_admin_permission(): bool { return current_user_can( 'manage_options' ); } /** * REST Callback: Index a WP Media Attachment. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function upload_attachment( WP_REST_Request $request ) { $attachment_id = $request->get_param( 'attachment_id' ); try { $doc_id = DocumentManager::upload_attachment( $attachment_id ); if ( $doc_id > 0 ) { return new WP_REST_Response( array( 'success' => true, 'document_id' => $doc_id, 'message' => __( 'Document indexed successfully.', 'ai-support-assistant' ), ), 200 ); } return new WP_Error( 'ai_support_assistant_upload_failed', __( 'Unable to process and index the document.', 'ai-support-assistant' ), array( 'status' => 500 ) ); } catch ( \Exception $e ) { return new WP_Error( 'ai_support_assistant_upload_failed', $e->getMessage(), array( 'status' => 500 ) ); } } /** * REST Callback: Crawl and index URL. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function crawl_url( WP_REST_Request $request ) { $url = $request->get_param( 'url' ); try { $doc_id = DocumentManager::crawl_and_index_url( $url ); if ( $doc_id > 0 ) { return new WP_REST_Response( array( 'success' => true, 'document_id' => $doc_id, 'message' => __( 'URL crawled and indexed successfully.', 'ai-support-assistant' ), ), 200 ); } return new WP_Error( 'ai_support_assistant_crawl_failed', __( 'Unable to process and index the URL.', 'ai-support-assistant' ), array( 'status' => 500 ) ); } catch ( \Exception $e ) { return new WP_Error( 'ai_support_assistant_crawl_failed', $e->getMessage(), array( 'status' => 500 ) ); } } /** * REST Callback: Reindex document. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function reindex_document( WP_REST_Request $request ) { $id = $request->get_param( 'id' ); try { $success = DocumentManager::index_document( $id ); if ( $success ) { return new WP_REST_Response( array( 'success' => true, 'message' => __( 'Document re-indexed successfully.', 'ai-support-assistant' ), ), 200 ); } return new WP_Error( 'ai_support_assistant_reindex_failed', __( 'Re-indexing process failed.', 'ai-support-assistant' ), array( 'status' => 500 ) ); } catch ( \Exception $e ) { return new WP_Error( 'ai_support_assistant_reindex_failed', $e->getMessage(), array( 'status' => 500 ) ); } } /** * REST Callback: Delete document. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function delete_document( WP_REST_Request $request ) { $id = $request->get_param( 'id' ); $success = DocumentManager::delete_document( $id ); if ( $success ) { return new WP_REST_Response( array( 'success' => true, 'message' => __( 'Document and vector embeddings deleted.', 'ai-support-assistant' ), ), 200 ); } return new WP_Error( 'ai_support_assistant_delete_failed', __( 'Delete document operation failed.', 'ai-support-assistant' ), array( 'status' => 500 ) ); } /** * REST Callback: Retrieve document chunks for admin preview list. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function get_document_chunks( WP_REST_Request $request ) { global $wpdb; $id = $request->get_param( 'id' ); $chunks_table = DatabaseManager::get_chunks_table(); $chunks = $wpdb->get_results( $wpdb->prepare( "SELECT chunk_number, page_number, chunk_text, token_count FROM %i WHERE document_id = %d ORDER BY chunk_number ASC", $chunks_table, $id ), ARRAY_A ); return new WP_REST_Response( array( 'chunks' => $chunks ), 200 ); } /** * REST Callback: Export logs CSV. * * Streams a CSV file download directly. ChatHistory::export_csv() will * send HTTP headers and call exit(), so the return below is unreachable * in practice but satisfies the REST API return-type contract. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function export_history( WP_REST_Request $request ) { ChatHistory::export_csv(); // Unreachable — export_csv() always calls exit() after streaming. return new WP_REST_Response( array( 'success' => true ), 200 ); // phpcs:ignore Generic.CodeAnalysis.UnreachableCode.Found } /** * REST Callback: Handle public chat request. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function handle_chat( WP_REST_Request $request ) { $nonce = $request->get_param( 'nonce' ); $wp_nonce = $request->get_header( 'x_wp_nonce' ); $authorized = false; if ( ! empty( $nonce ) && wp_verify_nonce( $nonce, 'ai_support_assistant_action' ) ) { $authorized = true; } elseif ( ! empty( $wp_nonce ) && wp_verify_nonce( $wp_nonce, 'wp_rest' ) ) { $authorized = true; } elseif ( current_user_can( 'manage_options' ) ) { $authorized = true; } if ( ! $authorized ) { return new WP_Error( 'ai_support_assistant_unauthorized', __( 'Invalid security nonce.', 'ai-support-assistant' ), array( 'status' => 403 ) ); } $question = $request->get_param( 'question' ); $session_id = $request->get_param( 'session_id' ); // If no session ID provided, generate a unique random hash. if ( empty( $session_id ) ) { $session_id = md5( uniqid( (string) wp_rand(), true ) ); } try { $result = ChatService::answer_question( $session_id, $question ); return new WP_REST_Response( array( 'answer' => $result['answer'], 'citations' => $result['citations'], 'session_id' => $session_id, 'enable_streaming' => ! empty( Settings::get_instance()->get( 'enable_streaming', 1 ) ) ? 1 : 0, ), 200 ); } catch ( \Throwable $e ) { Logger::error( 'Chat request error: ' . $e->getMessage() ); return new WP_REST_Response( array( 'answer' => __( 'Sorry, an unexpected error occurred while processing your query. Please try again.', 'ai-support-assistant' ), 'citations' => array(), 'session_id' => $session_id, 'enable_streaming' => 0, ), 200 ); } } /** * REST Callback: Delete a single history log entry. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function delete_history_log( WP_REST_Request $request ) { global $wpdb; $id = $request->get_param( 'id' ); $chat_table = DatabaseManager::get_chat_history_table(); $deleted = $wpdb->delete( $chat_table, array( 'id' => $id ), array( '%d' ) ); if ( false !== $deleted ) { return new WP_REST_Response( array( 'success' => true, 'message' => __( 'Log entry deleted successfully.', 'ai-support-assistant' ), ), 200 ); } return new WP_Error( 'ai_support_assistant_delete_failed', __( 'Delete log operation failed.', 'ai-support-assistant' ), array( 'status' => 500 ) ); } /** * REST Callback: Get details of a single log entry. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function get_history_log( WP_REST_Request $request ) { global $wpdb; $id = $request->get_param( 'id' ); $chat_table = DatabaseManager::get_chat_history_table(); $log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM %i WHERE id = %d", $chat_table, $id ) ); if ( $log ) { $messages = array(); if ( ! empty( $log->messages ) ) { $decoded = json_decode( $log->messages, true ); if ( is_array( $decoded ) ) { $messages = $decoded; } } if ( empty( $messages ) ) { if ( ! empty( $log->question ) ) { $messages[] = array( 'role' => 'user', 'content' => $log->question, 'time' => $log->created_at, ); } if ( ! empty( $log->answer ) ) { $messages[] = array( 'role' => 'assistant', 'content' => $log->answer, 'time' => $log->created_at, ); } } return new WP_REST_Response( array( 'success' => true, 'log' => array( 'id' => (int) $log->id, 'session_id' => $log->session_id, 'question' => $log->question, 'answer' => $log->answer, 'messages' => $messages, 'provider' => $log->provider, 'model' => $log->model, 'asked_at' => mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $log->created_at ), 'updated_at' => ! empty( $log->updated_at ) ? mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $log->updated_at ) : mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $log->created_at ), ), ), 200 ); } return new WP_Error( 'ai_support_assistant_not_found', __( 'Log entry not found.', 'ai-support-assistant' ), array( 'status' => 404 ) ); } }