prefix . 'aiohm_vector_entries', $wpdb->prefix . 'aiohm_conversations', $wpdb->prefix . 'aiohm_messages', $wpdb->prefix . 'aiohm_projects' ]; } } /** * Validate table name against allowed tables to prevent SQL injection * * @since 1.0.0 * @param string $table_name Table name to validate * @return bool True if valid, false otherwise */ private static function validate_table_name($table_name) { self::init_allowed_tables(); return in_array($table_name, self::$allowed_tables, true); } /** * Safely get table reference with validation * * @since 1.0.0 * @param string $table_name Table name to validate * @return string Validated table name * @throws Exception If table name is invalid */ private static function get_safe_table_name($table_name) { if (!self::validate_table_name($table_name)) { throw new Exception('Invalid table name for security reasons.'); } return $table_name; } /** * Get safe error message for frontend display * * Sanitizes error messages to prevent information disclosure while providing * helpful feedback to administrators. * * @since 1.0.0 * @param Exception $e The exception * @param string $context Context for logging * @return string Safe error message */ private static function get_safe_error_message($e, $context = 'general') { // For specific admin operations, allow more detailed error messages if ($context === 'Scan' && current_user_can('manage_options')) { $error_message = $e->getMessage(); // Allow consent-related errors to be shown to admins if (strpos($error_message, 'External API calls require user consent') !== false) { return $error_message; } // Allow other specific admin-friendly error messages if (strpos($error_message, 'API key') !== false || strpos($error_message, 'knowledge base') !== false || strpos($error_message, 'File') !== false) { return $error_message; } } // Return generic message for security for other contexts return __('Sorry, something went wrong. Please try again later.', 'aiohm-knowledge-assistant'); } /** * Enhanced input validation for AJAX requests * * Validates and sanitizes input data according to specified rules to prevent * security vulnerabilities and ensure data integrity. * * @since 1.0.0 * @param array $input Input data to validate * @param array $rules Validation rules array with field requirements * @return array|WP_Error Sanitized data or WP_Error on validation failure */ private static function validate_ajax_input($input, $rules) { $sanitized = []; $errors = []; foreach ($rules as $field => $rule) { $value = isset($input[$field]) ? $input[$field] : null; if (isset($rule['required']) && $rule['required'] && (empty($value) && $value !== '0')) { // translators: %s is the field name $errors[] = sprintf(__('Field %s is required.', 'aiohm-knowledge-assistant'), $field); continue; } if (empty($value) && $value !== '0') { $sanitized[$field] = ''; continue; } if (isset($rule['max_length']) && strlen($value) > $rule['max_length']) { // translators: %1$s is the field name, %2$d is the maximum length $errors[] = sprintf(__('Field %1$s exceeds maximum length of %2$d characters.', 'aiohm-knowledge-assistant'), $field, $rule['max_length']); continue; } switch ($rule['type']) { case 'text': $sanitized[$field] = sanitize_text_field($value); break; case 'textarea': $sanitized[$field] = sanitize_textarea_field($value); break; case 'html': $sanitized[$field] = wp_kses_post($value); break; case 'email': $sanitized[$field] = sanitize_email($value); if (!is_email($sanitized[$field])) { // translators: %s is the field name $errors[] = sprintf(__('Field %s must be a valid email address.', 'aiohm-knowledge-assistant'), $field); } break; case 'url': $sanitized[$field] = esc_url_raw($value); if (!filter_var($sanitized[$field], FILTER_VALIDATE_URL)) { // translators: %s is the field name $errors[] = sprintf(__('Field %s must be a valid URL.', 'aiohm-knowledge-assistant'), $field); } break; case 'int': $sanitized[$field] = intval($value); if (isset($rule['min']) && $sanitized[$field] < $rule['min']) { // translators: %1$s is the field name, %2$d is the minimum value $errors[] = sprintf(__('Field %1$s must be at least %2$d.', 'aiohm-knowledge-assistant'), $field, $rule['min']); } if (isset($rule['max']) && $sanitized[$field] > $rule['max']) { // translators: %1$s is the field name, %2$d is the maximum value $errors[] = sprintf(__('Field %1$s must be no more than %2$d.', 'aiohm-knowledge-assistant'), $field, $rule['max']); } break; case 'float': $sanitized[$field] = floatval($value); break; case 'array': if (!is_array($value)) { // translators: %s is the field name $errors[] = sprintf(__('Field %s must be an array.', 'aiohm-knowledge-assistant'), $field); } else { $sanitized[$field] = array_map('sanitize_text_field', $value); } break; case 'json': $decoded = json_decode($value, true); if (json_last_error() !== JSON_ERROR_NONE) { // translators: %s is the field name $errors[] = sprintf(__('Field %s must be valid JSON.', 'aiohm-knowledge-assistant'), $field); } else { $sanitized[$field] = $value; } break; default: $sanitized[$field] = sanitize_text_field($value); } } if (!empty($errors)) { return new WP_Error('validation_failed', implode(' ', $errors)); } return $sanitized; } // ================== START: CONSOLIDATED FIX ================== // By placing these functions directly inside the class, we guarantee they are // always available when needed, preventing the fatal errors that were causing the crashes. private static function create_conversation_internal($user_id, $project_id, $title) { global $wpdb; $table_name = $wpdb->prefix . 'aiohm_conversations'; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery // Internal conversation creation requires direct database access for atomic operations and proper cache invalidation $result = $wpdb->insert($table_name, ['user_id' => $user_id, 'project_id' => $project_id, 'title' => $title], ['%d', '%d', '%s']); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery if ($result) { wp_cache_delete('aiohm_user_conversations_' . $user_id, 'aiohm_core'); wp_cache_delete('aiohm_project_conversations_' . $project_id, 'aiohm_core'); return $wpdb->insert_id; } return false; } private static function add_message_to_conversation_internal($conversation_id, $sender, $content) { global $wpdb; $table_name = $wpdb->prefix . 'aiohm_messages'; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery // Message creation and conversation updates require direct database access for atomic operations $result = $wpdb->insert($table_name, ['conversation_id' => $conversation_id, 'sender' => $sender, 'content' => $content], ['%d', '%s', '%s']); if ($result) { $wpdb->update($wpdb->prefix . 'aiohm_conversations', ['updated_at' => current_time('mysql', 1)], ['id' => $conversation_id]); wp_cache_delete('aiohm_conversation_messages_' . $conversation_id, 'aiohm_core'); wp_cache_delete('aiohm_conversation_' . $conversation_id, 'aiohm_core'); } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery return $result !== false; } // =================== END: CONSOLIDATED FIX =================== /** * Generate AI-powered conversation title */ private static function generate_conversation_title($user_message, $ai_client, $model) { try { $title_prompt = "Create a short title (3-5 words) for this conversation. Remove quotes. Examples: Website Design Project, Marketing Strategy Planning, Brand Development Discussion. Message: " . substr($user_message, 0, 150); $title = $ai_client->get_chat_completion( "Create concise conversation titles without quotes or special characters.", $title_prompt, 0.2, // Low temperature for consistency $model ); $title = trim(wp_strip_all_tags($title)); $title = str_replace(['"', "'", '`', '«', '»'], '', $title); $title = preg_replace('/[^\w\s-]/', '', $title); $title = preg_replace('/\s+/', ' ', $title); $title = trim($title); if (strlen($title) > 50) { $title = mb_strimwidth($title, 0, 47, '...'); } if (strlen($title) < 3 || empty($title) || $title === '...' || strtolower($title) === 'new chat') { $title = self::create_fallback_title($user_message); } return $title; } catch (Exception $e) { return self::create_fallback_title($user_message); } } /** * Create a smart fallback title from user message */ private static function create_fallback_title($user_message) { $message = strtolower(trim($user_message)); // Common patterns for smart titles if (strpos($message, 'help') !== false && strpos($message, 'with') !== false) { return __('Help Request', 'aiohm-knowledge-assistant'); } elseif (strpos($message, 'create') !== false || strpos($message, 'make') !== false) { return __('Creation Project', 'aiohm-knowledge-assistant'); } elseif (strpos($message, 'plan') !== false || strpos($message, 'strategy') !== false) { return __('Planning Session', 'aiohm-knowledge-assistant'); } elseif (strpos($message, 'write') !== false || strpos($message, 'content') !== false) { return __('Content Writing', 'aiohm-knowledge-assistant'); } elseif (strpos($message, 'design') !== false) { return __('Design Discussion', 'aiohm-knowledge-assistant'); } elseif (strpos($message, 'market') !== false) { return __('Marketing Discussion', 'aiohm-knowledge-assistant'); } elseif (strpos($message, 'brand') !== false) { return __('Brand Development', 'aiohm-knowledge-assistant'); } elseif (strpos($message, 'website') !== false || strpos($message, 'web') !== false) { return __('Web Development', 'aiohm-knowledge-assistant'); } elseif (strpos($message, 'question') !== false) { return __('General Questions', 'aiohm-knowledge-assistant'); } else { $words = explode(' ', $message); $title_words = array_slice($words, 0, 3); $title = ucwords(implode(' ', $title_words)); return strlen($title) > 3 ? $title : 'General Discussion'; } } /** * Build the Content Security Policy string from settings. * @param bool $is_admin Whether to build for admin or frontend. * @return string The full CSP header value. */ private static function build_csp_string($is_admin = false) { $settings = AIOHM_KB_Assistant::get_settings(); // Base policies $policies = [ 'default-src' => ["'self'"], 'script-src' => ["'self'", "'unsafe-inline'"], 'style-src' => ["'self'", "'unsafe-inline'"], 'img-src' => ["'self'", 'data:', 'https:'], 'font-src' => ["'self'", 'data:'], 'connect-src' => ["'self'", 'https:'], 'frame-src' => ["'self'"], ]; if ($is_admin) { // Admin needs 'unsafe-eval' for some core scripts and inline styles. $policies['script-src'][] = "'unsafe-eval'"; } // Add user-defined sources $directives = [ 'script-src' => 'csp_script_src', 'style-src' => 'csp_style_src', 'font-src' => 'csp_font_src', 'frame-src' => 'csp_frame_src', 'connect-src' => 'csp_connect_src', 'img-src' => 'csp_img_src', ]; foreach ($directives as $directive => $setting_key) { if (!empty($settings[$setting_key])) { $sources = preg_split('/\s+/', trim($settings[$setting_key])); foreach ($sources as $source) { // Basic validation for URLs or keywords if (filter_var($source, FILTER_VALIDATE_URL) || preg_match("/^'[\w-]+'$/", $source) || $source === 'data:' || $source === 'https:') { if (!in_array($source, $policies[$directive])) { $policies[$directive][] = $source; } } } } } // Build the header string $csp_string = ''; foreach ($policies as $directive => $sources) { $csp_string .= $directive . ' ' . implode(' ', array_unique($sources)) . '; '; } return trim($csp_string); } public static function init() { // Add security headers and session security add_action('admin_init', array(__CLASS__, 'add_admin_security_headers')); // WordPress.org compliance: Add privacy policy content add_action('admin_init', array(__CLASS__, 'add_privacy_policy_content')); add_action('send_headers', array(__CLASS__, 'add_frontend_security_headers')); add_action('wp_login', array(__CLASS__, 'secure_user_session'), 10, 2); add_action('user_register', array(__CLASS__, 'secure_new_user_session')); // --- All original action hooks are preserved --- add_action('wp_ajax_aiohm_progressive_scan', array(__CLASS__, 'handle_progressive_scan_ajax')); add_action('wp_ajax_aiohm_check_api_key', array(__CLASS__, 'handle_check_api_key_ajax')); add_action('wp_ajax_aiohm_save_individual_api_key', array(__CLASS__, 'handle_save_individual_api_key_ajax')); add_action('wp_ajax_aiohm_export_kb', array(__CLASS__, 'handle_export_kb_ajax')); add_action('wp_ajax_aiohm_reset_kb', array(__CLASS__, 'handle_reset_kb_ajax')); add_action('wp_ajax_aiohm_toggle_kb_scope', array(__CLASS__, 'handle_toggle_kb_scope_ajax')); add_action('wp_ajax_aiohm_restore_kb', array(__CLASS__, 'handle_restore_kb_ajax')); add_action('wp_ajax_aiohm_delete_kb_entry', array(__CLASS__, 'handle_delete_kb_entry_ajax')); add_action('wp_ajax_aiohm_bulk_delete_kb', array(__CLASS__, 'handle_bulk_delete_kb_ajax')); add_action('wp_ajax_aiohm_bulk_toggle_kb_scope', array(__CLASS__, 'handle_bulk_toggle_kb_scope_ajax')); add_action('wp_ajax_aiohm_save_brand_soul', array(__CLASS__, 'handle_save_brand_soul_ajax')); add_action('wp_ajax_aiohm_add_brand_soul_to_kb', array(__CLASS__, 'handle_add_brand_soul_to_kb_ajax')); add_action('wp_ajax_aiohm_add_note_to_kb', array(__CLASS__, 'handle_add_note_to_kb_ajax')); add_action('admin_init', array(__CLASS__, 'handle_pdf_download')); // Initialize AJAX handlers self::init_ajax_hooks(); } /** * Add privacy policy content for WordPress.org compliance * Required for plugins that use external services */ public static function add_privacy_policy_content() { if (!function_exists('wp_add_privacy_policy_content')) { return; } $content = '
' . esc_html__('This plugin can optionally connect to external AI services to process your content:', 'aiohm-knowledge-assistant') . '
' . '' . esc_html__('When enabled by an administrator with user consent:', 'aiohm-knowledge-assistant') . '
' . '' . sprintf( // translators: %1$s is the ShareAI Terms URL link, %2$s is the Privacy Policy URL link esc_html__('ShareAI Terms: %1$s | Privacy Policy: %2$s', 'aiohm-knowledge-assistant'), 'https://shareai.now/terms', 'https://shareai.now/privacy' ) . '
' . '' . esc_html__('All external connections are disabled by default and require explicit administrator consent. You can disable external services at any time in the plugin settings.', 'aiohm-knowledge-assistant') . '
'; wp_add_privacy_policy_content('AIOHM Knowledge Assistant', wp_kses_post($content)); } /** * Initialize AJAX handlers and monitoring hooks */ private static function init_ajax_hooks() { add_action('wp_ajax_aiohm_save_mirror_mode_settings', array(__CLASS__, 'handle_save_mirror_mode_settings_ajax')); add_action('wp_ajax_aiohm_save_muse_mode_settings', array(__CLASS__, 'handle_save_muse_mode_settings_ajax')); // SECURITY: Removed nopriv hook - settings should only be accessible to authenticated users // Add hook to monitor settings changes add_action('update_option_aiohm_kb_settings', array(__CLASS__, 'monitor_settings_changes'), 10, 2); add_action('delete_option_aiohm_kb_settings', array(__CLASS__, 'monitor_settings_deletion'), 10, 1); add_action('wp_ajax_aiohm_generate_mirror_mode_qa', array(__CLASS__, 'handle_generate_mirror_mode_qa_ajax')); add_action('wp_ajax_aiohm_test_mirror_mode_chat', array(__CLASS__, 'handle_test_mirror_mode_chat_ajax')); add_action('wp_ajax_aiohm_save_muse_mode_settings', array(__CLASS__, 'handle_save_muse_mode_settings_ajax')); add_action('wp_ajax_aiohm_private_assistant_chat', array(__CLASS__, 'handle_private_assistant_chat_ajax')); add_action('wp_ajax_aiohm_private_chat', array(__CLASS__, 'handle_private_assistant_chat_ajax')); add_action('wp_ajax_aiohm_test_muse_mode_chat', array(__CLASS__, 'handle_test_muse_mode_chat_ajax')); add_action('wp_ajax_nopriv_aiohm_frontend_chat', array(__CLASS__, 'handle_frontend_chat_ajax')); add_action('wp_ajax_aiohm_frontend_chat', array(__CLASS__, 'handle_frontend_chat_ajax')); add_action('wp_ajax_nopriv_aiohm_search_knowledge', array(__CLASS__, 'handle_search_knowledge_ajax')); add_action('wp_ajax_aiohm_search_knowledge', array(__CLASS__, 'handle_search_knowledge_ajax')); add_action('wp_ajax_aiohm_admin_search_knowledge', array(__CLASS__, 'handle_admin_search_knowledge_ajax')); // --- Existing FIX for project and conversations --- add_action('wp_ajax_aiohm_get_project_conversations', array(__CLASS__, 'handle_get_project_conversations_ajax')); add_action('wp_ajax_aiohm_create_project', array(__CLASS__, 'handle_create_project_ajax')); // --- NEW FIX: Add the missing action handler for loading all projects and conversations --- add_action('wp_ajax_aiohm_load_history', array(__CLASS__, 'handle_load_history_ajax')); add_action('wp_ajax_aiohm_load_conversation', array(__CLASS__, 'handle_load_conversation_ajax')); add_action('wp_ajax_aiohm_save_project_notes', array(__CLASS__, 'handle_save_project_notes_ajax')); add_action('wp_ajax_aiohm_load_project_notes', array(__CLASS__, 'handle_load_project_notes_ajax')); add_action('wp_ajax_aiohm_delete_project', array(__CLASS__, 'handle_delete_project_ajax')); add_action('wp_ajax_aiohm_delete_conversation', array(__CLASS__, 'handle_delete_conversation_ajax')); add_action('wp_ajax_aiohm_create_conversation', array(__CLASS__, 'handle_create_conversation_ajax')); add_action('wp_ajax_aiohm_upload_project_files', array(__CLASS__, 'handle_upload_project_files_ajax')); add_action('wp_ajax_aiohm_get_brand_soul_content', array(__CLASS__, 'handle_get_brand_soul_content_ajax')); add_action('wp_ajax_aiohm_get_content_for_view', array(__CLASS__, 'handle_get_content_for_view_ajax')); add_action('wp_ajax_aiohm_get_usage_stats', array(__CLASS__, 'handle_get_usage_stats_ajax')); add_action('wp_ajax_aiohm_download_conversation_pdf', array(__CLASS__, 'handle_download_conversation_pdf_ajax')); add_action('wp_ajax_aiohm_add_conversation_to_kb', array(__CLASS__, 'handle_add_conversation_to_kb_ajax')); add_action('wp_ajax_aiohm_research_online', array(__CLASS__, 'handle_research_online_ajax')); add_action('wp_ajax_aiohm_get_pdf_url', array(__CLASS__, 'handle_get_pdf_url_ajax')); // File upload to KB handler add_action('wp_ajax_aiohm_kb_file_upload', array(__CLASS__, 'handle_kb_file_upload_ajax')); add_action('wp_ajax_aiohm_update_json_content', array(__CLASS__, 'handle_update_json_content_ajax')); add_action('wp_ajax_aiohm_update_text_content', array(__CLASS__, 'handle_update_text_content_ajax')); add_action('wp_ajax_aiohm_reprocess_pdf_content', array(__CLASS__, 'handle_reprocess_pdf_content_ajax')); add_action('wp_ajax_aiohm_reprocess_html_content', array(__CLASS__, 'handle_reprocess_html_content_ajax')); // Help Page AJAX handlers add_action('wp_ajax_aiohm_get_debug_info', array(__CLASS__, 'handle_get_debug_info_ajax')); add_action('wp_ajax_aiohm_test_all_api_connections', array(__CLASS__, 'handle_test_all_api_connections_ajax')); add_action('wp_ajax_aiohm_check_database_health', array(__CLASS__, 'handle_check_database_health_ajax')); add_action('wp_ajax_aiohm_submit_support_request', array(__CLASS__, 'handle_submit_support_request_ajax')); add_action('wp_ajax_aiohm_submit_feature_request', array(__CLASS__, 'handle_submit_feature_request_ajax')); // Allow JSON file uploads for KB functionality add_filter('upload_mimes', array(__CLASS__, 'allow_json_uploads')); add_filter('wp_check_filetype_and_ext', array(__CLASS__, 'allow_json_file_upload'), 10, 4); } /** * Handles the AJAX request to load all projects and recent conversations for the current user. * This directly supports the `loadHistory()` function in JavaScript. */ /** * Check if an AI provider is configured with valid credentials */ private static function is_provider_configured($settings, $provider) { switch ($provider) { case 'shareai': return !empty($settings['shareai_api_key']); case 'openai': return !empty($settings['openai_api_key']) || (function_exists('aiohm_kb_is_wp_ai_connector_configured') && aiohm_kb_is_wp_ai_connector_configured('openai')); case 'gemini': return !empty($settings['gemini_api_key']) || (function_exists('aiohm_kb_is_wp_ai_connector_configured') && aiohm_kb_is_wp_ai_connector_configured('gemini')); case 'claude': return !empty($settings['claude_api_key']) || (function_exists('aiohm_kb_is_wp_ai_connector_configured') && aiohm_kb_is_wp_ai_connector_configured('claude')); default: return false; } } public static function handle_load_history_ajax() { check_ajax_referer('aiohm_private_chat_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission Denied', 'aiohm-knowledge-assistant')]); wp_die(); } global $wpdb; $user_id = get_current_user_id(); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // User data queries with caching for dashboard performance // Fetch projects with caching $projects_cache_key = 'aiohm_user_projects_' . $user_id; $projects = wp_cache_get($projects_cache_key, 'aiohm_core'); if (false === $projects) { // Validate table name for security $safe_table = self::get_safe_table_name($wpdb->prefix . 'aiohm_projects'); $projects = $wpdb->get_results($wpdb->prepare( "SELECT id, project_name as name FROM `" . esc_sql($safe_table) . "` WHERE user_id = %d ORDER BY creation_date DESC", $user_id ), ARRAY_A); wp_cache_set($projects_cache_key, $projects, 'aiohm_core', 300); // 5 minute cache } // Fetch recent conversations with caching $conversations_cache_key = 'aiohm_user_conversations_' . $user_id; $conversations = wp_cache_get($conversations_cache_key, 'aiohm_core'); if (false === $conversations) { $conversations = $wpdb->get_results($wpdb->prepare( "SELECT id, title, project_id FROM " . $wpdb->prefix . "aiohm_conversations WHERE user_id = %d ORDER BY updated_at DESC LIMIT 50", $user_id ), ARRAY_A); wp_cache_set($conversations_cache_key, $conversations, 'aiohm_core', 180); // 3 minute cache } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching wp_send_json_success(['projects' => $projects, 'conversations' => $conversations]); wp_die(); } /** * Handles the AJAX request to load a specific conversation's messages. */ public static function handle_load_conversation_ajax() { check_ajax_referer('aiohm_private_chat_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission Denied', 'aiohm-knowledge-assistant')]); wp_die(); } $conversation_id = isset($_POST['conversation_id']) ? intval($_POST['conversation_id']) : 0; if (empty($conversation_id)) { wp_send_json_error(['message' => __('Invalid Conversation ID.', 'aiohm-knowledge-assistant')]); wp_die(); } global $wpdb; $user_id = get_current_user_id(); $messages_table = $wpdb->prefix . 'aiohm_messages'; $conversations_table = $wpdb->prefix . 'aiohm_conversations'; $projects_table = $wpdb->prefix . 'aiohm_projects'; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Conversation lookup with caching for message retrieval // Verify conversation belongs to the user and get its project ID with caching $conversation_cache_key = 'aiohm_conversation_' . $conversation_id . '_' . $user_id; $conversation_info = wp_cache_get($conversation_cache_key, 'aiohm_core'); if (false === $conversation_info) { $conversation_info = $wpdb->get_row($wpdb->prepare( "SELECT c.id, c.title, c.project_id, p.project_name FROM " . $wpdb->prefix . "aiohm_conversations c JOIN " . $wpdb->prefix . "aiohm_projects p ON c.project_id = p.id WHERE c.id = %d AND c.user_id = %d", $conversation_id, $user_id ), ARRAY_A); if ($conversation_info) { wp_cache_set($conversation_cache_key, $conversation_info, 'aiohm_core', 600); // 10 minute cache } } if (!$conversation_info) { wp_send_json_error(['message' => __('Conversation not found or not accessible.', 'aiohm-knowledge-assistant')]); wp_die(); } // Fetch messages for the conversation with caching $messages_cache_key = 'aiohm_conversation_messages_' . $conversation_id; $messages = wp_cache_get($messages_cache_key, 'aiohm_core'); if (false === $messages) { $messages = $wpdb->get_results($wpdb->prepare( "SELECT sender, content as message_content FROM " . $wpdb->prefix . "aiohm_messages WHERE conversation_id = %d ORDER BY created_at ASC", $conversation_id ), ARRAY_A); wp_cache_set($messages_cache_key, $messages, 'aiohm_core', 300); // 5 minute cache } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching wp_send_json_success([ 'messages' => $messages, 'project_id' => $conversation_info['project_id'], 'project_name' => $conversation_info['project_name'], 'conversation_title' => $conversation_info['title'] ]); wp_die(); } /** * Handles AJAX request to save project notes. */ public static function handle_save_project_notes_ajax() { check_ajax_referer('aiohm_private_chat_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission Denied', 'aiohm-knowledge-assistant')]); wp_die(); } $project_id = isset($_POST['project_id']) ? intval($_POST['project_id']) : 0; $note_content = isset($_POST['note_content']) ? sanitize_textarea_field(wp_unslash($_POST['note_content'])) : ''; if (empty($project_id)) { wp_send_json_error(['message' => __('Invalid Project ID.', 'aiohm-knowledge-assistant')]); wp_die(); } global $wpdb; $table_name = $wpdb->prefix . 'aiohm_projects'; $user_id = get_current_user_id(); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared // Project ownership verification and notes update require direct database access with proper table name interpolation // Ensure the project belongs to the current user $exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(id) FROM " . $wpdb->prefix . "aiohm_projects WHERE id = %d AND user_id = %d", $project_id, $user_id )); if (!$exists) { wp_send_json_error(['message' => __('Project not found or not owned by user.', 'aiohm-knowledge-assistant')]); wp_die(); } // Update project notes with cache clearing $updated = $wpdb->update( $table_name, ['notes' => $note_content], ['id' => $project_id], ['%s'], ['%d'] ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ($updated !== false) { wp_send_json_success(['message' => __('Notes saved.', 'aiohm-knowledge-assistant')]); } else { wp_send_json_error(['message' => __('Failed to save notes.', 'aiohm-knowledge-assistant')]); } wp_die(); } /** * Handles AJAX request to load project notes. */ public static function handle_load_project_notes_ajax() { check_ajax_referer('aiohm_private_chat_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission Denied', 'aiohm-knowledge-assistant')]); wp_die(); } $project_id = isset($_POST['project_id']) ? intval($_POST['project_id']) : 0; if (empty($project_id)) { wp_send_json_error(['message' => __('Invalid Project ID.', 'aiohm-knowledge-assistant')]); wp_die(); } global $wpdb; $table_name = $wpdb->prefix . 'aiohm_projects'; $user_id = get_current_user_id(); // Get notes with caching $notes_cache_key = 'aiohm_project_notes_' . $project_id . '_' . $user_id; $note_content = wp_cache_get($notes_cache_key, 'aiohm_core'); if (false === $note_content) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared // Project notes query with caching implementation requires direct database access with table name interpolation $note_content = $wpdb->get_var($wpdb->prepare( "SELECT notes FROM " . $wpdb->prefix . "aiohm_projects WHERE id = %d AND user_id = %d", $project_id, $user_id )); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared wp_cache_set($notes_cache_key, $note_content, 'aiohm_core', 600); // 10 minute cache } if ($note_content !== null) { wp_send_json_success(['note_content' => $note_content]); } else { wp_send_json_error(['message' => __('Project notes not found or not accessible.', 'aiohm-knowledge-assistant')]); } wp_die(); } /** * Handles AJAX request to delete a project. */ public static function handle_delete_project_ajax() { check_ajax_referer('aiohm_private_chat_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission Denied', 'aiohm-knowledge-assistant')]); wp_die(); } $project_id = isset($_POST['project_id']) ? intval($_POST['project_id']) : 0; if (empty($project_id)) { wp_send_json_error(['message' => __('Invalid Project ID.', 'aiohm-knowledge-assistant')]); wp_die(); } global $wpdb; $user_id = get_current_user_id(); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Project deletion operations require direct database access for atomic deletion with proper cache clearing // Delete associated conversations first $conversations_table = $wpdb->prefix . 'aiohm_conversations'; $wpdb->delete($conversations_table, ['project_id' => $project_id, 'user_id' => $user_id], ['%d', '%d']); // Delete the project $projects_table = $wpdb->prefix . 'aiohm_projects'; $deleted = $wpdb->delete($projects_table, ['id' => $project_id, 'user_id' => $user_id], ['%d', '%d']); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching if ($deleted) { // Clear all caches related to this project and user wp_cache_delete('aiohm_user_projects_' . $user_id, 'aiohm_core'); wp_cache_delete('aiohm_user_conversations_' . $user_id, 'aiohm_core'); wp_cache_delete('aiohm_project_notes_' . $project_id . '_' . $user_id, 'aiohm_core'); wp_cache_delete('aiohm_project_conversations_' . $project_id, 'aiohm_core'); wp_send_json_success(['message' => __('Project and its conversations deleted.', 'aiohm-knowledge-assistant')]); } else { wp_send_json_error(['message' => __('Failed to delete project or project not found.', 'aiohm-knowledge-assistant')]); } wp_die(); } /** * Handles AJAX request to delete a conversation. */ public static function handle_delete_conversation_ajax() { check_ajax_referer('aiohm_private_chat_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission Denied', 'aiohm-knowledge-assistant')]); wp_die(); } $conversation_id = isset($_POST['conversation_id']) ? intval($_POST['conversation_id']) : 0; if (empty($conversation_id)) { wp_send_json_error(['message' => __('Invalid Conversation ID.', 'aiohm-knowledge-assistant')]); wp_die(); } global $wpdb; $user_id = get_current_user_id(); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Conversation deletion operations require direct database access for atomic deletion with proper cache clearing // Delete associated messages first $messages_table = $wpdb->prefix . 'aiohm_messages'; $wpdb->delete($messages_table, ['conversation_id' => $conversation_id], ['%d']); // Delete the conversation $conversations_table = $wpdb->prefix . 'aiohm_conversations'; $deleted = $wpdb->delete($conversations_table, ['id' => $conversation_id, 'user_id' => $user_id], ['%d', '%d']); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching if ($deleted) { wp_send_json_success(['message' => __('Conversation and its messages deleted.', 'aiohm-knowledge-assistant')]); } else { wp_send_json_error(['message' => __('Failed to delete conversation or conversation not found.', 'aiohm-knowledge-assistant')]); } wp_die(); } /** * Handles AJAX request to create a new conversation. */ public static function handle_create_conversation_ajax() { check_ajax_referer('aiohm_private_chat_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission Denied', 'aiohm-knowledge-assistant')]); wp_die(); } $project_id = isset($_POST['project_id']) ? intval($_POST['project_id']) : 0; $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : 'New Chat'; if (empty($project_id)) { wp_send_json_error(['message' => __('Invalid Project ID.', 'aiohm-knowledge-assistant')]); wp_die(); } global $wpdb; $user_id = get_current_user_id(); $projects_table = $wpdb->prefix . 'aiohm_projects'; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared // Project verification query requires direct database access with table name interpolation $exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(id) FROM " . $wpdb->prefix . "aiohm_projects WHERE id = %d AND user_id = %d", $project_id, $user_id )); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared if (!$exists) { wp_send_json_error(['message' => __('Project not found or not owned by user.', 'aiohm-knowledge-assistant')]); wp_die(); } $conversation_id = self::create_conversation_internal($user_id, $project_id, $title); if ($conversation_id) { wp_send_json_success(['conversation_id' => $conversation_id, 'title' => $title]); } else { wp_send_json_error(['message' => __('Failed to create conversation.', 'aiohm-knowledge-assistant')]); } wp_die(); } // ================== START: NEW FUNCTION TO FIX PROJECTS ================== /** * Handles the AJAX request to get all conversations for a specific project. * This function was missing, causing projects not to load. */ public static function handle_get_project_conversations_ajax() { check_ajax_referer('aiohm_private_chat_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Permission Denied', 'aiohm-knowledge-assistant')]); wp_die(); } $project_id = isset($_POST['project_id']) ? intval($_POST['project_id']) : 0; if (empty($project_id)) { wp_send_json_error(['message' => __('Invalid Project ID.', 'aiohm-knowledge-assistant')]); wp_die(); } global $wpdb; $user_id = get_current_user_id(); $table_name = $wpdb->prefix . 'aiohm_conversations'; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared // User conversations query requires direct database access with table name interpolation $conversations = $wpdb->get_results($wpdb->prepare( "SELECT id, title FROM " . $wpdb->prefix . "aiohm_conversations WHERE user_id = %d AND project_id = %d ORDER BY updated_at DESC", $user_id, $project_id )); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared wp_send_json_success(['conversations' => $conversations]); wp_die(); } public static function handle_create_project_ajax() { check_ajax_referer('aiohm_private_chat_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => 'Permission denied.']); wp_die(); } $project_name = isset($_POST['name']) ? sanitize_text_field(wp_unslash($_POST['name'])) : ''; if (empty($project_name)) { wp_send_json_error(['message' => __('Project name cannot be empty.', 'aiohm-knowledge-assistant')]); wp_die(); } global $wpdb; $table_name = $wpdb->prefix . 'aiohm_projects'; $user_id = get_current_user_id(); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery // Project creation requires direct database access with cache invalidation handled elsewhere $result = $wpdb->insert($table_name, ['user_id' => $user_id, 'project_name' => $project_name], ['%d', '%s']); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery if ($result === false) { wp_send_json_error(['message' => __('Could not save the project to the database.', 'aiohm-knowledge-assistant')]); } else { $project_id = $wpdb->insert_id; wp_send_json_success(['new_project_id' => $project_id, 'name' => $project_name]); } wp_die(); } public static function handle_private_assistant_chat_ajax() { // Enhanced rate limiting for private chat if (!AIOHM_KB_Assistant::check_enhanced_rate_limit('private_chat', get_current_user_id(), 30)) { wp_send_json_error(['message' => __('Private chat rate limit exceeded. Please wait before sending more messages.', 'aiohm-knowledge-assistant')]); } if (!check_ajax_referer('aiohm_private_chat_nonce', 'nonce', false)) { wp_send_json_error(['message' => __('Security check failed.', 'aiohm-knowledge-assistant')]); } if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('You do not have permission to use this feature.', 'aiohm-knowledge-assistant')]); } try { $user_message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : ''; // Enhanced message validation if (empty(trim($user_message))) { wp_send_json_error(['message' => __('Message cannot be empty.', 'aiohm-knowledge-assistant')]); } if (strlen($user_message) > 3000) { wp_send_json_error(['message' => __('Message is too long. Please limit to 3000 characters.', 'aiohm-knowledge-assistant')]); } // Check for potentially malicious content if (preg_match('/