mxchat_init_ajax_hooks(); } /** * Register all AJAX action hooks */ private function mxchat_init_ajax_hooks() { // Settings AJAX add_action('wp_ajax_mxchat_save_setting', array($this, 'mxchat_save_setting_callback')); add_action('wp_ajax_mxchat_save_prompts_setting', array($this, 'mxchat_save_prompts_setting_callback')); add_action('wp_ajax_migrate_pinecone_settings', array($this, 'ajax_migrate_pinecone_settings')); // License AJAX add_action('wp_ajax_mxchat_handle_activate_license', array($this, 'mxchat_handle_activate_license')); add_action('wp_ajax_mxchat_check_license_status', array($this, 'mxchat_check_license_status')); add_action('wp_ajax_mxchat_deactivate_license', array($this, 'mxchat_deactivate_license')); // Actions & Intents AJAX add_action('wp_ajax_mxchat_toggle_action', array($this, 'mxchat_toggle_action')); add_action('wp_ajax_mxchat_update_intent_threshold', array($this, 'mxchat_update_intent_threshold')); add_action('wp_ajax_mxchat_save_selected_bot', array($this, 'mxchat_save_selected_bot')); add_action('wp_ajax_mxchat_check_api_keys', array($this, 'mxchat_check_api_keys')); // Debug & Optimization AJAX add_action('wp_ajax_mxchat_toggle_debug_mode', array($this, 'mxchat_toggle_debug_mode_callback')); add_action('wp_ajax_mxchat_get_debug_log', array($this, 'mxchat_get_debug_log_callback')); add_action('wp_ajax_mxchat_clear_debug_log', array($this, 'mxchat_clear_debug_log_callback')); add_action('wp_ajax_mxchat_export_settings', array($this, 'mxchat_export_settings_callback')); add_action('wp_ajax_mxchat_reset_all_settings', array($this, 'mxchat_reset_all_settings_callback')); // Global rate-limit usage counter reset (admin-only, nonce-guarded) add_action('wp_ajax_mxchat_reset_global_rate_limit', array($this, 'mxchat_reset_global_rate_limit_callback')); // Custom (OpenAI-compatible) Provider connection test add_action('wp_ajax_mxchat_test_custom_provider', array($this, 'mxchat_test_custom_provider_callback')); // Built-in provider key validation — cheap per-provider auth check (plan-mxchat-20260623-c41f74) add_action('wp_ajax_mxchat_test_provider_key', array($this, 'mxchat_test_provider_key_callback')); } /** * Test connection to a Custom (OpenAI-compatible) provider by hitting its /models endpoint * with whichever auth scheme the user configured. Reports model count or a clean error. */ public function mxchat_test_custom_provider_callback() { check_ajax_referer('mxchat_test_custom_provider'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => esc_html__('Unauthorized', 'mxchat'))); } $options = get_option('mxchat_options', array()); $base_url = isset($options['custom_provider_base_url']) ? trim((string) $options['custom_provider_base_url']) : ''; $api_key = isset($options['custom_provider_api_key']) ? trim((string) $options['custom_provider_api_key']) : ''; $auth = isset($options['custom_provider_auth_scheme']) ? $options['custom_provider_auth_scheme'] : 'bearer'; $api_version = isset($options['custom_provider_api_version']) ? trim((string) $options['custom_provider_api_version']) : ''; if (empty($base_url)) { wp_send_json_error(array('message' => esc_html__('Base URL is empty. Save it first.', 'mxchat'))); } $url = rtrim($base_url, '/') . '/models'; if (!empty($api_version)) { $url = add_query_arg('api-version', $api_version, $url); } $headers = array('Content-Type' => 'application/json'); if (!empty($api_key)) { if ($auth === 'api-key') { $headers['api-key'] = $api_key; } else { $headers['Authorization'] = 'Bearer ' . $api_key; } } $response = wp_remote_get($url, array( 'headers' => $headers, 'timeout' => 10, )); if (is_wp_error($response)) { wp_send_json_error(array('message' => sprintf(esc_html__('Network error: %s', 'mxchat'), esc_html($response->get_error_message())))); } $code = (int) wp_remote_retrieve_response_code($response); if ($code === 401 || $code === 403) { wp_send_json_error(array('message' => sprintf(esc_html__('Auth rejected (HTTP %d). Check API key and auth scheme.', 'mxchat'), $code))); } if ($code === 404) { wp_send_json_error(array('message' => esc_html__('Endpoint not found (HTTP 404). Check the Base URL.', 'mxchat'))); } if ($code < 200 || $code >= 300) { wp_send_json_error(array('message' => sprintf(esc_html__('Upstream returned HTTP %d.', 'mxchat'), $code))); } $body = json_decode(wp_remote_retrieve_body($response), true); $count = 0; if (is_array($body)) { if (isset($body['data']) && is_array($body['data'])) { $count = count($body['data']); } elseif (isset($body['models']) && is_array($body['models'])) { $count = count($body['models']); } } wp_send_json_success(array( 'message' => sprintf(esc_html__('Connection OK — %d model(s) reported.', 'mxchat'), $count), 'count' => $count, )); } /** * Validate a BUILT-IN provider key with the lightest authenticated call per * provider (a /models or key-info GET — never a generation). Reads the posted * key value so the owner can test BEFORE saving; falls back to the saved option * when the field is empty. Mirrors mxchat_test_custom_provider_callback and the * add-on test buttons (cf5bd5 veo / 8d16f1 perplexity). The key is never logged. * plan-mxchat-20260623-c41f74. */ public function mxchat_test_provider_key_callback() { check_ajax_referer('mxchat_test_provider_key'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => esc_html__('Unauthorized', 'mxchat'))); } $provider = isset($_POST['provider']) ? sanitize_key(wp_unslash($_POST['provider'])) : ''; $posted_key = isset($_POST['key']) ? trim((string) wp_unslash($_POST['key'])) : ''; $option_map = array( 'openai' => 'api_key', 'xai' => 'xai_api_key', 'claude' => 'claude_api_key', 'deepseek' => 'deepseek_api_key', 'gemini' => 'gemini_api_key', 'openrouter' => 'openrouter_api_key', ); if (!isset($option_map[$provider])) { wp_send_json_error(array('message' => esc_html__('Unknown provider.', 'mxchat'))); } // Prefer the just-typed value (test-before-save); fall back to the saved key. $key = $posted_key; if ($key === '') { $options = get_option('mxchat_options', array()); $key = isset($options[$option_map[$provider]]) ? trim((string) $options[$option_map[$provider]]) : ''; } if ($key === '') { wp_send_json_error(array('message' => esc_html__('No API key entered or saved for this provider.', 'mxchat'))); } // Lightest authenticated metadata call per provider — model-agnostic, no generation. $headers = array(); switch ($provider) { case 'openai': $url = 'https://api.openai.com/v1/models'; $headers = array('Authorization' => 'Bearer ' . $key); break; case 'xai': $url = 'https://api.x.ai/v1/models'; $headers = array('Authorization' => 'Bearer ' . $key); break; case 'deepseek': $url = 'https://api.deepseek.com/models'; $headers = array('Authorization' => 'Bearer ' . $key); break; case 'openrouter': // /auth/key validates the key itself (the public /models list does not). $url = 'https://openrouter.ai/api/v1/auth/key'; $headers = array('Authorization' => 'Bearer ' . $key); break; case 'gemini': $url = add_query_arg(array('pageSize' => 1, 'key' => $key), 'https://generativelanguage.googleapis.com/v1beta/models'); break; case 'claude': $url = 'https://api.anthropic.com/v1/models'; $headers = array('x-api-key' => $key, 'anthropic-version' => '2023-06-01'); break; default: wp_send_json_error(array('message' => esc_html__('Unknown provider.', 'mxchat'))); } $response = wp_remote_get($url, array( 'headers' => $headers, 'timeout' => 10, )); if (is_wp_error($response)) { wp_send_json_error(array('message' => sprintf(esc_html__('Network error: %s', 'mxchat'), esc_html($response->get_error_message())))); } $code = (int) wp_remote_retrieve_response_code($response); if ($code >= 200 && $code < 300) { wp_send_json_success(array('message' => esc_html__('Key is valid.', 'mxchat'))); } // Surface the provider's own error text when present (trimmed; key never echoed). $detail = ''; $body = json_decode(wp_remote_retrieve_body($response), true); if (is_array($body)) { if (isset($body['error']['message'])) { $detail = $body['error']['message']; } elseif (isset($body['error']) && is_string($body['error'])) { $detail = $body['error']; } elseif (isset($body['message'])) { $detail = $body['message']; } } $detail = trim((string) $detail); if (strlen($detail) > 200) { $detail = substr($detail, 0, 200) . '…'; } if ($code === 401 || $code === 403) { $msg = ($detail !== '') ? sprintf(esc_html__('Key rejected (HTTP %1$d): %2$s', 'mxchat'), $code, esc_html($detail)) : sprintf(esc_html__('Key rejected (HTTP %d). Check the API key.', 'mxchat'), $code); wp_send_json_error(array('message' => $msg)); } $msg = ($detail !== '') ? sprintf(esc_html__('Provider returned HTTP %1$d: %2$s', 'mxchat'), $code, esc_html($detail)) : sprintf(esc_html__('Provider returned HTTP %d.', 'mxchat'), $code); wp_send_json_error(array('message' => $msg)); } // ======================================== // SETTINGS AJAX HANDLERS // ======================================== /** * Validates and saves chat settings via AJAX request */ public function mxchat_save_setting_callback() { check_ajax_referer('mxchat_save_setting_nonce'); if (!current_user_can('manage_options')) { ('MXChat Save: Unauthorized access attempt'); wp_send_json_error(['message' => esc_html__('Unauthorized', 'mxchat')]); } $name = isset($_POST['name']) ? $_POST['name'] : ''; // Remove WP's added slashes before saving (wp_unslash is the canonical form; plan-3f8158). $value = isset($_POST['value']) ? wp_unslash($_POST['value']) : ''; //error_log('MXChat Save: Processing field name: ' . $name); //error_log('MXChat Save: Field value: ' . $value); if (empty($name)) { //error_log('MXChat Save: Empty field name detected'); wp_send_json_error(['message' => esc_html__('Invalid field name', 'mxchat')]); } // Load the full options array $options = get_option('mxchat_options', []); //error_log('MXChat Save: Current options array: ' . print_r($options, true)); // Extract field name from mxchat_options[field_name] format if present // But preserve the full name for special cases like rate_limits that need the full path $field_name = $name; if (preg_match('/^mxchat_options\[([^\[\]]+)\]$/', $name, $matches)) { $field_name = $matches[1]; } // Handle special cases switch ($field_name) { case 'model': //error_log('MXChat Save: Processing model selection'); //error_log('MXChat Save: Model value received: ' . $value); //error_log('MXChat Save: Value type: ' . gettype($value)); //error_log('MXChat Save: Value length: ' . strlen($value)); //error_log('MXChat Save: Value === "openrouter": ' . ($value === 'openrouter' ? 'YES' : 'NO')); // Allow 'openrouter' or validate against whitelist if ($value === 'openrouter') { //error_log('MXChat Save: Setting model to openrouter'); $options['model'] = 'openrouter'; } else { //error_log('MXChat Save: Checking against whitelist'); // Catalog refactor (plan-d14e89): canonical allowlist lives in // includes/class-mxchat-model-catalog.php. A new chat model // added there is automatically accepted by autosave. if (!class_exists('MxChat_Model_Catalog')) { require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-mxchat-model-catalog.php'; } $allowed_models = MxChat_Model_Catalog::chat_model_ids(); //error_log('MXChat Save: in_array result: ' . (in_array($value, $allowed_models) ? 'YES' : 'NO')); if (in_array($value, $allowed_models)) { //error_log('MXChat Save: Model is in whitelist, saving'); $options['model'] = sanitize_text_field($value); } else { //error_log('MXChat Save: Invalid model rejected: ' . $value); //error_log('MXChat Save: Allowed models: ' . print_r($allowed_models, true)); wp_send_json_error(['message' => esc_html__('Invalid model selected', 'mxchat')]); return; } } break; case 'openrouter_selected_model': //error_log('MXChat Save: Processing OpenRouter model: ' . $value); $options['openrouter_selected_model'] = sanitize_text_field($value); // Force immediate save for new keys //error_log('MXChat Save: OpenRouter model saved immediately'); break; case 'openrouter_selected_model_name': //error_log('MXChat Save: Processing OpenRouter model name: ' . $value); $options['openrouter_selected_model_name'] = sanitize_text_field($value); // Force immediate save for new keys //error_log('MXChat Save: OpenRouter model name saved immediately'); break; case 'openrouter_api_key': //error_log('MXChat Save: Processing OpenRouter API key'); $options['openrouter_api_key'] = sanitize_text_field($value); break; // REMOVED DUPLICATE case 'openrouter_selected_model_name' HERE! case 'additional_popular_questions': //error_log('MXChat Save: Processing additional_popular_questions'); $questions = json_decode($value, true); // No need for stripslashes here if (is_array($questions)) { $options[$field_name] = $questions; // Also update old option for backwards compatibility update_option('additional_popular_questions', $questions); //error_log('MXChat Save: Saved ' . count($questions) . ' additional questions'); } else { //error_log('MXChat Save: Failed to decode questions JSON'); } break; case 'email_blocker_header_content': //error_log('MXChat Save: Processing email_blocker_header_content'); // Allow HTML content but sanitize it safely $options[$field_name] = wp_kses_post($value); break; case 'intro_message': // Stored-XSS hardening (Wordfence CWE-79, plan-3f8158): sanitize on save as // defense in depth. wp_kses_post mirrors mxchat_sanitize() (the options.php // save path) so both save routes treat intro_message identically and strip //