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 '

' . esc_html__( 'Agentorie Chat is currently disabled.', 'agentorie-chat' ) . ' ' . esc_html__( 'Enable it in Settings', 'agentorie-chat' ) . '

'; } } /** * 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(); } }