init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks() {
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_notices', array( $this, 'display_admin_notices' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_action( 'admin_post_agentorie_chat_save_settings', array( $this, 'handle_save_settings' ) );
add_action( 'admin_post_agentorie_chat_activate_license', array( $this, 'handle_activate_license' ) );
add_action( 'admin_post_agentorie_chat_deactivate_license', array( $this, 'handle_deactivate_license' ) );
add_action( 'admin_post_agentorie_chat_sync_license', array( $this, 'handle_sync_license' ) );
add_action( 'admin_post_agentorie_chat_delete_conversation', array( $this, 'handle_delete_conversation' ) );
add_action( 'admin_post_agentorie_chat_delete_selected', array( $this, 'handle_delete_selected' ) );
add_action( 'admin_post_agentorie_chat_export_conversations', array( $this, 'handle_export' ) );
add_action( 'admin_post_agentorie_chat_export_contacts', array( $this, 'handle_export_contacts' ) );
add_action( 'admin_post_agentorie_chat_download_logs', array( $this, 'handle_download_logs' ) );
add_action( 'admin_post_agentorie_chat_clear_logs', array( $this, 'handle_clear_logs' ) );
// AJAX handler for content indexing.
add_action( 'wp_ajax_agentorie_chat_reindex_content', array( $this, 'handle_reindex_content' ) );
add_action( 'wp_ajax_agentorie_chat_save_content_indexing', array( $this, 'handle_save_content_indexing_ajax' ) );
}
/**
* Add admin menu pages.
*
* @return void
*/
public function add_admin_menu() {
add_menu_page(
__( 'Agentorie Chat', 'agentorie-chat' ),
$this->get_menu_title(),
'manage_options',
'agentorie-chat',
array( $this, 'display_dashboard_page' ),
'dashicons-format-chat',
30
);
add_submenu_page( 'agentorie-chat', __( 'Dashboard', 'agentorie-chat' ), __( 'Dashboard', 'agentorie-chat' ), 'manage_options', 'agentorie-chat', array( $this, 'display_dashboard_page' ) );
add_submenu_page( 'agentorie-chat', __( 'LiveChat', 'agentorie-chat' ), $this->get_livechat_menu_title(), 'manage_options', 'agentorie-chat-livechat', array( $this, 'display_livechat_page' ) );
add_submenu_page( 'agentorie-chat', __( 'Conversations', 'agentorie-chat' ), __( 'Conversations', 'agentorie-chat' ), 'manage_options', 'agentorie-chat-conversations', array( $this, 'display_conversations_page' ) );
add_submenu_page( 'agentorie-chat', __( 'Settings', 'agentorie-chat' ), __( 'Settings', 'agentorie-chat' ), 'manage_options', 'agentorie-chat-settings', array( $this, 'display_settings_page' ) );
}
/**
* Get menu title with unread count badge.
*
* @return string
*/
private function get_menu_title() {
$unread_count = Agentorie_Database::get_unread_count();
$title = __( 'Agentorie', 'agentorie-chat' );
if ( $unread_count > 0 ) {
$title .= sprintf( ' %d', $unread_count );
}
return $title;
}
/**
* Get LiveChat menu title with unread count badge.
*
* @return string
*/
private function get_livechat_menu_title() {
$unread_count = Agentorie_Database::get_unread_count();
$title = __( 'LiveChat', 'agentorie-chat' );
if ( $unread_count > 0 ) {
$title .= sprintf( ' %d', $unread_count );
}
return $title;
}
/**
* Enqueue admin assets.
*
* @param string $hook Current admin page hook.
* @return void
*/
public function enqueue_assets( $hook ) {
if ( ! $this->is_plugin_page( $hook ) ) {
return;
}
wp_enqueue_style( 'agentorie-chat-admin', AGENTORIE_CHAT_PLUGIN_URL . 'admin/css/admin.css', array(), AGENTORIE_CHAT_VERSION );
if ( false !== strpos( $hook, 'agentorie-chat-settings' ) ) {
wp_enqueue_style( 'agentorie-chat-settings', AGENTORIE_CHAT_PLUGIN_URL . 'admin/css/settings.css', array( 'agentorie-chat-admin' ), AGENTORIE_CHAT_VERSION );
wp_enqueue_script( 'agentorie-chat-settings', AGENTORIE_CHAT_PLUGIN_URL . 'admin/js/settings.js', array( 'jquery', 'agentorie-chat-admin' ), AGENTORIE_CHAT_VERSION, true );
wp_localize_script(
'agentorie-chat-settings',
'agentorieSettings',
array(
'i18n' => array(
// License activation.
'enterLicenseKey' => __( 'Please enter your License Key.', 'agentorie-chat' ),
'invalidFormat' => __( 'Invalid format: AG-XXXXXXXX-XXXXXXXX-XXXXXXXX', 'agentorie-chat' ),
'activating' => __( 'Activating...', 'agentorie-chat' ),
// License deactivation.
'deactivateTitle' => __( 'Deactivate License Key?', 'agentorie-chat' ),
'deactivateMessage' => __( 'The chat widget will stop working. You can reactivate anytime.', 'agentorie-chat' ),
'cancel' => __( 'Cancel', 'agentorie-chat' ),
'deactivate' => __( 'Deactivate', 'agentorie-chat' ),
// License sync.
'syncing' => __( 'Syncing...', 'agentorie-chat' ),
// Re-index.
'indexing' => __( 'Indexing...', 'agentorie-chat' ),
'reindexNow' => __( 'Re-index Now', 'agentorie-chat' ),
'indexingFailed' => __( 'Indexing failed.', 'agentorie-chat' ),
'saveToEnable' => __( 'Save settings to enable indexing.', 'agentorie-chat' ),
'indexingDisabled' => __( 'Indexing disabled.', 'agentorie-chat' ),
// Toasts.
'settingsSaved' => __( 'Settings saved!', 'agentorie-chat' ),
'licenseActivated' => __( 'License Key activated! Your chatbot is ready.', 'agentorie-chat' ),
'licenseDeactivated' => __( 'License Key deactivated.', 'agentorie-chat' ),
'licenseSynced' => __( 'License synchronized! Features updated.', 'agentorie-chat' ),
// Errors.
'errorEmptyKey' => __( 'Please enter a License Key.', 'agentorie-chat' ),
'errorInvalidKey' => __( 'Invalid License Key.', 'agentorie-chat' ),
'errorExpired' => __( 'License Key expired.', 'agentorie-chat' ),
'errorAlreadyUsed' => __( 'License Key already used on another site.', 'agentorie-chat' ),
'connectionError' => __( 'Connection error. Please try again.', 'agentorie-chat' ),
'errorGeneric' => __( 'An error occurred.', 'agentorie-chat' ),
// Debug tab.
'clearLogsConfirm' => __( 'Are you sure you want to clear all logs? This action cannot be undone.', 'agentorie-chat' ),
),
)
);
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';
if ( 'appearance' === $current_tab ) {
wp_enqueue_style( 'agentorie-chat-settings-appearance', AGENTORIE_CHAT_PLUGIN_URL . 'admin/css/settings-appearance.css', array( 'agentorie-chat-settings' ), AGENTORIE_CHAT_VERSION );
wp_enqueue_script( 'agentorie-chat-settings-appearance', AGENTORIE_CHAT_PLUGIN_URL . 'admin/js/settings-appearance.js', array( 'jquery' ), AGENTORIE_CHAT_VERSION, true );
}
if ( 'content_indexing' === $current_tab ) {
wp_enqueue_style( 'agentorie-chat-settings-content', AGENTORIE_CHAT_PLUGIN_URL . 'admin/css/settings-content.css', array( 'agentorie-chat-settings' ), AGENTORIE_CHAT_VERSION );
wp_enqueue_script( 'agentorie-chat-settings-content', AGENTORIE_CHAT_PLUGIN_URL . 'admin/js/settings-content.js', array( 'jquery', 'agentorie-chat-admin' ), AGENTORIE_CHAT_VERSION, true );
wp_localize_script(
'agentorie-chat-settings-content',
'agentorieContentIndexing',
array(
'siteUrl' => home_url( '/' ),
'wooLimit' => (int) agentorie_chat_get_setting( 'woo_indexing_limit', 10 ),
'i18n' => array(
'showList' => __( 'Show list', 'agentorie-chat' ),
'hideList' => __( 'Hide list', 'agentorie-chat' ),
'externalUrlsDetected' => __( 'External URLs detected and will be ignored:', 'agentorie-chat' ),
'allUrlsValid' => __( 'All URLs are valid.', 'agentorie-chat' ),
'saving' => __( 'Saving...', 'agentorie-chat' ),
'indexing' => __( 'Indexing...', 'agentorie-chat' ),
'saveAndUpdate' => __( 'Save & Update', 'agentorie-chat' ),
'testTip' => __( 'To test changes, start a new conversation. Use the reset button in the chat header.', 'agentorie-chat' ),
'indexingFailed' => __( 'Indexing failed.', 'agentorie-chat' ),
'connectionError' => __( 'Connection error. Please try again.', 'agentorie-chat' ),
'saveFailed' => __( 'Failed to save settings.', 'agentorie-chat' ),
'oneLine' => __( '1 line', 'agentorie-chat' ),
/* translators: %d: number of lines */
'lines' => __( '%d lines', 'agentorie-chat' ),
),
)
);
}
if ( 'ai_settings' === $current_tab ) {
wp_enqueue_style(
'agentorie-chat-settings-ai',
AGENTORIE_CHAT_PLUGIN_URL . 'admin/css/settings-ai.css',
array(),
AGENTORIE_CHAT_VERSION
);
wp_enqueue_script(
'agentorie-chat-settings-ai',
AGENTORIE_CHAT_PLUGIN_URL . 'admin/js/settings-ai.js',
array(),
AGENTORIE_CHAT_VERSION,
true
);
wp_localize_script(
'agentorie-chat-settings-ai',
'agentorieAISettings',
array(
'i18n' => array(
'settingsSaved' => __( 'AI Settings saved successfully.', 'agentorie-chat' ),
'syncFailed' => __( 'Settings saved locally, but sync to server failed. They will sync on next save.', 'agentorie-chat' ),
),
)
);
}
}
wp_enqueue_script( 'agentorie-chat-admin', AGENTORIE_CHAT_PLUGIN_URL . 'admin/js/admin.js', array( 'jquery' ), AGENTORIE_CHAT_VERSION, true );
wp_localize_script(
'agentorie-chat-admin',
'agentorieAdmin',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'agentorie_chat_admin_nonce' ),
)
);
if ( false !== strpos( $hook, 'agentorie-chat' ) || false !== strpos( $hook, 'agentorie-chat-conversations' ) ) {
wp_enqueue_script( 'agentorie-chat-chartjs', AGENTORIE_CHAT_PLUGIN_URL . 'admin/js/chart.min.js', array(), '4.5.1', true );
}
// Dashboard page (main plugin page).
if ( 'toplevel_page_agentorie-chat' === $hook ) {
wp_enqueue_style( 'agentorie-chat-dashboard', AGENTORIE_CHAT_PLUGIN_URL . 'admin/css/dashboard.css', array( 'agentorie-chat-admin' ), AGENTORIE_CHAT_VERSION );
wp_enqueue_script( 'agentorie-chat-dashboard', AGENTORIE_CHAT_PLUGIN_URL . 'admin/js/dashboard.js', array( 'agentorie-chat-chartjs' ), AGENTORIE_CHAT_VERSION, true );
}
if ( false !== strpos( $hook, 'agentorie-chat-livechat' ) ) {
$this->enqueue_livechat_assets();
}
if ( false !== strpos( $hook, 'agentorie-chat-conversations' ) ) {
$this->enqueue_conversations_assets();
}
}
/**
* Enqueue LiveChat specific assets.
*
* @return void
*/
private function enqueue_livechat_assets() {
wp_enqueue_style( 'agentorie-chat-livechat', AGENTORIE_CHAT_PLUGIN_URL . 'admin/css/livechat.css', array( 'agentorie-chat-admin' ), AGENTORIE_CHAT_VERSION );
// Main LiveChat script.
wp_enqueue_script( 'agentorie-chat-livechat', AGENTORIE_CHAT_PLUGIN_URL . 'admin/js/livechat.js', array( 'jquery', 'agentorie-chat-admin' ), AGENTORIE_CHAT_VERSION, true );
// WebSocket support (requires Socket.io).
$websocket_enabled = agentorie_chat_get_setting( 'websocket_enabled', true );
if ( $websocket_enabled ) {
wp_enqueue_script( 'agentorie-chat-socket-io', AGENTORIE_CHAT_PLUGIN_URL . 'assets/js/vendor/socket.io.min.js', array(), '4.8.2', true );
wp_enqueue_script( 'agentorie-chat-livechat-websocket', AGENTORIE_CHAT_PLUGIN_URL . 'admin/js/livechat-websocket.js', array( 'jquery', 'agentorie-chat-livechat', 'agentorie-chat-socket-io' ), AGENTORIE_CHAT_VERSION, true );
}
// Get current admin's personal LiveChat settings from user_meta.
$current_user_id = get_current_user_id();
$admin_name = get_user_meta( $current_user_id, 'agentorie_chat_livechat_name', true );
$admin_avatar = get_user_meta( $current_user_id, 'agentorie_chat_livechat_avatar', true );
// Fallback to display_name if no custom name set.
if ( empty( $admin_name ) ) {
$user = get_userdata( $current_user_id );
$admin_name = $user ? $user->display_name : __( 'Support', 'agentorie-chat' );
}
if ( empty( $admin_avatar ) ) {
$admin_avatar = '👤';
}
wp_localize_script(
'agentorie-chat-livechat',
'agentorieLivechat',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'agentorie_chat_admin_nonce' ),
'pluginUrl' => AGENTORIE_CHAT_PLUGIN_URL,
'botName' => agentorie_chat_get_setting( 'bot_name', 'Stella' ),
'adminId' => $current_user_id,
'adminName' => $admin_name,
'adminAvatar' => $admin_avatar,
'websocket' => array(
'enabled' => agentorie_chat_get_setting( 'websocket_enabled', true ),
),
'strings' => array(
'connected' => __( 'Connected', 'agentorie-chat' ),
'disconnected' => __( 'Disconnected', 'agentorie-chat' ),
'online' => __( 'Online', 'agentorie-chat' ),
'offline' => __( 'Offline', 'agentorie-chat' ),
'noConversations' => __( 'No active conversations', 'agentorie-chat' ),
'selectConversation' => __( 'Select a conversation', 'agentorie-chat' ),
'visitor' => __( 'Visitor', 'agentorie-chat' ),
'takeControl' => __( 'Take control', 'agentorie-chat' ),
'releaseToBot' => __( 'Release to bot', 'agentorie-chat' ),
'confirmRelease' => __( 'Release control to the bot?', 'agentorie-chat' ),
'typeMessage' => __( 'Type your message...', 'agentorie-chat' ),
'takeControlFirst' => __( 'Take control to send messages...', 'agentorie-chat' ),
'justNow' => __( 'Just now', 'agentorie-chat' ),
'typing' => __( 'Typing...', 'agentorie-chat' ),
'wantsAdvisor' => __( 'Wants to speak with an advisor', 'agentorie-chat' ),
'wantsToNegotiate' => __( 'Wants to negotiate', 'agentorie-chat' ),
'takenOver' => __( 'Taken over', 'agentorie-chat' ),
'sessionExpired' => __( 'Session expired. The page will reload.', 'agentorie-chat' ),
'seatLimitTitle' => __( 'Seat Limit Reached', 'agentorie-chat' ),
'seatLimitMessage' => __( 'All admin seats are currently in use.', 'agentorie-chat' ),
'seatLimitHint' => __( 'An admin can free up a seat by releasing their conversation to the bot.', 'agentorie-chat' ),
'needMoreSeats' => __( 'Need more seats?', 'agentorie-chat' ),
'seatsIncluded' => __( 'seat(s) available', 'agentorie-chat' ),
'waitOrRelease' => __( 'Please wait or release a conversation.', 'agentorie-chat' ),
'close' => __( 'Close', 'agentorie-chat' ),
'confirm' => __( 'Confirm', 'agentorie-chat' ),
'cancel' => __( 'Cancel', 'agentorie-chat' ),
// Additional strings for JS fallbacks.
'humanRequestedNotif' => __( 'A visitor needs assistance!', 'agentorie-chat' ),
'alertTitle' => __( 'New Alert', 'agentorie-chat' ),
'activeSince' => __( 'Active since', 'agentorie-chat' ),
'clickToUnlock' => __( 'Click anywhere to enable sound', 'agentorie-chat' ),
'newConversation' => __( 'New conversation', 'agentorie-chat' ),
'confirmReleaseTitle' => __( 'Release to Bot', 'agentorie-chat' ),
'humanRequested' => __( 'Requesting human', 'agentorie-chat' ),
'negotiating' => __( 'Wants to negotiate', 'agentorie-chat' ),
'adminLimitReached' => __( 'Team seat limit reached.', 'agentorie-chat' ),
'adminsConnected' => __( 'admin(s) already connected.', 'agentorie-chat' ),
/* translators: %s: admin display name who took over the conversation */
'isNowAssisting' => __( '%s is now assisting you', 'agentorie-chat' ),
/* translators: %1$s: admin display name, %2$s: bot/assistant name */
'hasHandedBack' => __( '%1$s has handed you back to %2$s', 'agentorie-chat' ),
),
)
);
}
/**
* Enqueue conversations page assets.
*
* @return void
*/
private function enqueue_conversations_assets() {
wp_enqueue_style( 'agentorie-chat-conversations', AGENTORIE_CHAT_PLUGIN_URL . 'admin/css/conversations.css', array( 'agentorie-chat-admin' ), AGENTORIE_CHAT_VERSION );
wp_enqueue_script( 'agentorie-chat-conversations', AGENTORIE_CHAT_PLUGIN_URL . 'admin/js/conversations.js', array( 'jquery', 'agentorie-chat-chartjs', 'agentorie-chat-admin' ), AGENTORIE_CHAT_VERSION, true );
// Get chart period from URL parameter.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter parameter.
$chart_period = isset( $_GET['period'] ) ? absint( wp_unslash( $_GET['period'] ) ) : 7;
$chart_period = in_array( $chart_period, array( 7, 14, 30, 90 ), true ) ? $chart_period : 7;
wp_localize_script(
'agentorie-chat-conversations',
'agentorieConversations',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'deleteUrl' => admin_url( 'admin-post.php' ),
'nonce' => wp_create_nonce( 'agentorie_chat_admin_nonce' ),
'chartData' => self::get_chart_data( $chart_period ),
'i18n' => array(
'messages' => __( 'Messages', 'agentorie-chat' ),
'errorLoading' => __( 'Error loading conversation', 'agentorie-chat' ),
'connectionError' => __( 'Connection error', 'agentorie-chat' ),
'advisor' => __( 'Advisor', 'agentorie-chat' ),
'confirmDelete' => __( 'Are you sure you want to delete this conversation?', 'agentorie-chat' ),
'confirmMultiple' => __( 'Are you sure you want to delete the selected conversations?', 'agentorie-chat' ),
/* translators: {count}: number of selected conversations, keep it as is */
'confirmDeleteMultiple' => __( 'Delete {count} conversation(s)?', 'agentorie-chat' ),
'noSelection' => __( 'Please select at least one conversation.', 'agentorie-chat' ),
'loading' => __( 'Loading...', 'agentorie-chat' ),
'confirmPurgeOld' => __( 'This will permanently delete old conversations. Continue?', 'agentorie-chat' ),
'confirmDeleteAll' => __( 'This will permanently delete ALL data for this visitor. Continue?', 'agentorie-chat' ),
// Additional strings for JS fallbacks (chart labels).
'conversations' => __( 'Conversations', 'agentorie-chat' ),
'contacts' => __( 'Contact Requests', 'agentorie-chat' ),
'adminInterventions' => __( 'Admin Interventions', 'agentorie-chat' ),
),
)
);
}
/**
* Check if current page is a plugin page.
*
* @param string $hook Current admin page hook.
* @return bool
*/
private function is_plugin_page( $hook ) {
return ( false !== strpos( $hook, 'agentorie' ) );
}
/**
* Display dashboard page.
*
* @return void
*/
public function display_dashboard_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ), '', array( 'response' => 403 ) );
}
$stats = $this->get_dashboard_stats();
// Pass chart data to JavaScript.
wp_localize_script(
'agentorie-chat-dashboard',
'agentorieDashboard',
array(
'chartData' => isset( $stats['chart_data'] ) ? $stats['chart_data'] : array(
'labels' => array(),
'values' => array(),
),
'i18n' => array(
'messages' => __( 'Messages', 'agentorie-chat' ),
),
)
);
include AGENTORIE_CHAT_PLUGIN_DIR . 'admin/views/dashboard.php';
}
/**
* Display LiveChat page.
*
* @return void
*/
public function display_livechat_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ), '', array( 'response' => 403 ) );
}
// Load conversations for LiveChat.
// Seat limit is enforced server-side (WebSocket/SaaS) at takeover time.
$agentorie_timeout_minutes = agentorie_chat_get_setting( 'livechat_timeout_minutes', 60 );
$agentorie_conversations = Agentorie_Database::get_active_conversations( $agentorie_timeout_minutes );
include AGENTORIE_CHAT_PLUGIN_DIR . 'admin/views/livechat.php';
}
/**
* Display conversations page.
*
* @return void
*/
public function display_conversations_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ), '', array( 'response' => 403 ) );
}
$per_page = 20;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only pagination parameter.
$agentorie_page = isset( $_GET['paged'] ) ? max( 1, absint( wp_unslash( $_GET['paged'] ) ) ) : 1;
$offset = ( $agentorie_page - 1 ) * $per_page;
// Filter parameters.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter parameter.
$filter_date = isset( $_GET['filter_date'] ) ? sanitize_key( wp_unslash( $_GET['filter_date'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter parameter.
$filter_flag = isset( $_GET['filter_flag'] ) ? sanitize_key( wp_unslash( $_GET['filter_flag'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter parameter.
$filter_messages = isset( $_GET['filter_messages'] ) ? sanitize_key( wp_unslash( $_GET['filter_messages'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter parameter.
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
// Sorting parameters.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only sorting parameter.
$orderby = isset( $_GET['orderby'] ) ? sanitize_key( wp_unslash( $_GET['orderby'] ) ) : 'created_at';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only sorting parameter.
$order = isset( $_GET['order'] ) ? strtoupper( sanitize_key( wp_unslash( $_GET['order'] ) ) ) : 'DESC';
// Validate orderby.
$allowed_orderby = array( 'created_at', 'msg_count', 'session_id', 'visitor_id' );
$orderby = in_array( $orderby, $allowed_orderby, true ) ? $orderby : 'created_at';
// Validate order.
$order = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC';
// Get filtered conversations.
$agentorie_conversations = Agentorie_Database::get_all_conversations(
$per_page,
$offset,
$filter_date,
$filter_flag,
$filter_messages,
$search,
$orderby,
$order
);
// Get total count with filters.
$agentorie_total_items = Agentorie_Database::count_all_conversations(
$filter_date,
$filter_flag,
$filter_messages,
$search
);
$agentorie_total_pages = ceil( $agentorie_total_items / $per_page );
include AGENTORIE_CHAT_PLUGIN_DIR . 'admin/views/conversations.php';
}
/**
* Display settings page.
*
* @return void
*/
public function display_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ), '', array( 'response' => 403 ) );
}
$license_key = agentorie_chat_get_setting( 'license_key', '' );
if ( ! empty( $license_key ) ) {
Agentorie_API::check_license( $license_key, true );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';
$tabs = array(
'general' => __( 'General', 'agentorie-chat' ),
'ai_settings' => __( 'AI Settings', 'agentorie-chat' ),
'appearance' => __( 'Appearance', 'agentorie-chat' ),
'content_indexing' => __( 'Content Indexing', 'agentorie-chat' ),
'livechat' => __( 'LiveChat', 'agentorie-chat' ),
'advanced' => __( 'Advanced', 'agentorie-chat' ),
);
// Auto-detect indexed content and re-enable toggles if needed (e.g. after reinstall).
if ( 'content_indexing' === $current_tab ) {
$this->maybe_reenable_indexing_toggles();
}
include AGENTORIE_CHAT_PLUGIN_DIR . 'admin/views/settings.php';
}
/**
* Re-enable content indexing toggles if indexed content exists in database.
*
* Handles the edge case where the plugin is reinstalled but the database tables
* still contain indexed content. Without this, the toggles show "off" while the
* AI continues using the indexed content.
*
* @since 1.1.7
* @return void
*/
private function maybe_reenable_indexing_toggles() {
if ( ! class_exists( 'Agentorie_Content_Indexer' ) ) {
return;
}
$indexer = Agentorie_Content_Indexer::get_instance();
// Page/post indexing.
$content_enabled = agentorie_chat_get_setting( 'content_indexing_enabled', false );
if ( ! $content_enabled ) {
$stats = $indexer->get_stats();
if ( ! empty( $stats['total_indexed'] ) && $stats['total_indexed'] > 0 ) {
agentorie_chat_update_setting( 'content_indexing_enabled', true );
if ( class_exists( 'Agentorie_API_SaaS' ) ) {
$api = Agentorie_API_SaaS::get_instance();
$api->sync_config( array( 'content_indexing_enabled' => true ) );
}
}
}
// WooCommerce product indexing.
$woo_enabled = agentorie_chat_get_setting( 'woo_indexing_enabled', false );
if ( ! $woo_enabled && class_exists( 'WooCommerce' ) ) {
$woo_stats = $indexer->get_product_stats();
if ( ! empty( $woo_stats['total_indexed'] ) && $woo_stats['total_indexed'] > 0 ) {
agentorie_chat_update_setting( 'woo_indexing_enabled', true );
}
}
}
/**
* Display admin notices.
*
* @return void
*/
public function display_admin_notices() {
$screen = get_current_screen();
if ( ! $screen || ! $this->is_plugin_page( $screen->id ) ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['welcome'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['welcome'] ) ) ) {
echo '
' . esc_html__( 'Welcome to Agentorie Chat!', 'agentorie-chat' ) . ' ' . esc_html__( 'Get started by configuring your settings below.', 'agentorie-chat' ) . '
';
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['updated'] ) && 'true' === sanitize_text_field( wp_unslash( $_GET['updated'] ) ) ) {
echo '' . esc_html__( 'Settings saved successfully.', 'agentorie-chat' ) . '
';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : '';
if ( 'ai_settings' === $current_tab ) {
echo '' . esc_html__( 'Note: Active conversations will continue using the previous settings until they are restarted. New conversations will use the updated settings immediately.', 'agentorie-chat' ) . '
';
}
}
if ( ! agentorie_chat_get_setting( 'enabled', false ) ) {
echo '';
}
}
/**
* Register plugin settings.
*
* @return void
*/
public function register_settings() {
register_setting(
'agentorie_chat_general',
'agentorie_chat_settings',
array(
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_settings' ),
)
);
}
/**
* Sanitize all plugin settings.
*
* Centralized sanitization callback for register_setting().
* This ensures data integrity even if update_option() is called directly.
*
* @since 1.0.0
* @param array $input Raw settings input.
* @return array Sanitized settings.
*/
public function sanitize_settings( $input ) {
if ( ! is_array( $input ) ) {
return array();
}
$sanitized = array();
// Get existing settings to preserve values not in input.
$existing = get_option( 'agentorie_chat_settings', array() );
// Define sanitization rules for each setting.
$fields = array(
// Booleans.
'enabled' => 'bool',
'admin_only' => 'bool',
'hide_on_mobile' => 'bool',
'delete_data_on_uninstall' => 'bool',
'debug_mode' => 'bool',
'voice_enabled' => 'bool',
'speech_to_text' => 'bool',
'text_to_speech' => 'bool',
'email_on_contact' => 'bool',
'email_on_human_request' => 'bool',
'widget_show_on_mobile' => 'bool',
'button_pulse' => 'bool',
'show_header_avatar' => 'bool',
'show_client_avatar' => 'bool',
'content_indexing_enabled' => 'bool',
'index_posts' => 'bool',
'index_custom_post_types' => 'bool',
'woo_indexing_enabled' => 'bool',
'demo_lang_active' => 'bool',
'demo_voice_active' => 'bool',
'conversation_tone' => 'text',
'custom_tone' => 'text',
// Strings (text fields).
'license_key' => 'text',
'license_status' => 'key',
'bot_name' => 'text',
'bot_subtitle' => 'text',
'bot_avatar' => 'emoji',
'welcome_title' => 'text',
'welcome_message' => 'textarea',
'welcome_font_weight' => 'select',
'admin_name' => 'text',
'admin_avatar' => 'emoji',
'client_avatar' => 'emoji',
'button_icon' => 'emoji',
'availability_text' => 'text',
'company_name' => 'text',
'company_phone' => 'text',
'company_address' => 'text',
'company_hours' => 'text',
'company_website' => 'url',
'company_description' => 'textarea',
'exclude_pages' => 'text',
'input_placeholder' => 'text',
'voice_placeholder' => 'text',
'lang_selector_title' => 'text',
'exclude_ip' => 'ip',
'default_language' => 'text',
'monthly_reset_date' => 'text',
'content_exclude_urls' => 'textarea',
'content_manual_urls' => 'textarea',
'last_content_sync' => 'text',
'lead_collection_instructions' => 'textarea',
// Emails.
'notification_email' => 'email',
'company_email' => 'email',
'lead_collection_email' => 'email',
// Integers.
'livechat_timeout_minutes' => 'int',
'livechat_auto_release_hours' => 'int',
'widget_delay_seconds' => 'int',
'conversation_retention_hours' => 'int',
'monthly_limit' => 'int',
'monthly_usage' => 'int',
'content_indexing_limit' => 'int',
'woo_indexing_limit' => 'int',
'woo_manual_products' => 'textarea',
'woo_indexing_language' => 'key',
'demo_lang_remaining' => 'int',
'demo_voice_remaining' => 'int',
'rate_limit' => 'int',
'test_conversations_count' => 'int',
// Colors.
'widget_primary_color' => 'color',
'widget_text_color' => 'color',
'primary_color' => 'color',
'primary_text_color' => 'color',
'user_bubble_color' => 'color',
'user_text_color' => 'color',
'bot_bubble_color' => 'color',
'bot_text_color' => 'color',
'background_color' => 'color',
// Select fields with allowed values.
'widget_position' => array( 'bottom-right', 'bottom-left' ),
'button_size' => array( 'small', 'medium', 'large' ),
'button_style' => array( 'solid', 'gradient', 'outline' ),
'button_effect' => array( 'none', 'glossy', 'radiant', 'glassmorphism' ),
'button_animation' => array( 'none', 'pulse', 'bounce', 'shake', 'rotate' ),
'button_margin_bottom' => array( '0', '10', '20', '30', '40', '50', '60', '70', '80', '90', '100' ),
'header_effect' => array( 'none', 'glossy', 'glassmorphism' ),
'show_lang_selector' => 'bool',
'chat_width' => array( '350', '380', '420', '450' ),
'chat_height' => array( '500', '550', '600', '650', '70vh' ),
'border_radius' => array( '8', '12', '16', '20', '24' ),
'shadow_style' => array( 'none', 'light', 'medium', 'strong' ),
'header_style' => array( 'solid', 'gradient' ),
'content_indexing_mode' => array( 'auto', 'manual' ),
'woo_indexing_mode' => array( 'auto', 'manual' ),
// Arrays.
'license_features' => 'array',
'widget_excluded_pages' => 'array',
'authorized_languages' => 'array',
'content_index_post_types' => 'array',
);
foreach ( $fields as $key => $type ) {
// Use input value if exists, otherwise preserve existing.
if ( array_key_exists( $key, $input ) ) {
$value = $input[ $key ];
} elseif ( array_key_exists( $key, $existing ) ) {
$sanitized[ $key ] = $existing[ $key ];
continue;
} else {
continue;
}
// Apply sanitization based on type.
if ( is_array( $type ) ) {
// Select field with allowed values.
$sanitized[ $key ] = in_array( $value, $type, true ) ? $value : $type[0];
} else {
$sanitized[ $key ] = $this->sanitize_field( $value, $type );
}
}
// Preserve any unknown fields from existing settings (forward compatibility).
foreach ( $existing as $key => $value ) {
if ( ! array_key_exists( $key, $sanitized ) ) {
$sanitized[ $key ] = $value;
}
}
return $sanitized;
}
/**
* Sanitize a single field value.
*
* @since 1.0.0
* @param mixed $value Raw value.
* @param string $type Field type.
* @return mixed Sanitized value.
*/
private function sanitize_field( $value, $type ) {
switch ( $type ) {
case 'bool':
return (bool) $value;
case 'int':
return absint( $value );
case 'text':
return sanitize_text_field( $value );
case 'textarea':
return sanitize_textarea_field( $value );
case 'key':
return sanitize_key( $value );
case 'email':
return sanitize_email( $value );
case 'url':
return esc_url_raw( $value );
case 'color':
return sanitize_hex_color( $value ) ? sanitize_hex_color( $value ) : '';
case 'ip':
return filter_var( $value, FILTER_VALIDATE_IP ) ? sanitize_text_field( $value ) : '';
case 'emoji':
// Allow emoji/short text (max 10 chars).
return sanitize_text_field( substr( $value, 0, 10 ) );
case 'array':
return is_array( $value ) ? array_map( 'sanitize_text_field', $value ) : array();
default:
return sanitize_text_field( $value );
}
}
/**
* Handle save settings form submission.
*
* @return void
*/
public function handle_save_settings() {
if ( ! isset( $_POST['agentorie_chat_settings_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['agentorie_chat_settings_nonce'] ) ), 'agentorie_chat_save_settings' ) ) {
wp_die( esc_html__( 'Security check failed.', 'agentorie-chat' ), '', array( 'back_link' => true ) );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ) );
}
$section = isset( $_POST['section'] ) ? sanitize_key( wp_unslash( $_POST['section'] ) ) : 'general';
$extra_args = array();
switch ( $section ) {
case 'general':
$this->save_general_settings();
break;
case 'appearance':
$extra_args = $this->save_appearance_settings();
break;
case 'livechat':
$this->save_livechat_settings();
break;
case 'content_indexing':
$this->save_content_indexing_settings();
$section = 'content_indexing'; // Keep tab after redirect.
break;
case 'ai_settings':
$extra_args = $this->save_ai_settings();
break;
case 'advanced':
$this->save_advanced_settings();
break;
}
// Clear third-party caches when settings are saved.
if ( function_exists( 'agentorie_chat_clear_third_party_caches' ) ) {
agentorie_chat_clear_third_party_caches();
}
$redirect_args = array(
'page' => 'agentorie-chat-settings',
'tab' => $section,
'updated' => 'true',
);
// Merge extra args from save methods (e.g. sync_failed from ai_settings).
if ( ! empty( $extra_args ) && is_array( $extra_args ) ) {
$redirect_args = array_merge( $redirect_args, $extra_args );
}
wp_safe_redirect( add_query_arg( $redirect_args, admin_url( 'admin.php' ) ) );
exit;
}
/**
* Save general settings.
*
* @return void
*/
private function save_general_settings() {
// Nonce verified in handle_save_settings().
// phpcs:disable WordPress.Security.NonceVerification.Missing
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only checking existence for checkbox.
agentorie_chat_update_setting( 'enabled', isset( $_POST['enabled'] ) );
agentorie_chat_update_setting( 'admin_only', isset( $_POST['admin_only'] ) );
$email = isset( $_POST['notification_email'] ) ? sanitize_email( wp_unslash( $_POST['notification_email'] ) ) : '';
agentorie_chat_update_setting( 'notification_email', $email );
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
/**
* Get supported languages for AI Settings.
*
* Returns an associative array of BCP-47 language codes to native labels.
* This list matches Google Cloud TTS Standard voices (FEMALE) availability.
*
* @since 1.1.6
* @return array Language code => Native label.
*/
public static function get_supported_languages() {
return array(
'af-ZA' => 'Afrikaans',
'ar-XA' => 'العربية (Arabic)',
'bg-BG' => 'Български (Bulgarian)',
'bn-IN' => 'বাংলা (Bengali)',
'ca-ES' => 'Català (Catalan)',
'cmn-CN' => '中文普通话 (Chinese Mandarin - CN)',
'cmn-TW' => '中文國語 (Chinese Mandarin - TW)',
'cs-CZ' => 'Čeština (Czech)',
'da-DK' => 'Dansk (Danish)',
'de-DE' => 'Deutsch (German)',
'el-GR' => 'Ελληνικά (Greek)',
'en-AU' => 'English (Australia)',
'en-GB' => 'English (UK)',
'en-IN' => 'English (India)',
'en-US' => 'English (US)',
'es-ES' => 'Español (Spain)',
'es-US' => 'Español (US)',
'et-EE' => 'Eesti (Estonian)',
'eu-ES' => 'Euskara (Basque)',
'fi-FI' => 'Suomi (Finnish)',
'fil-PH' => 'Filipino',
'fr-CA' => 'Français (Canada)',
'fr-FR' => 'Français (France)',
'gl-ES' => 'Galego (Galician)',
'gu-IN' => 'ગુજરાતી (Gujarati)',
'he-IL' => 'עברית (Hebrew)',
'hi-IN' => 'हिन्दी (Hindi)',
'hr-HR' => 'Hrvatski (Croatian)',
'hu-HU' => 'Magyar (Hungarian)',
'id-ID' => 'Bahasa Indonesia (Indonesian)',
'is-IS' => 'Íslenska (Icelandic)',
'it-IT' => 'Italiano (Italian)',
'ja-JP' => '日本語 (Japanese)',
'kn-IN' => 'ಕನ್ನಡ (Kannada)',
'ko-KR' => '한국어 (Korean)',
'lt-LT' => 'Lietuvių (Lithuanian)',
'lv-LV' => 'Latviešu (Latvian)',
'ml-IN' => 'മലയാളം (Malayalam)',
'mr-IN' => 'मराठी (Marathi)',
'ms-MY' => 'Bahasa Melayu (Malay)',
'nb-NO' => 'Norsk Bokmål (Norwegian)',
'nl-BE' => 'Nederlands (Belgium)',
'nl-NL' => 'Nederlands (Netherlands)',
'pa-IN' => 'ਪੰਜਾਬੀ (Punjabi)',
'pl-PL' => 'Polski (Polish)',
'pt-BR' => 'Português (Brazil)',
'pt-PT' => 'Português (Portugal)',
'ro-RO' => 'Română (Romanian)',
'ru-RU' => 'Русский (Russian)',
'sk-SK' => 'Slovenčina (Slovak)',
'sl-SI' => 'Slovenščina (Slovenian)',
'sr-RS' => 'Српски (Serbian)',
'sv-SE' => 'Svenska (Swedish)',
'ta-IN' => 'தமிழ் (Tamil)',
'te-IN' => 'తెలుగు (Telugu)',
'th-TH' => 'ไทย (Thai)',
'tr-TR' => 'Türkçe (Turkish)',
'uk-UA' => 'Українська (Ukrainian)',
'vi-VN' => 'Tiếng Việt (Vietnamese)',
'yue-HK' => '粵語 (Cantonese)',
);
}
/**
* Save AI Settings.
*
* Handles company information, default language, and conversation tone.
* Syncs configuration to SaaS after saving locally.
*
* @since 1.1.6
* @return array Redirect query args.
*/
private function save_ai_settings() {
// Nonce verified in handle_save_settings().
// phpcs:disable WordPress.Security.NonceVerification.Missing
// --- Company Information ---
$company_name = isset( $_POST['company_name'] ) ? sanitize_text_field( wp_unslash( $_POST['company_name'] ) ) : '';
agentorie_chat_update_setting( 'company_name', $company_name );
$company_description = isset( $_POST['company_description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['company_description'] ) ) : '';
agentorie_chat_update_setting( 'company_description', $company_description );
$company_phone = isset( $_POST['company_phone'] ) ? sanitize_text_field( wp_unslash( $_POST['company_phone'] ) ) : '';
agentorie_chat_update_setting( 'company_phone', $company_phone );
$company_email = isset( $_POST['company_email'] ) ? sanitize_email( wp_unslash( $_POST['company_email'] ) ) : '';
agentorie_chat_update_setting( 'company_email', $company_email );
$company_address = isset( $_POST['company_address'] ) ? sanitize_text_field( wp_unslash( $_POST['company_address'] ) ) : '';
agentorie_chat_update_setting( 'company_address', $company_address );
$company_hours = isset( $_POST['company_hours'] ) ? sanitize_text_field( wp_unslash( $_POST['company_hours'] ) ) : '';
agentorie_chat_update_setting( 'company_hours', $company_hours );
$company_website = isset( $_POST['company_website'] ) ? esc_url_raw( wp_unslash( $_POST['company_website'] ) ) : '';
agentorie_chat_update_setting( 'company_website', $company_website );
// --- Default Language ---
$allowed_languages = array_keys( self::get_supported_languages() );
$default_language = isset( $_POST['default_language'] ) ? sanitize_text_field( wp_unslash( $_POST['default_language'] ) ) : 'en-US';
if ( ! in_array( $default_language, $allowed_languages, true ) ) {
$default_language = 'en-US';
}
agentorie_chat_update_setting( 'default_language', $default_language );
// --- Conversation Tone ---
$allowed_tones = array( 'formal', 'friendly', 'casual', 'enthusiastic', 'custom' );
$tone = isset( $_POST['conversation_tone'] ) ? sanitize_key( wp_unslash( $_POST['conversation_tone'] ) ) : 'friendly';
if ( ! in_array( $tone, $allowed_tones, true ) ) {
$tone = 'friendly';
}
agentorie_chat_update_setting( 'conversation_tone', $tone );
$custom_tone = isset( $_POST['custom_tone'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_tone'] ) ) : '';
agentorie_chat_update_setting( 'custom_tone', mb_substr( $custom_tone, 0, 75 ) );
// phpcs:enable WordPress.Security.NonceVerification.Missing
// --- Sync to SaaS ---
$sync_failed = false;
try {
$api = Agentorie_API::get_instance();
$api->sync_config(
array(
'company_info' => array(
'company_name' => $company_name,
'company_description' => $company_description,
'company_phone' => $company_phone,
'company_email' => $company_email,
'company_address' => $company_address,
'company_hours' => $company_hours,
'company_website' => $company_website,
),
'default_language' => $default_language,
'conversation_tone' => $tone,
'custom_tone' => $custom_tone,
)
);
} catch ( \Exception $e ) {
$sync_failed = true;
if ( class_exists( 'Agentorie_Logger' ) ) {
Agentorie_Logger::error( 'AI Settings sync failed', array( 'error' => $e->getMessage() ) );
}
}
// Return extra redirect args if sync failed.
if ( $sync_failed ) {
return array( 'sync_failed' => 'true' );
}
return array();
}
/**
* Save appearance settings.
*
* Saves all appearance fields locally, then syncs Bot Identity
* fields to the SaaS backend via sync_config().
*
* @since 1.0.0
* @return array Redirect query args (sync_failed if applicable).
*/
private function save_appearance_settings() {
// Nonce verified in handle_save_settings().
// phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$identity = $this->save_identity_settings();
$color_fields = array( 'primary_color', 'primary_text_color', 'user_bubble_color', 'user_text_color', 'bot_bubble_color', 'bot_text_color', 'background_color' );
foreach ( $color_fields as $field ) {
if ( isset( $_POST[ $field ] ) ) {
$color = sanitize_hex_color( wp_unslash( $_POST[ $field ] ) );
if ( $color ) {
agentorie_chat_update_setting( $field, $color );
}
}
}
$button_size = isset( $_POST['button_size'] ) ? sanitize_key( wp_unslash( $_POST['button_size'] ) ) : 'medium';
if ( in_array( $button_size, array( 'small', 'medium', 'large' ), true ) ) {
agentorie_chat_update_setting( 'button_size', $button_size );
}
$button_icon = '💬';
if ( ! empty( $_POST['button_icon_custom'] ) ) {
$button_icon = sanitize_text_field( substr( wp_unslash( $_POST['button_icon_custom'] ), 0, 10 ) );
} elseif ( ! empty( $_POST['button_icon'] ) ) {
$button_icon = sanitize_text_field( substr( wp_unslash( $_POST['button_icon'] ), 0, 10 ) );
}
agentorie_chat_update_setting( 'button_icon', $button_icon );
$button_style = isset( $_POST['button_style'] ) ? sanitize_key( wp_unslash( $_POST['button_style'] ) ) : 'solid';
if ( in_array( $button_style, array( 'solid', 'gradient', 'outline' ), true ) ) {
agentorie_chat_update_setting( 'button_style', $button_style );
}
agentorie_chat_update_setting( 'button_pulse', isset( $_POST['button_pulse'] ) );
$button_effect = isset( $_POST['button_effect'] ) ? sanitize_key( wp_unslash( $_POST['button_effect'] ) ) : 'none';
if ( in_array( $button_effect, array( 'none', 'glossy', 'radiant', 'glassmorphism' ), true ) ) {
agentorie_chat_update_setting( 'button_effect', $button_effect );
}
$button_animation = isset( $_POST['button_animation'] ) ? sanitize_key( wp_unslash( $_POST['button_animation'] ) ) : 'pulse';
if ( in_array( $button_animation, array( 'none', 'pulse', 'bounce', 'shake', 'rotate' ), true ) ) {
agentorie_chat_update_setting( 'button_animation', $button_animation );
}
$button_margin_bottom = isset( $_POST['button_margin_bottom'] ) ? sanitize_text_field( wp_unslash( $_POST['button_margin_bottom'] ) ) : '20';
if ( in_array( $button_margin_bottom, array( '0', '10', '20', '30', '40', '50', '60', '70', '80', '90', '100' ), true ) ) {
agentorie_chat_update_setting( 'button_margin_bottom', $button_margin_bottom );
}
$chat_width = isset( $_POST['chat_width'] ) ? sanitize_text_field( wp_unslash( $_POST['chat_width'] ) ) : '380';
if ( in_array( $chat_width, array( '350', '380', '420', '450' ), true ) ) {
agentorie_chat_update_setting( 'chat_width', $chat_width );
}
$chat_height = isset( $_POST['chat_height'] ) ? sanitize_text_field( wp_unslash( $_POST['chat_height'] ) ) : '550';
if ( in_array( $chat_height, array( '500', '550', '600', '650', '70vh' ), true ) ) {
agentorie_chat_update_setting( 'chat_height', $chat_height );
}
$border_radius = isset( $_POST['border_radius'] ) ? sanitize_text_field( wp_unslash( $_POST['border_radius'] ) ) : '16';
if ( in_array( $border_radius, array( '8', '12', '16', '20', '24' ), true ) ) {
agentorie_chat_update_setting( 'border_radius', $border_radius );
}
$shadow_style = isset( $_POST['shadow_style'] ) ? sanitize_key( wp_unslash( $_POST['shadow_style'] ) ) : 'medium';
if ( in_array( $shadow_style, array( 'none', 'light', 'medium', 'strong' ), true ) ) {
agentorie_chat_update_setting( 'shadow_style', $shadow_style );
}
$header_style = isset( $_POST['header_style'] ) ? sanitize_key( wp_unslash( $_POST['header_style'] ) ) : 'solid';
if ( in_array( $header_style, array( 'solid', 'gradient' ), true ) ) {
agentorie_chat_update_setting( 'header_style', $header_style );
}
$header_effect = isset( $_POST['header_effect'] ) ? sanitize_key( wp_unslash( $_POST['header_effect'] ) ) : 'none';
if ( in_array( $header_effect, array( 'none', 'glossy', 'glassmorphism' ), true ) ) {
agentorie_chat_update_setting( 'header_effect', $header_effect );
}
agentorie_chat_update_setting( 'show_header_avatar', isset( $_POST['show_header_avatar'] ) );
agentorie_chat_update_setting( 'show_client_avatar', isset( $_POST['show_client_avatar'] ) );
agentorie_chat_update_setting( 'show_lang_selector', isset( $_POST['show_lang_selector'] ) );
$client_avatar = '👤';
if ( ! empty( $_POST['client_avatar_custom'] ) ) {
$client_avatar = sanitize_text_field( substr( wp_unslash( $_POST['client_avatar_custom'] ), 0, 10 ) );
} elseif ( ! empty( $_POST['client_avatar'] ) ) {
$client_avatar = sanitize_text_field( substr( wp_unslash( $_POST['client_avatar'] ), 0, 10 ) );
}
agentorie_chat_update_setting( 'client_avatar', $client_avatar );
// Voice features.
agentorie_chat_update_setting( 'speech_to_text', isset( $_POST['speech_to_text'] ) );
agentorie_chat_update_setting( 'text_to_speech', isset( $_POST['text_to_speech'] ) );
// phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// --- Sync Bot Identity to SaaS ---
$sync_failed = false;
try {
$api = Agentorie_API::get_instance();
$api->sync_config(
array(
'bot_identity' => $identity,
)
);
} catch ( \Exception $e ) {
$sync_failed = true;
if ( class_exists( 'Agentorie_Logger' ) ) {
Agentorie_Logger::error( 'Bot Identity sync failed', array( 'error' => $e->getMessage() ) );
}
}
if ( $sync_failed ) {
return array( 'sync_failed' => 'true' );
}
return array();
}
/**
* Save identity settings and return saved values.
*
* Returns the identity data array so the caller can sync it to the SaaS.
*
* @since 1.0.0
* @return array Identity data that was saved.
*/
private function save_identity_settings() {
// Nonce verified in handle_save_settings().
// phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$bot_name = isset( $_POST['bot_name'] ) ? sanitize_text_field( wp_unslash( $_POST['bot_name'] ) ) : 'Stella';
agentorie_chat_update_setting( 'bot_name', $bot_name );
$bot_subtitle = isset( $_POST['bot_subtitle'] ) ? sanitize_text_field( wp_unslash( $_POST['bot_subtitle'] ) ) : '';
agentorie_chat_update_setting( 'bot_subtitle', $bot_subtitle );
$bot_avatar = '🤖';
if ( ! empty( $_POST['bot_avatar_custom'] ) ) {
$bot_avatar = sanitize_text_field( substr( wp_unslash( $_POST['bot_avatar_custom'] ), 0, 10 ) );
} elseif ( ! empty( $_POST['bot_avatar'] ) ) {
$bot_avatar = sanitize_text_field( substr( wp_unslash( $_POST['bot_avatar'] ), 0, 10 ) );
}
agentorie_chat_update_setting( 'bot_avatar', $bot_avatar );
$availability = isset( $_POST['availability_text'] ) ? sanitize_text_field( wp_unslash( $_POST['availability_text'] ) ) : '';
agentorie_chat_update_setting( 'availability_text', $availability );
$welcome_title = isset( $_POST['welcome_title'] ) ? sanitize_text_field( wp_unslash( $_POST['welcome_title'] ) ) : '';
agentorie_chat_update_setting( 'welcome_title', $welcome_title );
$welcome_message = isset( $_POST['welcome_message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['welcome_message'] ) ) : '';
agentorie_chat_update_setting( 'welcome_message', $welcome_message );
$welcome_font_weight = isset( $_POST['welcome_font_weight'] ) ? sanitize_text_field( wp_unslash( $_POST['welcome_font_weight'] ) ) : '600';
if ( in_array( $welcome_font_weight, array( '300', '400', '500', '600', '700', '800', '900' ), true ) ) {
agentorie_chat_update_setting( 'welcome_font_weight', $welcome_font_weight );
}
// Input placeholders.
$input_placeholder = isset( $_POST['input_placeholder'] ) ? sanitize_text_field( wp_unslash( $_POST['input_placeholder'] ) ) : '';
agentorie_chat_update_setting( 'input_placeholder', mb_substr( $input_placeholder, 0, 50 ) );
$voice_placeholder = isset( $_POST['voice_placeholder'] ) ? sanitize_text_field( wp_unslash( $_POST['voice_placeholder'] ) ) : '';
agentorie_chat_update_setting( 'voice_placeholder', mb_substr( $voice_placeholder, 0, 50 ) );
$lang_selector_title = isset( $_POST['lang_selector_title'] ) ? sanitize_text_field( wp_unslash( $_POST['lang_selector_title'] ) ) : '';
agentorie_chat_update_setting( 'lang_selector_title', mb_substr( $lang_selector_title, 0, 50 ) );
// phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
return array(
'bot_name' => $bot_name,
'bot_subtitle' => $bot_subtitle,
'bot_avatar' => $bot_avatar,
'welcome_title' => $welcome_title,
'welcome_message' => $welcome_message,
);
}
/**
* Save LiveChat settings.
*
* @return void
*/
private function save_livechat_settings() {
// Nonce verified in handle_save_settings().
// phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$current_user_id = get_current_user_id();
// Personal settings (per admin) - stored in user_meta.
$my_name = isset( $_POST['livechat_my_name'] ) ? sanitize_text_field( wp_unslash( $_POST['livechat_my_name'] ) ) : '';
if ( ! empty( $my_name ) ) {
update_user_meta( $current_user_id, 'agentorie_chat_livechat_name', $my_name );
}
$my_avatar = isset( $_POST['livechat_my_avatar'] ) ? sanitize_text_field( substr( wp_unslash( $_POST['livechat_my_avatar'] ), 0, 10 ) ) : '👤';
update_user_meta( $current_user_id, 'agentorie_chat_livechat_avatar', $my_avatar );
// Global settings - stored in plugin options.
$timeout = isset( $_POST['livechat_timeout_minutes'] ) ? absint( wp_unslash( $_POST['livechat_timeout_minutes'] ) ) : 60;
agentorie_chat_update_setting( 'livechat_timeout_minutes', max( 5, min( 1440, $timeout ) ) );
// phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}
/**
* Save advanced settings.
*
* @return void
*/
private function save_advanced_settings() {
// Nonce verified in handle_save_settings().
// phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$position = isset( $_POST['widget_position'] ) ? sanitize_key( wp_unslash( $_POST['widget_position'] ) ) : 'bottom-right';
if ( ! in_array( $position, array( 'bottom-right', 'bottom-left' ), true ) ) {
$position = 'bottom-right';
}
agentorie_chat_update_setting( 'widget_position', $position );
agentorie_chat_update_setting( 'hide_on_mobile', isset( $_POST['hide_on_mobile'] ) );
$exclude_pages = isset( $_POST['exclude_pages'] ) ? sanitize_text_field( wp_unslash( $_POST['exclude_pages'] ) ) : '';
agentorie_chat_update_setting( 'exclude_pages', $exclude_pages );
$exclude_ip = isset( $_POST['exclude_ip'] ) ? sanitize_text_field( wp_unslash( $_POST['exclude_ip'] ) ) : '';
if ( ! empty( $exclude_ip ) && ! filter_var( $exclude_ip, FILTER_VALIDATE_IP ) ) {
$exclude_ip = '';
}
agentorie_chat_update_setting( 'exclude_ip', $exclude_ip );
$retention = isset( $_POST['conversation_retention_hours'] ) ? absint( wp_unslash( $_POST['conversation_retention_hours'] ) ) : 6;
$retention = max( 1, min( 24, $retention ) );
agentorie_chat_update_setting( 'conversation_retention_hours', $retention );
agentorie_chat_update_setting( 'delete_data_on_uninstall', isset( $_POST['delete_data_on_uninstall'] ) );
$debug_mode_changed = agentorie_chat_get_setting( 'debug_mode', false ) !== isset( $_POST['debug_mode'] );
agentorie_chat_update_setting( 'debug_mode', isset( $_POST['debug_mode'] ) );
if ( $debug_mode_changed && class_exists( 'Agentorie_Logger' ) ) {
Agentorie_Logger::clear_cache();
}
// phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}
/**
* Save Content Indexing settings.
*
* @return void
*/
private function save_content_indexing_settings() {
// Nonce verified in handle_save_settings().
// phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// Main toggle.
$content_indexing_enabled = isset( $_POST['content_indexing_enabled'] );
$old_value = agentorie_chat_get_setting( 'content_indexing_enabled', false );
agentorie_chat_update_setting( 'content_indexing_enabled', $content_indexing_enabled );
// Indexing mode (auto or manual).
$indexing_mode = isset( $_POST['content_indexing_mode'] ) ? sanitize_key( wp_unslash( $_POST['content_indexing_mode'] ) ) : 'auto';
if ( in_array( $indexing_mode, array( 'auto', 'manual' ), true ) ) {
agentorie_chat_update_setting( 'content_indexing_mode', $indexing_mode );
}
// Include posts (auto mode).
agentorie_chat_update_setting( 'index_posts', isset( $_POST['index_posts'] ) );
// Custom post types toggle (auto mode).
agentorie_chat_update_setting( 'index_custom_post_types', isset( $_POST['index_custom_post_types'] ) );
// Custom post types selection (auto mode).
$selected_cpts = array();
if ( isset( $_POST['index_custom_post_types'] ) && isset( $_POST['content_index_post_types'] ) && is_array( $_POST['content_index_post_types'] ) ) {
$selected_cpts = array_map( 'sanitize_key', wp_unslash( $_POST['content_index_post_types'] ) );
// Validate against registered public CPTs.
$registered_cpts = get_post_types(
array(
'public' => true,
'_builtin' => false,
),
'names'
);
$selected_cpts = array_values( array_intersect( $selected_cpts, $registered_cpts ) );
}
agentorie_chat_update_setting( 'content_index_post_types', $selected_cpts );
// Exclude URLs (auto mode) - sanitize each URL.
$exclude_urls = isset( $_POST['content_exclude_urls'] ) ? sanitize_textarea_field( wp_unslash( $_POST['content_exclude_urls'] ) ) : '';
$exclude_urls = $this->sanitize_url_list( $exclude_urls );
agentorie_chat_update_setting( 'content_exclude_urls', $exclude_urls );
// Manual URLs (manual mode) - sanitize and validate.
$manual_urls = isset( $_POST['content_manual_urls'] ) ? sanitize_textarea_field( wp_unslash( $_POST['content_manual_urls'] ) ) : '';
$manual_urls = $this->sanitize_url_list( $manual_urls, true ); // true = validate against site URL.
agentorie_chat_update_setting( 'content_manual_urls', $manual_urls );
// phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// Sync to SaaS if changed.
if ( $content_indexing_enabled !== $old_value ) {
$api = Agentorie_API::get_instance();
$api->sync_config( array( 'content_indexing_enabled' => $content_indexing_enabled ) );
}
// If disabled, always clear the index (handles reinstall cases).
if ( ! $content_indexing_enabled && class_exists( 'Agentorie_Content_Indexer' ) ) {
Agentorie_Content_Indexer::get_instance()->clear_index();
}
// WooCommerce product indexing toggle (same form).
// Always save regardless of WooCommerce status — the checkbox is in the form.
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in handle_save_settings().
$woo_enabled = isset( $_POST['woo_indexing_enabled'] );
agentorie_chat_update_setting( 'woo_indexing_enabled', $woo_enabled );
// phpcs:enable WordPress.Security.NonceVerification.Missing
$this->save_woo_selection_settings();
// If disabled, remove product entries from index.
if ( ! $woo_enabled ) {
global $wpdb;
$table = Agentorie_Database::site_content_table();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
$wpdb->query( "DELETE FROM {$table} WHERE post_type = 'product'" );
}
}
/**
* Save the product selection settings (mode, manual list, language).
*
* The fields are only in the form when WooCommerce is active, so nothing
* is changed when they are absent.
*
* @since 1.2.0
* @return void
*/
private function save_woo_selection_settings() {
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified by both callers.
if ( ! isset( $_POST['woo_indexing_mode'] ) ) {
return;
}
$mode = sanitize_key( wp_unslash( $_POST['woo_indexing_mode'] ) );
agentorie_chat_update_setting( 'woo_indexing_mode', 'manual' === $mode ? 'manual' : 'auto' );
if ( isset( $_POST['woo_manual_products'] ) ) {
$lines = preg_split( '/\r\n|\r|\n/', sanitize_textarea_field( wp_unslash( $_POST['woo_manual_products'] ) ) );
$lines = array_values( array_filter( array_map( 'trim', $lines ), 'strlen' ) );
agentorie_chat_update_setting( 'woo_manual_products', implode( "\n", $lines ) );
}
if ( isset( $_POST['woo_indexing_language'] ) ) {
$language = sanitize_key( wp_unslash( $_POST['woo_indexing_language'] ) );
$languages = Agentorie_Content_Indexer::get_site_languages();
agentorie_chat_update_setting( 'woo_indexing_language', array_key_exists( $language, $languages ) ? $language : '' );
}
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
/**
* Sanitize a list of URLs (one per line).
*
* @param string $urls_text URLs separated by newlines.
* @param bool $validate_site_url Only allow URLs from this site.
* @return string Sanitized URLs.
*/
private function sanitize_url_list( $urls_text, $validate_site_url = false ) {
$lines = explode( "\n", $urls_text );
$site_url = trailingslashit( home_url() );
$valid_urls = array();
foreach ( $lines as $line ) {
$url = trim( $line );
if ( empty( $url ) ) {
continue;
}
// Basic URL sanitization.
$url = esc_url_raw( $url );
if ( empty( $url ) ) {
continue;
}
// If validation is required, check it's from this site.
if ( $validate_site_url && 0 !== strpos( $url, $site_url ) ) {
continue; // Skip external URLs.
}
$valid_urls[] = $url;
}
return implode( "\n", $valid_urls );
}
/**
* AJAX handler for content re-indexing.
*
* @return void
*/
public function handle_reindex_content() {
check_ajax_referer( 'agentorie_chat_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'agentorie-chat' ) ) );
}
// Check if Content Indexer class exists.
if ( ! class_exists( 'Agentorie_Content_Indexer' ) ) {
wp_send_json_error( array( 'message' => __( 'Content Indexer not available.', 'agentorie-chat' ) ) );
}
try {
$indexer = Agentorie_Content_Indexer::get_instance();
$count = $indexer->index_all_content();
$stats = $indexer->get_stats();
// Get product stats separately for the response message.
$product_stats = $indexer->get_product_stats();
$page_count = $count - $product_stats['total_indexed'];
// Build a clear message showing both pages and products.
$parts = array();
if ( $page_count > 0 ) {
$parts[] = sprintf(
/* translators: %d: number of pages indexed */
__( '%d pages', 'agentorie-chat' ),
$page_count
);
}
if ( $product_stats['total_indexed'] > 0 ) {
$parts[] = sprintf(
/* translators: %d: number of products indexed */
__( '%d products', 'agentorie-chat' ),
$product_stats['total_indexed']
);
}
if ( ! empty( $parts ) ) {
$message = sprintf(
/* translators: %s: comma-separated list of indexed items */
__( '%s indexed successfully.', 'agentorie-chat' ),
implode( ' + ', $parts )
);
} else {
$message = __( 'No content to index.', 'agentorie-chat' );
}
// Update last sync timestamp.
agentorie_chat_update_setting( 'last_content_sync', current_time( 'mysql' ) );
wp_send_json_success(
array(
'message' => $message,
'stats' => $stats,
'product_stats' => $product_stats,
)
);
} catch ( Exception $e ) {
Agentorie_Logger::error( 'Content indexing failed', array( 'error' => $e->getMessage() ) );
wp_send_json_error( array( 'message' => __( 'An error occurred during indexing.', 'agentorie-chat' ) ) );
}
}
/**
* AJAX handler for saving content indexing settings.
*
* @return void
*/
public function handle_save_content_indexing_ajax() {
check_ajax_referer( 'agentorie_chat_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied.', 'agentorie-chat' ) ) );
}
// Save the settings (reuse existing method logic).
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified above via check_ajax_referer.
$content_indexing_enabled = isset( $_POST['content_indexing_enabled'] );
$old_value = agentorie_chat_get_setting( 'content_indexing_enabled', false );
agentorie_chat_update_setting( 'content_indexing_enabled', $content_indexing_enabled );
// Indexing mode (auto or manual).
$indexing_mode = isset( $_POST['content_indexing_mode'] ) ? sanitize_key( wp_unslash( $_POST['content_indexing_mode'] ) ) : 'auto';
if ( in_array( $indexing_mode, array( 'auto', 'manual' ), true ) ) {
agentorie_chat_update_setting( 'content_indexing_mode', $indexing_mode );
}
// Include posts (auto mode).
agentorie_chat_update_setting( 'index_posts', isset( $_POST['index_posts'] ) );
// Custom post types toggle (auto mode).
agentorie_chat_update_setting( 'index_custom_post_types', isset( $_POST['index_custom_post_types'] ) );
// Custom post types selection (auto mode).
$selected_cpts = array();
if ( isset( $_POST['index_custom_post_types'] ) && isset( $_POST['content_index_post_types'] ) && is_array( $_POST['content_index_post_types'] ) ) {
$selected_cpts = array_map( 'sanitize_key', wp_unslash( $_POST['content_index_post_types'] ) );
// Validate against registered public CPTs.
$registered_cpts = get_post_types(
array(
'public' => true,
'_builtin' => false,
),
'names'
);
$selected_cpts = array_values( array_intersect( $selected_cpts, $registered_cpts ) );
}
agentorie_chat_update_setting( 'content_index_post_types', $selected_cpts );
// Exclude URLs (auto mode).
$exclude_urls = isset( $_POST['content_exclude_urls'] ) ? sanitize_textarea_field( wp_unslash( $_POST['content_exclude_urls'] ) ) : '';
$exclude_urls = $this->sanitize_url_list( $exclude_urls );
agentorie_chat_update_setting( 'content_exclude_urls', $exclude_urls );
// Manual URLs (manual mode).
$manual_urls = isset( $_POST['content_manual_urls'] ) ? sanitize_textarea_field( wp_unslash( $_POST['content_manual_urls'] ) ) : '';
$manual_urls = $this->sanitize_url_list( $manual_urls, true );
agentorie_chat_update_setting( 'content_manual_urls', $manual_urls );
// phpcs:enable WordPress.Security.NonceVerification.Missing
// Sync to SaaS if changed.
if ( $content_indexing_enabled !== $old_value ) {
$api = Agentorie_API::get_instance();
$api->sync_config( array( 'content_indexing_enabled' => $content_indexing_enabled ) );
// If disabled, clear the index.
if ( ! $content_indexing_enabled && class_exists( 'Agentorie_Content_Indexer' ) ) {
Agentorie_Content_Indexer::get_instance()->clear_index();
}
}
// WooCommerce product indexing toggle (same form).
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified via check_ajax_referer above.
$woo_enabled = isset( $_POST['woo_indexing_enabled'] );
agentorie_chat_update_setting( 'woo_indexing_enabled', $woo_enabled );
// phpcs:enable WordPress.Security.NonceVerification.Missing
$this->save_woo_selection_settings();
if ( ! $woo_enabled ) {
global $wpdb;
$table = Agentorie_Database::site_content_table();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
$wpdb->query( "DELETE FROM {$table} WHERE post_type = 'product'" );
}
wp_send_json_success( array( 'message' => __( 'Settings saved.', 'agentorie-chat' ) ) );
}
/**
* Handle license activation.
*
* @return void
*/
public function handle_activate_license() {
if ( ! isset( $_POST['agentorie_chat_license_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['agentorie_chat_license_nonce'] ) ), 'agentorie_chat_activate_license' ) ) {
wp_die( esc_html__( 'Security check failed.', 'agentorie-chat' ), '', array( 'back_link' => true ) );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ) );
}
$license_key = isset( $_POST['license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) : '';
if ( empty( $license_key ) ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'agentorie-chat-settings',
'tab' => 'general',
'error' => 'empty_key',
),
admin_url( 'admin.php' )
)
);
exit;
}
$api = Agentorie_API::get_instance();
$result = $api->activate_license( $license_key );
if ( is_wp_error( $result ) ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'agentorie-chat-settings',
'tab' => 'general',
'error' => 'activation_failed',
),
admin_url( 'admin.php' )
)
);
exit;
}
agentorie_chat_update_setting( 'license_key', $license_key );
agentorie_chat_update_setting( 'license_status', 'active' );
if ( ! empty( $result['features'] ) ) {
agentorie_chat_update_setting( 'license_features', $result['features'] );
}
Agentorie_Session::clear_license_cache();
wp_safe_redirect(
add_query_arg(
array(
'page' => 'agentorie-chat-settings',
'tab' => 'general',
'activated' => 'true',
),
admin_url( 'admin.php' )
)
);
exit;
}
/**
* Handle license deactivation.
*
* @return void
*/
public function handle_deactivate_license() {
if ( ! isset( $_POST['agentorie_chat_license_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['agentorie_chat_license_nonce'] ) ), 'agentorie_chat_deactivate_license' ) ) {
wp_die( esc_html__( 'Security check failed.', 'agentorie-chat' ), '', array( 'back_link' => true ) );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ) );
}
$api = Agentorie_API::get_instance();
$api->deactivate_license();
agentorie_chat_update_setting( 'license_key', '' );
agentorie_chat_update_setting( 'license_status', 'inactive' );
agentorie_chat_update_setting( 'license_features', array( 'base_chat' ) );
agentorie_chat_update_setting( 'enabled', false );
Agentorie_API_SaaS::invalidate_license_cache();
Agentorie_Session::clear_license_cache();
wp_safe_redirect(
add_query_arg(
array(
'page' => 'agentorie-chat-settings',
'tab' => 'general',
'license' => 'deactivated',
),
admin_url( 'admin.php' )
)
);
exit;
}
/**
* Handle license synchronization.
*
* @return void
*/
public function handle_sync_license() {
if ( ! isset( $_POST['agentorie_chat_license_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['agentorie_chat_license_nonce'] ) ), 'agentorie_chat_sync_license' ) ) {
wp_die( esc_html__( 'Security check failed.', 'agentorie-chat' ), '', array( 'back_link' => true ) );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ) );
}
$license_key = agentorie_chat_get_setting( 'license_key', '' );
if ( empty( $license_key ) ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'agentorie-chat-settings',
'tab' => 'general',
'error' => 'empty_key',
),
admin_url( 'admin.php' )
)
);
exit;
}
Agentorie_API_SaaS::invalidate_license_cache();
$response = Agentorie_API::check_license( $license_key, true );
if ( is_wp_error( $response ) ) {
Agentorie_Logger::error( 'License sync failed', array( 'error' => $response->get_error_message() ) );
wp_safe_redirect(
add_query_arg(
array(
'page' => 'agentorie-chat-settings',
'tab' => 'general',
'error' => 'connection_error',
),
admin_url( 'admin.php' )
)
);
exit;
}
$features = isset( $response['features'] ) ? $response['features'] : array();
Agentorie_Logger::debug( 'License synchronized', array( 'features' => $features ) );
wp_safe_redirect(
add_query_arg(
array(
'page' => 'agentorie-chat-settings',
'tab' => 'general',
'license' => 'synced',
),
admin_url( 'admin.php' )
)
);
exit;
}
/**
* Handle logs download.
*
* @return void
*/
public function handle_download_logs() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ) );
}
if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'agentorie_chat_download_logs' ) ) {
wp_die( esc_html__( 'Security check failed.', 'agentorie-chat' ), '', array( 'back_link' => true ) );
}
if ( ! class_exists( 'Agentorie_Logger' ) ) {
wp_die( esc_html__( 'Logger not available.', 'agentorie-chat' ), '', array( 'back_link' => true ) );
}
Agentorie_Logger::download_logs();
}
/**
* Handle logs clearing.
*
* @return void
*/
public function handle_clear_logs() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ) );
}
if ( ! isset( $_POST['agentorie_chat_logs_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['agentorie_chat_logs_nonce'] ) ), 'agentorie_chat_clear_logs' ) ) {
wp_die( esc_html__( 'Security check failed.', 'agentorie-chat' ), '', array( 'back_link' => true ) );
}
if ( class_exists( 'Agentorie_Logger' ) ) {
Agentorie_Logger::clear_logs();
Agentorie_Logger::info( 'Logs cleared by admin' );
}
wp_safe_redirect(
add_query_arg(
array(
'page' => 'agentorie-chat-settings',
'tab' => 'advanced',
'updated' => 'true',
),
admin_url( 'admin.php' )
)
);
exit;
}
/**
* Handle single conversation deletion.
*
* @return void
*/
public function handle_delete_conversation() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ) );
}
check_admin_referer( 'agentorie_chat_delete_conversation' );
$session_id = isset( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : '';
if ( empty( $session_id ) ) {
wp_safe_redirect( add_query_arg( 'error', 'invalid_session', admin_url( 'admin.php?page=agentorie-chat-conversations' ) ) );
exit;
}
Agentorie_Database::delete_session( $session_id );
wp_safe_redirect( add_query_arg( 'deleted', '1', admin_url( 'admin.php?page=agentorie-chat-conversations' ) ) );
exit;
}
/**
* Handle bulk conversation deletion.
*
* @return void
*/
public function handle_delete_selected() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ) );
}
check_admin_referer( 'agentorie_chat_delete_selected' );
$session_ids = isset( $_POST['session_ids'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['session_ids'] ) ) : array();
if ( empty( $session_ids ) ) {
wp_safe_redirect( add_query_arg( 'error', 'no_selection', admin_url( 'admin.php?page=agentorie-chat-conversations' ) ) );
exit;
}
$deleted_count = 0;
foreach ( $session_ids as $session_id ) {
if ( Agentorie_Database::delete_session( $session_id ) ) {
++$deleted_count;
}
}
wp_safe_redirect( add_query_arg( 'deleted', $deleted_count, admin_url( 'admin.php?page=agentorie-chat-conversations' ) ) );
exit;
}
/**
* Sanitize a value for CSV export to prevent formula injection.
*
* Spreadsheet applications like Excel and LibreOffice can execute
* formulas when a cell value starts with =, +, -, @, tab, or carriage return.
* This method prefixes such values with a single quote to neutralize execution.
*
* @since 1.1.4
* @param mixed $value Raw value to sanitize.
* @return mixed Sanitized value safe for CSV export.
*/
private function sanitize_csv_value( $value ) {
if ( is_string( $value ) && isset( $value[0] ) && in_array( $value[0], array( '=', '+', '-', '@', "\t", "\r" ), true ) ) {
return "'" . $value;
}
return $value;
}
/**
* Handle conversations export.
*
* @return void
*/
public function handle_export() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ) );
}
check_admin_referer( 'agentorie_chat_export_conversations' );
$conversations = Agentorie_Database::get_all_conversations( 1000, 0 );
$filename = 'agentorie-conversations-' . gmdate( 'Y-m-d' ) . '.csv';
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename=' . $filename );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Writing to php://output for CSV download stream
$output = fopen( 'php://output', 'w' );
fprintf( $output, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) );
fputcsv(
$output,
array(
__( 'Session ID', 'agentorie-chat' ),
__( 'Visitor ID', 'agentorie-chat' ),
__( 'Messages Count', 'agentorie-chat' ),
__( 'Human Requested', 'agentorie-chat' ),
__( 'Negotiation', 'agentorie-chat' ),
__( 'Created', 'agentorie-chat' ),
__( 'Last Activity', 'agentorie-chat' ),
__( 'Conversation History', 'agentorie-chat' ),
)
);
// Batch-load all messages to avoid N+1 queries.
$session_ids = array_column( $conversations, 'session_id' );
$all_messages = array();
foreach ( array_chunk( $session_ids, 100 ) as $chunk ) {
$all_messages += Agentorie_Database::get_messages_batch( $chunk, 500 );
}
foreach ( $conversations as $conv ) {
$sid = $conv['session_id'];
$messages = isset( $all_messages[ $sid ] ) ? $all_messages[ $sid ] : array();
$messages_text = '';
if ( ! empty( $messages ) ) {
$msg_parts = array();
foreach ( $messages as $msg ) {
$role = 'user' === $msg['role'] ? __( 'Visitor', 'agentorie-chat' ) : __( 'Bot', 'agentorie-chat' );
$created_at = isset( $msg['created_at'] ) ? $msg['created_at'] : '';
$time = wp_date( 'H:i', Agentorie_Database::to_timestamp( $created_at ) );
$is_adm = ! empty( $msg['is_admin'] ) ? ' (' . __( 'Admin', 'agentorie-chat' ) . ')' : '';
$msg_parts[] = '[' . $time . '] ' . $role . $is_adm . ': ' . $msg['message'];
}
$messages_text = implode( "\n", $msg_parts );
}
$visitor_id = isset( $conv['visitor_id'] ) ? $conv['visitor_id'] : '';
$msg_count = isset( $conv['msg_count'] ) ? $conv['msg_count'] : 0;
$created_at = isset( $conv['created_at'] ) ? $conv['created_at'] : '';
$last_activity = isset( $conv['last_activity'] ) ? $conv['last_activity'] : '';
fputcsv(
$output,
array(
$conv['session_id'],
$this->sanitize_csv_value( $visitor_id ),
$msg_count,
! empty( $conv['human_requested'] ) ? __( 'Yes', 'agentorie-chat' ) : __( 'No', 'agentorie-chat' ),
! empty( $conv['negotiate_detected'] ) ? __( 'Yes', 'agentorie-chat' ) : __( 'No', 'agentorie-chat' ),
$created_at,
$last_activity,
$this->sanitize_csv_value( $messages_text ),
)
);
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Writing to php://output for CSV download
fclose( $output );
exit;
}
/**
* Handle export contacts action.
*
* Exports all contacts to a CSV file.
*
* @return void
*/
public function handle_export_contacts() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Permission denied.', 'agentorie-chat' ) );
}
check_admin_referer( 'agentorie_chat_export_contacts' );
$contacts = Agentorie_Database::get_contacts( 1000, 0 );
$filename = 'agentorie-contacts-' . gmdate( 'Y-m-d' ) . '.csv';
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename=' . $filename );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Writing to php://output for CSV download stream
$output = fopen( 'php://output', 'w' );
fprintf( $output, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) );
fputcsv(
$output,
array(
__( 'Name', 'agentorie-chat' ),
__( 'Email', 'agentorie-chat' ),
__( 'Phone', 'agentorie-chat' ),
__( 'Message', 'agentorie-chat' ),
__( 'Session ID', 'agentorie-chat' ),
__( 'Date', 'agentorie-chat' ),
)
);
foreach ( $contacts as $contact ) {
fputcsv(
$output,
array(
$this->sanitize_csv_value( isset( $contact['name'] ) ? $contact['name'] : '' ),
$this->sanitize_csv_value( isset( $contact['email'] ) ? $contact['email'] : '' ),
$this->sanitize_csv_value( isset( $contact['phone'] ) ? $contact['phone'] : '' ),
$this->sanitize_csv_value( isset( $contact['message'] ) ? $contact['message'] : '' ),
isset( $contact['session_id'] ) ? $contact['session_id'] : '',
isset( $contact['created_at'] ) ? $contact['created_at'] : '',
)
);
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Writing to php://output for CSV download
fclose( $output );
exit;
}
/**
* Check if an admin is currently online.
*
* @return bool
*/
public static function is_admin_online() {
$presence = get_transient( self::PRESENCE_TRANSIENT );
if ( ! $presence || ! is_array( $presence ) || empty( $presence['active'] ) ) {
return false;
}
$last_ping = isset( $presence['last_ping'] ) ? $presence['last_ping'] : 0;
return ( time() - $last_ping ) < self::PRESENCE_TIMEOUT;
}
/**
* Set admin presence status.
*
* @param bool $online Whether admin is online.
* @return void
*/
public static function set_presence( $online ) {
if ( $online ) {
set_transient(
self::PRESENCE_TRANSIENT,
array(
'active' => true,
'admin_id' => get_current_user_id(),
'last_ping' => time(),
),
60
);
} else {
delete_transient( self::PRESENCE_TRANSIENT );
}
}
/**
* Get dashboard statistics.
*
* @return array
*/
private function get_dashboard_stats() {
// Read usage from SaaS response cache (source of truth).
$license_data = get_transient( 'agentorie_chat_license_cache' );
return array(
'total_conversations' => Agentorie_Database::count_conversations_today(),
'total_messages' => Agentorie_Database::count_messages_today(),
'active_sessions' => Agentorie_Database::count_active_sessions(),
'human_requests' => Agentorie_Database::count_human_requests_today(),
'monthly_usage' => isset( $license_data['usage']['conversations'] ) ? (int) $license_data['usage']['conversations'] : 0,
'monthly_limit' => isset( $license_data['usage']['limit'] ) ? (int) $license_data['usage']['limit'] : 100,
'chart_data' => self::get_chart_data( 7 ),
);
}
/**
* Get chart data for the specified number of days.
*
* @param int $days Number of days to retrieve.
* @return array
*/
public static function get_chart_data( $days = 7 ) {
$data = Agentorie_Database::get_messages_per_day( $days );
$labels = array();
$values = array();
$data_map = array();
foreach ( $data as $row ) {
$data_map[ $row['date'] ] = (int) $row['count'];
}
for ( $i = $days - 1; $i >= 0; $i-- ) {
$date = wp_date( 'Y-m-d', strtotime( "-{$i} days" ) );
$labels[] = wp_date( 'D j', strtotime( "-{$i} days" ) );
$values[] = isset( $data_map[ $date ] ) ? $data_map[ $date ] : 0;
}
return array(
'labels' => $labels,
'values' => $values,
);
}
/**
* Get statistics.
*
* @return array
*/
public static function get_stats() {
return Agentorie_Database::get_stats();
}
}