' . esc_html__(
'This plugin requires a publicly accessible HTTPS domain. Localhost and local development environments are not supported.',
'ai-chatbot-for-support-e-commerce'
) . '
';
echo '
';
}
public function dismiss_domain_notice() {
check_ajax_referer( 'acsec_chatbot_notice_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error();
}
update_user_meta(
get_current_user_id(),
'acsec_chatbot_domain_notice_dismissed',
1
);
wp_send_json_success();
}
/**
* Creates the "AI Chatbot Content" tag if it does not already exist.
*/
public function acsec_create_chatbot_tag() {
$tag_name = 'AI Chatbot Content';
$taxonomy = 'post_tag';
// 1. Check if the term already exists to avoid duplicates
// Since WP 6.0, term_exists results are cached for better performance.
if ( ! term_exists( $tag_name, $taxonomy ) ) {
// 2. Insert the term into the post_tag taxonomy
wp_insert_term(
$tag_name,
$taxonomy,
array(
'description' => 'Posts to train AI Chatbot responses.',
'slug' => 'ai-chatbot-content'
)
);
}
}
public function main_settings_section_callback() {
// echo '
' . esc_html__( 'Note: Note: This plugin requires a publicly accessible HTTPS domain. Localhost and local development environments are not supported.' ) . '
';
}
public function acsec_chatbot_chatbot_title_callback() {
$chatbot_title = get_option( 'acsec_chatbot_chatbot_title', 'AI Chatbot' );
echo '';
echo '
' . esc_html__( 'The title displayed on the chatbot widget header.', 'ai-chatbot-for-support-e-commerce' ) . '
';
}
public function node_url_field_callback() {
$node_url = get_option( 'acsec_chatbot_node_url', 'https://your-rag-node.com/' );
echo '';
echo '
' . esc_html__( 'The base URL of your AI Chatbot Node Server.', 'ai-chatbot-for-support-e-commerce' ) . '
';
}
public function settings_page_html() {
// Load the HTML for the admin page
$path = ACSEC_PLUGIN_DIR . 'views/acsec-admin-settings.php';
if ( file_exists( $path ) ) {
include $path;
} else {
echo '
Error: Admin settings view file not found at ' . esc_html( $path ) . '
';
}
}
// --- NEW HANDLER: Saves AI Model/Provider configuration locally via AJAX ---
public function handle_ai_config_save() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Permission denied.' ) );
}
check_ajax_referer( 'acsec-chatbot-save-config', 'security' );
$active_provider = isset( $_POST['active_provider'] )
? sanitize_text_field( wp_unslash( $_POST['active_provider'] ) )
: 'gemini';
$openai_model = isset( $_POST['openai_model'] )
? sanitize_text_field( wp_unslash( $_POST['openai_model'] ) )
: 'gpt-4-turbo';
$gemini_model = isset( $_POST['gemini_model'] )
? sanitize_text_field( wp_unslash( $_POST['gemini_model'] ) )
: 'gemini-2.5-flash';
// Save the non-key configuration settings
update_option( 'acsec_chatbot_active_provider', $active_provider );
update_option( 'acsec_chatbot_openai_model', $openai_model );
update_option( 'acsec_chatbot_gemini_model', $gemini_model );
wp_send_json_success( array( 'message' => 'AI configuration saved locally.' ) );
}
// --- API HANDLERS (handle_api_keys_ajax remains similar, fetching saved config) ---
/**
* Handles the AJAX request to register the site with the Node server using
* the Challenge-Response verification flow.
*/
public function handle_site_register_ajax() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Forbidden' ), 403 );
}
check_ajax_referer( 'acsec-chatbot-site-register', 'security' );
// ... (unchanged logic)
$node_url = ACSEC_NODE_URL;
if ( empty( $node_url ) ) {
wp_send_json_error( array( 'message' => 'RAG Node URL is required. Please save it first.' ) );
}
// --- Gather site and admin information ---
$site_name = get_bloginfo( 'name' );
$site_url = get_site_url();
$admin_email = get_option( 'admin_email' );
$admin_name = 'WordPress Admin'; // Default fallback name
$admin_user = get_user_by( 'email', $admin_email );
if ( $admin_user ) {
$admin_name = $admin_user->display_name;
}
// Step 1: Get token
$token_response = wp_remote_post( "$node_url/api/site/request-token", [
'method' => 'POST',
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode(['siteUrl' => $site_url]),
]);
$token_data = json_decode( wp_remote_retrieve_body( $token_response ), true );
$token = $token_data['token'] ?? null;
if ( ! $token ) {
wp_send_json_error(['message' => 'Failed to get token from Node server.',$token_response]);
}
// Store the token in a temporary option for the public REST endpoint to serve
update_option( 'acsec_chatbot_challenge_token_temp', $token, false );
// ----------------------------------------------------------------------
// Step 2: Finalize Registration by sending the public verification URL
// ----------------------------------------------------------------------
// The public URL the Node server will call to verify ownership
// $challenge_verification_url = 'http://lamp-web-1:8000/wooc/index.php?rest_route=/acsec-chatbot/v1/challenge-token';// get_rest_url( null, 'acsec-chatbot/v1/challenge-token' );
$challenge_verification_url = get_rest_url( null, 'acsec-chatbot/v1/challenge-token' );
// Result will be something like:
// 'https://yourwordpress.com/wp-json/acsec-chatbot/v1/challenge-token'
$payload = [
'siteName' => $site_name,
'siteUrl' => $site_url,
'ownerEmail' => $admin_email,
'ownerName' => $admin_name,
'token' => $token,
'challengeVerificationUrl' => $challenge_verification_url, // <-- New key
];
$response = wp_remote_post( "$node_url/api/site/register", [
'method' => 'POST',
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode( $payload ),
'timeout' => 30, // Increased timeout for external fetch on Node side
]);
if ( is_wp_error( $response ) ) {
wp_send_json_error( array( 'message' => 'API Error during registration: ' . $response->get_error_message() ) );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$code = wp_remote_retrieve_response_code( $response );
// ----------------------------------------------------------------------
// Step 3: Cleanup and Final Key Storage
// ----------------------------------------------------------------------
// Always delete the temporary challenge token immediately after the attempt
delete_option( 'acsec_chatbot_challenge_token_temp' );
if ( 200 === $code && ! empty( $body['siteId'] ) && ! empty( $body['apiKey'] ) ) {
update_option( 'acsec_chatbot_site_id', $body['siteId'] );
update_option( 'acsec_chatbot_api_key', $body['apiKey'] );
wp_send_json_success( array(
'message' => 'Site registered successfully!',
'siteId' => $body['siteId'],
'apiKey' => $body['apiKey'],
) );
} else {
$error_message = isset( $body['message'] ) ? $body['message'] : 'Registration failed with status code: ' . $code;
wp_send_json_error( array( 'message' => $error_message ) );
}
}
/**
* (B) Handles sending user-provided AI keys (e.g., OpenAI, Gemini) and configuration to the Node.
*/
public function handle_api_keys_ajax() {
check_ajax_referer( 'acsec-chatbot-send-keys', 'security' );
$node_url = ACSEC_NODE_URL;
$site_id = get_option( 'acsec_chatbot_site_id' );
$api_key = get_option( 'acsec_chatbot_api_key' ); // Site API Key
if ( empty( $node_url ) || empty( $site_id ) || empty( $api_key ) ) {
wp_send_json_error( array( 'message' => 'Site Registration is incomplete. Cannot send AI keys.' ) );
}
// --- Retrieve AI Keys from POST Data (sent via JavaScript) ---
$openai_key_default = isset( $_POST['openai_key'] ) ? sanitize_text_field( wp_unslash( $_POST['openai_key'] ) ) : '' ;
$gemini_key_default= isset( $_POST['gemini_key'] ) ? sanitize_text_field( wp_unslash( $_POST['gemini_key'] ) ) : '' ;
$ai_keys = array(
'OPENAI_API_KEY' => $openai_key_default,
'GEMINI_API_KEY' => $gemini_key_default,
);
// --- Retrieve AI Configuration from Saved Options (saved by handle_ai_config_save) ---
$active_provider = get_option( 'acsec_chatbot_active_provider', 'gemini' );
$openai_model = get_option( 'acsec_chatbot_openai_model', 'gpt-4-turbo' );
$gemini_model = get_option( 'acsec_chatbot_gemini_model', 'gemini-2.5-flash-preview-09-2025' );
// Payload for the RAG Node
$payload = array(
'siteId' => $site_id,
'keys' => $ai_keys,
'config' => [
'activeProvider' => $active_provider,
'openaiModel' => $openai_model,
'geminiModel' => $gemini_model,
]
);
$api_endpoint = trailingslashit( $node_url ) . 'api/site/configure-ai-keys';
$response = wp_remote_post( $api_endpoint, array(
'method' => 'POST',
'headers' => array(
'Content-Type' => 'application/json',
'x-site-key' => $api_key,
),
'body' => json_encode( $payload ),
'timeout' => 30,
) );
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
$message = is_wp_error( $response ) ? $response->get_error_message() : 'Failed to send keys. Check Node logs.';
wp_send_json_error( array( 'message' => 'Error sending AI keys: ' . $message , 'payload'=>$payload ) );
}
// Keys successfully sent, record timestamp
update_option( 'acsec_chatbot_keys_sent', time() );
wp_send_json_success( array( 'message' => 'AI keys and configuration sent successfully.' ) );
}
/**
* (C) Data Push Logic - The Universal Content Handler
* ... (unchanged)
*/
public function handle_data_push_ajax() {
check_ajax_referer( 'acsec-chatbot-data-push', 'security' );
// ... (unchanged logic)
$node_url = ACSEC_NODE_URL;
$api_key = get_option( 'acsec_chatbot_api_key' );
if ( empty( $node_url ) || empty( $api_key ) ) {
wp_send_json_error( array( 'message' => 'Please complete Site Registration first.' ) );
}
$posted_data_types = isset( $_POST['data_types'] ) && is_array( $_POST['data_types'] )
? array_map( 'sanitize_text_field', wp_unslash( $_POST['data_types'] ) )
: [];
if ( empty( $posted_data_types ) ) {
wp_send_json_error( array( 'message' => 'Please Select Data Synchronous types!' ) );
}
update_option( 'acsec_chatbot_data_push_types', $posted_data_types ); // Save selected data types
// --- 1. Gather ALL Universal WordPress Data ---
$data_to_send = [
'faqs' => [],
'policies' => [],
'posts' => [],
'products' => [],
];
if(in_array('faqs', $posted_data_types)){
$data_to_send['faqs']=$this->fetch_faqs_cpt( 'acsec_chatbot_faq' );
// echo "A";
}
if(in_array('pages', $posted_data_types)){
$data_to_send['pages']=$this->fetch_wordpress_pages_non_policy();
// echo "B";
}
if(in_array('posts',$posted_data_types) ){
$data_to_send['posts']=$this->fetch_acsec_content_posts();
// echo "C";
}
if(in_array('policies',$posted_data_types)){
$data_to_send['policies']=$this->fetch_acsec_content_policy_pages();
// echo "D";
}
// --- 2. Conditional WooCommerce Data ---
if ( function_exists( 'WC' ) && in_array('products',$posted_data_types) ) {
$data_to_send['products'] = $this->fetch_woocommerce_products();
}
// --- 3. Prepare and Send the HTTP Request ---
$api_endpoint = trailingslashit( $node_url ) . 'api/site/push-data';
$response = wp_remote_post( $api_endpoint, array(
'method' => 'POST',
'headers' => array(
'Content-Type' => 'application/json',
'x-site-key' => $api_key, // REQUIRED Header
),
'body' => json_encode( $data_to_send ),
'timeout' => 90, // Increased timeout for large data sets
) );
if ( is_wp_error( $response ) ) {
wp_send_json_error( array( 'message' => 'Data Push API Error: ' . $response->get_error_message() ) );
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$code = wp_remote_retrieve_response_code( $response );
if ( 200 === $code ) {
$success_message = isset( $body['message'] ) ? $body['message'] : 'Content successfully pushed to the RAG Node.';
wp_send_json_success( array( 'message' => $success_message ) );
} else {
$error_message = isset( $body['message'] ) ? $body['message'] : 'Data push failed with status code: ' . $code.json_encode($data_to_send);
wp_send_json_error( array( 'message' => $error_message ) );
}
}
// --- CONTENT FETCHING HELPERS ---
/**
* (D) Fetch standard published posts.
*/
private function fetch_wordpress_posts() {
$posts_data = [];
$posts = get_posts( array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 100,
'fields' => 'ids', // Optimization: fetch only IDs first
) );
foreach ( $posts as $post_id ) {
$post = get_post( $post_id );
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
$contentV=wp_strip_all_tags( apply_filters( 'the_content', $post->post_content ) );
$posts_data[] = [
'id' => $post->ID,
'type' => $post->post_type,
'title' => $post->post_title,
'content' => $contentV,
'url' => get_permalink( $post->ID ),
];
}
return $posts_data;
}
/**
* (E) Fetch standard published pages, excluding policy pages (which are handled separately).
*/
private function fetch_wordpress_pages_non_policy() {
// For simplicity, we assume pages that don't contain 'policy' in the title or content are standard.
// A better implementation would use a meta key or category check.
$pages_data = [];
$pages = get_pages( array(
'post_status' => 'publish',
'posts_per_page' => 100,
) );
foreach ( $pages as $page ) {
if ( false === stripos( $page->post_title, 'policy' ) && false === stripos( $page->post_content, 'privacy policy' ) ) {
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
$contentV = wp_strip_all_tags( apply_filters( 'the_content', $page->post_content ) );
$pages_data[] = [
'id' => $page->ID,
'type' => $page->post_type,
'title' => $page->post_title,
'content' => $contentV,
'url' => get_permalink( $page->ID ),
];
}
}
return $pages_data;
}
/**
* (F) Fetch policy-related pages (e.g., Privacy, Terms).
*/
/**
* (F) Fetches pages specifically designated for AI Chatbot Content.
* Refinement: Targets pages either by common policy slugs (automated fallback) or
* a user-defined list of Page IDs (user preference), avoiding the need for taxonomies on pages.
*/
private function fetch_acsec_content_policy_pages() {
$rag_content_data = [];
// --- 1. Define Search Criteria ---
// Placeholder for User-Defined IDs (Assumes this setting will be created in admin-settings.php)
// In the final plugin, this would be loaded from a setting, e.g.:
// $user_ids_setting = get_option('acsec_chatbot_rag_page_ids', '');
$user_ids_setting = ''; // Mock empty string for demonstration
$target_ids = array_filter( array_map( 'absint', explode( ',', $user_ids_setting ) ) );
// Fallback Slugs (Used only if $target_ids is empty)
$policy_slugs = [
'privacy-policy',
'terms-of-service',
'terms-and-conditions',
'terms-conditions',
'terms-conditions-of-service',
'cookie-policy',
'data-protection-policy',
'disclaimer',
'refund-policy',
'shipping-policy',
];
$args = array(
'post_type' => 'page',
'post_status' => 'publish',
'posts_per_page' => 100,
);
// --- 2. Build Efficient WP_Query ---
// A. If user has defined specific IDs, prioritize only those IDs (most precise).
if ( ! empty( $target_ids ) ) {
$args['post__in'] = $target_ids;
} else {
// B. If no IDs are defined, fall back to searching by common policy slugs.
// This is efficient as it uses 'post_name__in' to query the database directly.
$args['post_name__in'] = $policy_slugs;
}
// --- 3. Execute and Process Query ---
$pages = get_posts( $args );
// Process the small, filtered result set (limiting content to 1000 words)
foreach ( $pages as $page ) {
// Step A: Get the full content, apply filters (shortcodes), and strip HTML tags.
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
$full_content = wp_strip_all_tags( apply_filters( 'the_content', $page->post_content ) );
// Step B: Limit content to 1000 words for RAG indexing efficiency.
$limited_content = wp_trim_words( $full_content, 1000, '' );
$rag_content_data[] = [
'id' => $page->ID,
'type' => $page->post_type,
'title' => $page->post_title,
'content' => $limited_content,
'url' => get_permalink( $page->ID ),
];
}
return $rag_content_data;
}
/**
* (G) Fetch custom post type content, typically FAQs.
*/
private function fetch_faqs_cpt( $cpt_slug ) {
$faqs_data = [];
$faqs = get_posts( array(
'post_type' => $cpt_slug,
'post_status' => 'publish',
'posts_per_page' => 100,
'fields' => 'ids',
) );
foreach ( $faqs as $faq_id ) {
$post = get_post( $faq_id );
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
$contentV = wp_strip_all_tags( apply_filters( 'the_content', $page->post_content ) );
$faqs_data[] = [
'id' => $post->ID,
'title' => $post->post_title,
'content' => $contentV,
'url' => get_permalink( $post->ID ),
];
}
return $faqs_data;
}
/**
* (H) Fetch WooCommerce Products (New/Refined Helper)
*/
private function fetch_woocommerce_products() {
if ( ! function_exists( 'wc_get_products' ) ) {
return []; // Safety check
}
$products_data = [];
$products = wc_get_products( array(
'limit' => 100, // Limit to 100 to prevent massive memory usage
'status' => 'publish',
'return' => 'objects'
) );
foreach ( $products as $product ) {
$attributes = [];
foreach ( $product->get_attributes() as $attribute_name => $attribute ) {
$attributes[] = $attribute_name . ': ' . implode( ', ', $attribute->get_options() );
}
$products_data[] = [
'id' => $product->get_id(),
'title' => $product->get_name(),
'description' => $product->get_description(), // Full description
'short_description' => $product->get_short_description(),
'price' => $product->get_price(),
'sku' => $product->get_sku(),
'attributes' => implode( '; ', $attributes ),
'dimensions' => $product->get_dimensions( false ), // width, height, length
'url' => $product->get_permalink(),
'image_url' => wp_get_attachment_url( $product->get_image_id(), 'full' ), // Main product image URL
'images_gallery'=> array_map( function( $image_id ) {
return wp_get_attachment_url( $image_id, 'full' );
}, $product->get_gallery_image_ids() ),
];
}
return $products_data;
}
/**
* (F) Fetches pages specifically designated for AI Chatbot Content.
* Uses tax_query to efficiently filter posts by the 'ai-chatbot-content' tag.
* Performance is optimized with:
* - Limited posts_per_page (100)
* - no_found_rows for faster queries
* - fields='ids' to minimize memory usage
* - Disabled cache updates for better performance
* This runs only during admin data sync, not on frontend requests.
*/
private function fetch_acsec_content_posts() {
$rag_content_data = [];
$rag_term_slug = 'ai-chatbot-content';
$args = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 100, // Avoid -1 for performance
'no_found_rows' => true, // Keeps query fast
'fields' => 'ids', // Only fetch IDs initially
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
// tax_query is necessary here to filter posts by specific tag efficiently
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
'tax_query' => array(
array(
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => $rag_term_slug,
'include_children' => false,
),
),
);
$pages = get_posts( $args );
// 3. Process the smaller, filtered result set
foreach ( $pages as $page ) {
// Step A: Get the full content, apply filters (shortcodes), and strip HTML tags.
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
$full_content = wp_strip_all_tags( apply_filters( 'the_content', $page->post_content ) );
// Step B: Refinement - Limit content to 1000 words for RAG indexing efficiency.
// We pass an empty string ('') as the third argument to avoid the default '...' ellipsis.
$limited_content = wp_trim_words( $full_content, 1000, '' );
$rag_content_data[] = [
'id' => $page->ID,
'type' => $page->post_type,
'title' => $page->post_title,
'content' => $limited_content, // Now uses the limited content
'url' => get_permalink( $page->ID ),
];
}
return $rag_content_data;
}
}