PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.8
MxChat – AI Chatbot & Content Generation for WordPress v3.2.8
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / admin / class-ajax-handler.php

class-ajax-handler.php in MxChat – AI Chatbot & Content Generation for WordPress 3.2.8, at admin/class-ajax-handler.php

1,484 lines 64.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * File: admin/class-ajax-handler.php
4 *
5 * Handles all AJAX requests for MxChat admin functionality
6 */
7
8 if (!defined('ABSPATH')) {
9 exit; // Exit if accessed directly
10 }
11
12 class MxChat_Ajax_Handler {
13
14 private $pinecone_manager = null;
15
16 /**
17 * Constructor - Register all AJAX hooks
18 */
19 public function __construct() {
20 $this->mxchat_init_ajax_hooks();
21 }
22
23
24 /**
25 * Register all AJAX action hooks
26 */
27 private function mxchat_init_ajax_hooks() {
28 // Settings AJAX
29 add_action('wp_ajax_mxchat_save_setting', array($this, 'mxchat_save_setting_callback'));
30 add_action('wp_ajax_mxchat_save_prompts_setting', array($this, 'mxchat_save_prompts_setting_callback'));
31 add_action('wp_ajax_migrate_pinecone_settings', array($this, 'ajax_migrate_pinecone_settings'));
32
33 // License AJAX
34 add_action('wp_ajax_mxchat_handle_activate_license', array($this, 'mxchat_handle_activate_license'));
35 add_action('wp_ajax_mxchat_check_license_status', array($this, 'mxchat_check_license_status'));
36 add_action('wp_ajax_mxchat_deactivate_license', array($this, 'mxchat_deactivate_license'));
37
38 // Actions & Intents AJAX
39 add_action('wp_ajax_mxchat_toggle_action', array($this, 'mxchat_toggle_action'));
40 add_action('wp_ajax_mxchat_update_intent_threshold', array($this, 'mxchat_update_intent_threshold'));
41
42 add_action('wp_ajax_mxchat_save_selected_bot', array($this, 'mxchat_save_selected_bot'));
43 add_action('wp_ajax_mxchat_check_api_keys', array($this, 'mxchat_check_api_keys'));
44
45 // Debug & Optimization AJAX
46 add_action('wp_ajax_mxchat_toggle_debug_mode', array($this, 'mxchat_toggle_debug_mode_callback'));
47 add_action('wp_ajax_mxchat_get_debug_log', array($this, 'mxchat_get_debug_log_callback'));
48 add_action('wp_ajax_mxchat_clear_debug_log', array($this, 'mxchat_clear_debug_log_callback'));
49 add_action('wp_ajax_mxchat_export_settings', array($this, 'mxchat_export_settings_callback'));
50 add_action('wp_ajax_mxchat_reset_all_settings', array($this, 'mxchat_reset_all_settings_callback'));
51
52 // Global rate-limit usage counter reset (admin-only, nonce-guarded)
53 add_action('wp_ajax_mxchat_reset_global_rate_limit', array($this, 'mxchat_reset_global_rate_limit_callback'));
54
55 // Custom (OpenAI-compatible) Provider connection test
56 add_action('wp_ajax_mxchat_test_custom_provider', array($this, 'mxchat_test_custom_provider_callback'));
57 }
58
59 /**
60 * Test connection to a Custom (OpenAI-compatible) provider by hitting its /models endpoint
61 * with whichever auth scheme the user configured. Reports model count or a clean error.
62 */
63 public function mxchat_test_custom_provider_callback() {
64 check_ajax_referer('mxchat_test_custom_provider');
65 if (!current_user_can('manage_options')) {
66 wp_send_json_error(array('message' => esc_html__('Unauthorized', 'mxchat')));
67 }
68
69 $options = get_option('mxchat_options', array());
70 $base_url = isset($options['custom_provider_base_url']) ? trim((string) $options['custom_provider_base_url']) : '';
71 $api_key = isset($options['custom_provider_api_key']) ? trim((string) $options['custom_provider_api_key']) : '';
72 $auth = isset($options['custom_provider_auth_scheme']) ? $options['custom_provider_auth_scheme'] : 'bearer';
73 $api_version = isset($options['custom_provider_api_version']) ? trim((string) $options['custom_provider_api_version']) : '';
74
75 if (empty($base_url)) {
76 wp_send_json_error(array('message' => esc_html__('Base URL is empty. Save it first.', 'mxchat')));
77 }
78
79 $url = rtrim($base_url, '/') . '/models';
80 if (!empty($api_version)) {
81 $url = add_query_arg('api-version', $api_version, $url);
82 }
83
84 $headers = array('Content-Type' => 'application/json');
85 if (!empty($api_key)) {
86 if ($auth === 'api-key') {
87 $headers['api-key'] = $api_key;
88 } else {
89 $headers['Authorization'] = 'Bearer ' . $api_key;
90 }
91 }
92
93 $response = wp_remote_get($url, array(
94 'headers' => $headers,
95 'timeout' => 10,
96 ));
97
98 if (is_wp_error($response)) {
99 wp_send_json_error(array('message' => sprintf(esc_html__('Network error: %s', 'mxchat'), esc_html($response->get_error_message()))));
100 }
101
102 $code = (int) wp_remote_retrieve_response_code($response);
103 if ($code === 401 || $code === 403) {
104 wp_send_json_error(array('message' => sprintf(esc_html__('Auth rejected (HTTP %d). Check API key and auth scheme.', 'mxchat'), $code)));
105 }
106 if ($code === 404) {
107 wp_send_json_error(array('message' => esc_html__('Endpoint not found (HTTP 404). Check the Base URL.', 'mxchat')));
108 }
109 if ($code < 200 || $code >= 300) {
110 wp_send_json_error(array('message' => sprintf(esc_html__('Upstream returned HTTP %d.', 'mxchat'), $code)));
111 }
112
113 $body = json_decode(wp_remote_retrieve_body($response), true);
114 $count = 0;
115 if (is_array($body)) {
116 if (isset($body['data']) && is_array($body['data'])) {
117 $count = count($body['data']);
118 } elseif (isset($body['models']) && is_array($body['models'])) {
119 $count = count($body['models']);
120 }
121 }
122
123 wp_send_json_success(array(
124 'message' => sprintf(esc_html__('Connection OK — %d model(s) reported.', 'mxchat'), $count),
125 'count' => $count,
126 ));
127 }
128
129 // ========================================
130 // SETTINGS AJAX HANDLERS
131 // ========================================
132
133 /**
134 * Validates and saves chat settings via AJAX request
135 */
136 public function mxchat_save_setting_callback() {
137 check_ajax_referer('mxchat_save_setting_nonce');
138 if (!current_user_can('manage_options')) {
139 ('MXChat Save: Unauthorized access attempt');
140 wp_send_json_error(['message' => esc_html__('Unauthorized', 'mxchat')]);
141 }
142
143 $name = isset($_POST['name']) ? $_POST['name'] : '';
144 // Strip slashes from the value before saving
145 $value = isset($_POST['value']) ? stripslashes($_POST['value']) : '';
146
147 //error_log('MXChat Save: Processing field name: ' . $name);
148 //error_log('MXChat Save: Field value: ' . $value);
149
150 if (empty($name)) {
151 //error_log('MXChat Save: Empty field name detected');
152 wp_send_json_error(['message' => esc_html__('Invalid field name', 'mxchat')]);
153 }
154
155 // Load the full options array
156 $options = get_option('mxchat_options', []);
157 //error_log('MXChat Save: Current options array: ' . print_r($options, true));
158
159 // Extract field name from mxchat_options[field_name] format if present
160 // But preserve the full name for special cases like rate_limits that need the full path
161 $field_name = $name;
162 if (preg_match('/^mxchat_options\[([^\[\]]+)\]$/', $name, $matches)) {
163 $field_name = $matches[1];
164 }
165
166 // Handle special cases
167 switch ($field_name) {
168 case 'model':
169 //error_log('MXChat Save: Processing model selection');
170 //error_log('MXChat Save: Model value received: ' . $value);
171 //error_log('MXChat Save: Value type: ' . gettype($value));
172 //error_log('MXChat Save: Value length: ' . strlen($value));
173 //error_log('MXChat Save: Value === "openrouter": ' . ($value === 'openrouter' ? 'YES' : 'NO'));
174
175 // Allow 'openrouter' or validate against whitelist
176 if ($value === 'openrouter') {
177 //error_log('MXChat Save: Setting model to openrouter');
178 $options['model'] = 'openrouter';
179 } else {
180 //error_log('MXChat Save: Checking against whitelist');
181 // Catalog refactor (plan-d14e89): canonical allowlist lives in
182 // includes/class-mxchat-model-catalog.php. A new chat model
183 // added there is automatically accepted by autosave.
184 if (!class_exists('MxChat_Model_Catalog')) {
185 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-mxchat-model-catalog.php';
186 }
187 $allowed_models = MxChat_Model_Catalog::chat_model_ids();
188
189 //error_log('MXChat Save: in_array result: ' . (in_array($value, $allowed_models) ? 'YES' : 'NO'));
190
191 if (in_array($value, $allowed_models)) {
192 //error_log('MXChat Save: Model is in whitelist, saving');
193 $options['model'] = sanitize_text_field($value);
194 } else {
195 //error_log('MXChat Save: Invalid model rejected: ' . $value);
196 //error_log('MXChat Save: Allowed models: ' . print_r($allowed_models, true));
197 wp_send_json_error(['message' => esc_html__('Invalid model selected', 'mxchat')]);
198 return;
199 }
200 }
201 break;
202
203 case 'openrouter_selected_model':
204 //error_log('MXChat Save: Processing OpenRouter model: ' . $value);
205 $options['openrouter_selected_model'] = sanitize_text_field($value);
206 // Force immediate save for new keys
207 //error_log('MXChat Save: OpenRouter model saved immediately');
208 break;
209
210 case 'openrouter_selected_model_name':
211 //error_log('MXChat Save: Processing OpenRouter model name: ' . $value);
212 $options['openrouter_selected_model_name'] = sanitize_text_field($value);
213 // Force immediate save for new keys
214 //error_log('MXChat Save: OpenRouter model name saved immediately');
215 break;
216
217 case 'openrouter_api_key':
218 //error_log('MXChat Save: Processing OpenRouter API key');
219 $options['openrouter_api_key'] = sanitize_text_field($value);
220 break;
221
222 // REMOVED DUPLICATE case 'openrouter_selected_model_name' HERE!
223
224 case 'additional_popular_questions':
225 //error_log('MXChat Save: Processing additional_popular_questions');
226 $questions = json_decode($value, true); // No need for stripslashes here
227 if (is_array($questions)) {
228 $options[$field_name] = $questions;
229 // Also update old option for backwards compatibility
230 update_option('additional_popular_questions', $questions);
231 //error_log('MXChat Save: Saved ' . count($questions) . ' additional questions');
232 } else {
233 //error_log('MXChat Save: Failed to decode questions JSON');
234 }
235 break;
236 case 'email_blocker_header_content':
237 //error_log('MXChat Save: Processing email_blocker_header_content');
238 // Allow HTML content but sanitize it safely
239 $options[$field_name] = wp_kses_post($value);
240 break;
241 case 'email_blocker_button_text':
242 //error_log('MXChat Save: Processing email_blocker_button_text');
243 $options[$field_name] = sanitize_text_field($value);
244 break;
245 case 'name_field_placeholder':
246 //error_log('MXChat Save: Processing name_field_placeholder');
247 $options[$field_name] = sanitize_text_field($value);
248 break;
249 case 'similarity_threshold':
250 //error_log('MXChat Save: Processing similarity_threshold');
251 // Validate and save - enforce min 20, max 85
252 $threshold = intval($value);
253 if ($threshold < 20) $threshold = 20;
254 if ($threshold > 85) $threshold = 85;
255 $options[$field_name] = $threshold;
256 break;
257 case 'rag_sources_limit':
258 //error_log('MXChat Save: Processing rag_sources_limit');
259 // Validate and save - enforce min 3, max 10, default 6
260 $rag_limit = intval($value);
261 if ($rag_limit < 3) $rag_limit = 3;
262 if ($rag_limit > 10) $rag_limit = 10;
263 $options[$field_name] = $rag_limit;
264 break;
265 case 'rag_chunks_limit':
266 // Validate and save - enforce min 8, max 20, default 15
267 $chunks_limit = intval($value);
268 if ($chunks_limit < 8) $chunks_limit = 8;
269 if ($chunks_limit > 20) $chunks_limit = 20;
270 $options[$field_name] = $chunks_limit;
271 break;
272 case 'live_agent_status':
273 //error_log('MXChat Save: Processing live_agent_status');
274 // Set the new value
275 $options[$field_name] = ($value === 'on') ? 'on' : 'off';
276 break;
277 case 'enable_web_search':
278 //error_log('MXChat Save: Processing enable_web_search');
279 $options[$field_name] = ($value === 'on') ? 'on' : 'off';
280 break;
281 case 'enable_woocommerce_integration':
282 //error_log('MXChat Save: Processing enable_woocommerce_integration');
283 // Handle values that used to be 1/0
284 $options[$field_name] = ($value === 'on' || $value === '1') ? 'on' : 'off';
285 break;
286 case 'post_type_visibility_mode':
287 // Validate mode value
288 $allowed_modes = array('all', 'include', 'exclude');
289 $options[$field_name] = in_array($value, $allowed_modes) ? $value : 'all';
290 break;
291 case 'post_type_visibility_list':
292 // Handle JSON array of post types
293 $post_types = json_decode($value, true);
294 if (is_array($post_types)) {
295 // Sanitize each post type slug
296 $options[$field_name] = array_map('sanitize_key', $post_types);
297 } else {
298 $options[$field_name] = array();
299 }
300 break;
301 case 'script_loading_strategy':
302 // Validate script loading strategy value
303 $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
304 $options[$field_name] = in_array($value, $allowed_strategies) ? $value : 'default';
305 break;
306 case 'auto_retry_on_transient_error':
307 // Boolean toggle — accept 1/0/on/off, default to '1' if any truthy value.
308 $options[$field_name] = ($value === '1' || $value === 'on' || $value === 1 || $value === true) ? '1' : '0';
309 break;
310 default:
311 // Handle transcripts options
312 if (strpos($name, 'mxchat_transcripts_options') !== false) {
313 // Extract field name from mxchat_transcripts_options[field_name]
314 if (preg_match('/mxchat_transcripts_options\[([^\]]+)\]/', $name, $matches)) {
315 $field_name = $matches[1];
316
317 // Get current transcripts options
318 $transcripts_options = get_option('mxchat_transcripts_options', array());
319
320 // Ensure it's an array
321 if (!is_array($transcripts_options)) {
322 $transcripts_options = array();
323 }
324
325 // Handle checkbox values (convert 'on'/'off' to 1/0)
326 if ($value === 'on' || $value === '1') {
327 $transcripts_options[$field_name] = 1;
328 } else if ($value === 'off' || $value === '0' || $value === '') {
329 $transcripts_options[$field_name] = 0;
330 } else {
331 // For text/select fields, sanitize appropriately
332 if ($field_name === 'mxchat_notification_email') {
333 $transcripts_options[$field_name] = sanitize_email($value);
334 } else {
335 $transcripts_options[$field_name] = sanitize_text_field($value);
336 }
337 }
338
339 // Use direct database update to bypass any filters
340 global $wpdb;
341
342 // Serialize the options array
343 $serialized = maybe_serialize($transcripts_options);
344
345 // Check if the option already exists in the database
346 $existing = $wpdb->get_var("SELECT option_id FROM {$wpdb->options} WHERE option_name = 'mxchat_transcripts_options'");
347
348 if ($existing) {
349 // Option exists, do an update
350 $result = $wpdb->update(
351 $wpdb->options,
352 array('option_value' => $serialized),
353 array('option_name' => 'mxchat_transcripts_options'),
354 array('%s'),
355 array('%s')
356 );
357 } else {
358 // Option doesn't exist (new install), do an insert
359 $result = $wpdb->insert(
360 $wpdb->options,
361 array(
362 'option_name' => 'mxchat_transcripts_options',
363 'option_value' => $serialized,
364 'autoload' => 'yes'
365 ),
366 array('%s', '%s', '%s')
367 );
368 }
369
370 // Clear all caches after direct DB update
371 wp_cache_delete('mxchat_transcripts_options', 'options');
372 wp_cache_delete('alloptions', 'options');
373 wp_cache_flush();
374
375 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
376 return;
377 }
378 }
379 // Whole-chatbot global cap (sits in mxchat_options['rate_limits_global']).
380 // Field names: mxchat_options[rate_limits_global][limit|timeframe|limit_custom]
381 else if (strpos($name, 'mxchat_options[rate_limits_global]') !== false) {
382 preg_match('/\[rate_limits_global\]\[(.*?)\]/', $name, $matches);
383 if (isset($matches[1])) {
384 $setting_key = $matches[1];
385 if (!isset($options['rate_limits_global']) || !is_array($options['rate_limits_global'])) {
386 $options['rate_limits_global'] = array('limit' => 'unlimited', 'timeframe' => 'daily');
387 }
388 if ($setting_key === 'limit') {
389 // Selection from the preset dropdown. If __custom__, resolve from limit_custom; otherwise store directly.
390 if ($value === '__custom__') {
391 $custom = isset($options['rate_limits_global']['limit_custom']) ? (string) $options['rate_limits_global']['limit_custom'] : '';
392 if ($custom !== '' && ctype_digit($custom) && (int) $custom >= 1) {
393 $options['rate_limits_global']['limit'] = $custom;
394 }
395 // else leave existing limit untouched until the custom value arrives
396 } else {
397 $options['rate_limits_global']['limit'] = $value;
398 }
399 } elseif ($setting_key === 'limit_custom') {
400 $clean = preg_replace('/[^0-9]/', '', (string) $value);
401 $options['rate_limits_global']['limit_custom'] = $clean;
402 // Mirror a valid custom value into limit UNCONDITIONALLY (plan-74eb86).
403 // The custom number input is only editable when the dropdown is on
404 // "Custom…" (the toggle JS hides it for presets/unlimited) and autosave
405 // sends one field per change event, so a limit_custom change only fires
406 // in custom mode — there is no preset to clobber. The old guard required
407 // limit to already be non-preset, which it isn't on a first-time custom
408 // entry (the limit=__custom__ event arrives before limit_custom is set),
409 // so the value never landed in limit on the first save and reverted on refresh.
410 if ($clean !== '' && (int) $clean >= 1) {
411 $options['rate_limits_global']['limit'] = $clean;
412 }
413 } elseif ($setting_key === 'timeframe') {
414 $allowed_tf = array('hourly','daily','weekly','monthly');
415 $options['rate_limits_global']['timeframe'] = in_array($value, $allowed_tf, true) ? $value : 'daily';
416 }
417 }
418 }
419 // First check for rate limits settings
420 else if (strpos($name, 'mxchat_options[rate_limits]') !== false) {
421 //error_log('MXChat Save: Detected rate_limits field: ' . $name);
422
423 // Extract role ID and setting from the name
424 preg_match('/\[rate_limits\]\[(.*?)\]\[(.*?)\]/', $name, $matches);
425 //error_log('MXChat Save: Regex matches: ' . print_r($matches, true));
426
427 if (isset($matches[1]) && isset($matches[2])) {
428 $role_id = $matches[1];
429 $setting_key = $matches[2]; // limit, timeframe, message, or limit_custom
430
431 //error_log('MXChat Save: Role ID = ' . $role_id . ', Setting Key = ' . $setting_key);
432
433 // Initialize rate_limits if it doesn't exist
434 if (!isset($options['rate_limits'])) {
435 // //error_log('MXChat Save: Initializing rate_limits array');
436 $options['rate_limits'] = [];
437 }
438
439 // Initialize role settings if it doesn't exist
440 if (!isset($options['rate_limits'][$role_id])) {
441 //error_log('MXChat Save: Initializing rate_limits for role: ' . $role_id);
442 $options['rate_limits'][$role_id] = [
443 'limit' => ($role_id === 'logged_out') ? '10' : '100',
444 'timeframe' => 'daily',
445 'message' => 'Rate limit exceeded. Please try again later.'
446 ];
447 }
448
449 if ($setting_key === 'limit') {
450 if ($value === '__custom__') {
451 // Pull the integer from limit_custom that may have arrived (or will arrive).
452 $custom = isset($options['rate_limits'][$role_id]['limit_custom']) ? (string) $options['rate_limits'][$role_id]['limit_custom'] : '';
453 if ($custom !== '' && ctype_digit($custom) && (int) $custom >= 1) {
454 $options['rate_limits'][$role_id]['limit'] = $custom;
455 }
456 } else {
457 $options['rate_limits'][$role_id]['limit'] = $value;
458 }
459 } elseif ($setting_key === 'limit_custom') {
460 $clean = preg_replace('/[^0-9]/', '', (string) $value);
461 $options['rate_limits'][$role_id]['limit_custom'] = $clean;
462 // Mirror a valid custom value into limit UNCONDITIONALLY — same reasoning
463 // as the global branch above (plan-74eb86). The per-role custom input is
464 // only editable in custom mode and autosave is one-field-per-change, so
465 // this never clobbers a preset; it fixes the first-time-save revert.
466 if ($clean !== '' && (int) $clean >= 1) {
467 $options['rate_limits'][$role_id]['limit'] = $clean;
468 }
469 } else {
470 // Update the specific setting (timeframe, message)
471 $options['rate_limits'][$role_id][$setting_key] = $value;
472 }
473 //error_log('MXChat Save: Updated rate_limits[' . $role_id . '][' . $setting_key . '] = ' . $value);
474 } else {
475 //error_log('MXChat Save: Failed to parse rate_limits pattern: ' . $name);
476 }
477 }
478 // Then check for role rate limits (old format)
479 else if (strpos($name, 'mxchat_options[role_rate_limits]') !== false) {
480 //error_log('MXChat Save: Processing role_rate_limits field: ' . $name);
481 // Extract role ID from the name
482 preg_match('/\[role_rate_limits\]\[(.*?)\]/', $name, $matches);
483 //error_log('MXChat Save: Regex matches: ' . print_r($matches, true));
484
485 if (isset($matches[1])) {
486 $role_id = $matches[1];
487 // Initialize role_rate_limits if it doesn't exist
488 if (!isset($options['role_rate_limits'])) {
489 //error_log('MXChat Save: Initializing role_rate_limits array');
490 $options['role_rate_limits'] = [];
491 }
492 // Update the specific role's rate limit
493 $options['role_rate_limits'][$role_id] = sanitize_text_field($value);
494 //error_log('MXChat Save: Updated role_rate_limits[' . $role_id . '] = ' . $value);
495 } else {
496 //error_log('MXChat Save: Failed to parse role_rate_limits pattern: ' . $name);
497 }
498 }
499 // Handle toggles - check both extracted field_name and original name for toggle detection
500 else if (strpos($field_name, 'toggle') !== false || in_array($field_name, [
501 'chat_persistence_toggle',
502 'privacy_toggle',
503 'complianz_toggle',
504 'chat_toolbar_toggle',
505 'show_pdf_upload_button',
506 'show_word_upload_button',
507 'enable_streaming_toggle',
508 'contextual_awareness_toggle',
509 'citation_links_toggle',
510 'enable_email_block',
511 'enable_name_field',
512 'custom_provider_for_embeddings',
513 'custom_provider_for_images'
514 ])) {
515 //error_log('MXChat Save: Processing toggle: ' . $field_name);
516 $options[$field_name] = ($value === 'on') ? 'on' : 'off';
517 } else {
518 //error_log('MXChat Save: Processing standard field: ' . $field_name);
519 // Store all other values directly using the extracted field name
520 $options[$field_name] = $value;
521 }
522 break;
523 }
524
525 // Save all updates to the options array
526 $updated = update_option('mxchat_options', $options);
527 //error_log('MXChat Save: Update result: ' . ($updated ? 'success' : 'unchanged') . ' for field: ' . $name);
528 //error_log('MXChat Save: Updated options array: ' . print_r($options, true));
529
530 // Log the save action if debug mode is enabled
531 if ( class_exists( 'MxChat_Admin' ) ) {
532 MxChat_Admin::mxchat_log_debug(
533 'settings_save',
534 sprintf( 'Field saved: %s', $field_name ),
535 array(
536 'field' => $field_name,
537 'updated' => $updated,
538 )
539 );
540 }
541
542 // Always return success even if WordPress says nothing changed
543 // (which happens when the value is the same as before)
544 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
545 }
546
547 /**
548 * Save the selected bot for knowledge base operations
549 */
550 public function mxchat_save_selected_bot() {
551 // Check nonce
552 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mxchat_save_setting_nonce')) {
553 wp_send_json_error('Invalid nonce');
554 }
555
556 // Check permissions
557 if (!current_user_can('manage_options')) {
558 wp_send_json_error('Unauthorized');
559 }
560
561 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
562
563 // Save as user meta for the current user
564 $user_id = get_current_user_id();
565 update_user_meta($user_id, 'mxchat_selected_knowledge_bot', $bot_id);
566
567 // Also save as an option for site-wide default
568 update_option('mxchat_current_knowledge_bot', $bot_id);
569
570 // No cache clearing needed since we removed caching
571
572 wp_send_json_success(array(
573 'message' => 'Bot selection saved',
574 'bot_id' => $bot_id
575 ));
576 }
577
578 /**
579 * Handles AJAX request for saving chat settings
580 */
581 public function mxchat_save_prompts_setting_callback() {
582 check_ajax_referer('mxchat_prompts_setting_nonce');
583
584 if (!current_user_can('manage_options')) {
585 wp_send_json_error(['message' => esc_html__('Unauthorized', 'mxchat')]);
586 }
587
588 $name = isset($_POST['name']) ? $_POST['name'] : '';
589 $value = isset($_POST['value']) ? stripslashes($_POST['value']) : '';
590
591 //error_log('[MXCHAT-PROMPTS] Saving setting: ' . $name . ' = ' . $value);
592
593 if (empty($name)) {
594 wp_send_json_error(['message' => esc_html__('Invalid field name', 'mxchat')]);
595 }
596
597 // Handle Pinecone settings - BYPASS WORDPRESS SANITIZATION
598 if (strpos($name, 'mxchat_pinecone_addon_options') !== false) {
599 //error_log('[MXCHAT-PROMPTS] Processing Pinecone setting: ' . $name);
600
601 // Extract the field name
602 if (preg_match('/mxchat_pinecone_addon_options\[([^\]]+)\]/', $name, $matches)) {
603 $field_name = $matches[1];
604 //error_log('[MXCHAT-PROMPTS] Extracted field name: ' . $field_name);
605
606 // Get current options directly from database - NO WordPress filters
607 global $wpdb;
608 $current_options_raw = $wpdb->get_var(
609 $wpdb->prepare(
610 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
611 'mxchat_pinecone_addon_options'
612 )
613 );
614
615 // FIX: Handle the case where the option doesn't exist yet
616 if ($current_options_raw === null) {
617 // Option doesn't exist, create it with default values
618 $current_options = array(
619 'mxchat_use_pinecone' => '0',
620 'mxchat_pinecone_api_key' => '',
621 'mxchat_pinecone_host' => '',
622 'mxchat_pinecone_index' => '',
623 'mxchat_pinecone_environment' => ''
624 );
625 //error_log('[MXCHAT-PROMPTS] Option does not exist, creating with defaults');
626 } else {
627 // Unserialize the raw data
628 $current_options = maybe_unserialize($current_options_raw);
629 if (!is_array($current_options)) {
630 // Fallback to defaults if unserialization fails
631 $current_options = array(
632 'mxchat_use_pinecone' => '0',
633 'mxchat_pinecone_api_key' => '',
634 'mxchat_pinecone_host' => '',
635 'mxchat_pinecone_index' => '',
636 'mxchat_pinecone_environment' => ''
637 );
638 //error_log('[MXCHAT-PROMPTS] Failed to unserialize, using defaults');
639 }
640 }
641
642 //error_log('[MXCHAT-PROMPTS] Current options from DB: ' . print_r($current_options, true));
643
644 // Update the specific field with proper sanitization
645 switch ($field_name) {
646 case 'mxchat_use_pinecone':
647 $new_value = ($value === '1') ? '1' : '0';
648 break;
649 case 'mxchat_pinecone_api_key':
650 case 'mxchat_pinecone_host':
651 case 'mxchat_pinecone_index':
652 case 'mxchat_pinecone_environment':
653 $new_value = sanitize_text_field($value);
654 if ($field_name === 'mxchat_pinecone_host') {
655 $new_value = str_replace(['https://', 'http://'], '', $new_value);
656 }
657 break;
658 default:
659 wp_send_json_error(['message' => esc_html__('Unknown Pinecone field', 'mxchat')]);
660 }
661
662 $current_options[$field_name] = $new_value;
663 //error_log('[MXCHAT-PROMPTS] New value for ' . $field_name . ': "' . $new_value . '"');
664 //error_log('[MXCHAT-PROMPTS] Updated options: ' . print_r($current_options, true));
665
666 // Save directly to database to bypass WordPress sanitization
667 $serialized_options = maybe_serialize($current_options);
668
669 // FIX: Use INSERT ... ON DUPLICATE KEY UPDATE or separate INSERT/UPDATE logic
670 $option_exists = $wpdb->get_var(
671 $wpdb->prepare(
672 "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s",
673 'mxchat_pinecone_addon_options'
674 )
675 );
676
677 if ($option_exists > 0) {
678 // Update existing option
679 $save_result = $wpdb->update(
680 $wpdb->options,
681 array('option_value' => $serialized_options),
682 array('option_name' => 'mxchat_pinecone_addon_options'),
683 array('%s'),
684 array('%s')
685 );
686 //error_log('[MXCHAT-PROMPTS] Updated existing option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED'));
687 } else {
688 // Insert new option
689 $save_result = $wpdb->insert(
690 $wpdb->options,
691 array(
692 'option_name' => 'mxchat_pinecone_addon_options',
693 'option_value' => $serialized_options,
694 'autoload' => 'yes'
695 ),
696 array('%s', '%s', '%s')
697 );
698 //error_log('[MXCHAT-PROMPTS] Inserted new option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED'));
699 }
700
701 // Clear any WordPress option cache to ensure get_option() returns fresh data
702 wp_cache_delete('mxchat_pinecone_addon_options', 'options');
703
704 // IMPROVED VERIFICATION - Check if the database operation succeeded
705 if ($save_result !== false) {
706 // Double-check by reading fresh from database
707 $verification_raw = $wpdb->get_var(
708 $wpdb->prepare(
709 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
710 'mxchat_pinecone_addon_options'
711 )
712 );
713 $verification_options = maybe_unserialize($verification_raw);
714 $verified_value = isset($verification_options[$field_name]) ? $verification_options[$field_name] : 'NOT_FOUND';
715
716 //error_log('[MXCHAT-PROMPTS] Final verification - Expected: "' . $new_value . '", Got: "' . $verified_value . '"');
717
718 // Use loose comparison (==) instead of strict (===) to avoid type issues
719 if ($verified_value == $new_value || $save_result > 0) {
720 wp_send_json_success(['message' => esc_html__('Pinecone setting saved', 'mxchat')]);
721 } else {
722 // Still return success if the DB operation worked, even if verification is quirky
723 //error_log('[MXCHAT-PROMPTS] Verification mismatch but DB operation succeeded');
724 wp_send_json_success(['message' => esc_html__('Pinecone setting saved (DB success)', 'mxchat')]);
725 }
726 } else {
727 wp_send_json_error(['message' => esc_html__('Database save failed', 'mxchat')]);
728 }
729 } else {
730 wp_send_json_error(['message' => esc_html__('Invalid field name format', 'mxchat')]);
731 }
732
733 return; // Exit here for Pinecone settings
734 }
735 // Handle auto-sync settings (existing functionality)
736 if (strpos($name, 'mxchat_auto_sync_') === 0) {
737 $value = ($value === 'on' || $value === '1') ? '1' : '0';
738 $updated = update_option($name, $value);
739
740 if ($updated || get_option($name) === $value) {
741 wp_send_json_success(['message' => esc_html__('Auto-sync setting saved', 'mxchat')]);
742 } else {
743 wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]);
744 }
745 }
746
747 // Handle chunking settings - use direct DB access to bypass WordPress filters
748 if (strpos($name, 'mxchat_chunk') === 0 || $name === 'mxchat_chunking_enabled') {
749 global $wpdb;
750
751 // Get current options directly from database
752 $current_options_raw = $wpdb->get_var(
753 $wpdb->prepare(
754 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
755 'mxchat_options'
756 )
757 );
758
759 $options = $current_options_raw !== null ? maybe_unserialize($current_options_raw) : array();
760 if (!is_array($options)) {
761 $options = array();
762 }
763
764 // Update the specific chunking field
765 if ($name === 'mxchat_chunking_enabled') {
766 $options['chunking_enabled'] = in_array($value, array('on', '1', 'true', true), true);
767 } elseif ($name === 'mxchat_chunk_size') {
768 $options['chunk_size'] = max(1000, min(10000, intval($value)));
769 }
770
771 // Save directly to database
772 $serialized_options = maybe_serialize($options);
773
774 $option_exists = $wpdb->get_var(
775 $wpdb->prepare(
776 "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s",
777 'mxchat_options'
778 )
779 );
780
781 if ($option_exists > 0) {
782 $save_result = $wpdb->update(
783 $wpdb->options,
784 array('option_value' => $serialized_options),
785 array('option_name' => 'mxchat_options'),
786 array('%s'),
787 array('%s')
788 );
789 } else {
790 $save_result = $wpdb->insert(
791 $wpdb->options,
792 array(
793 'option_name' => 'mxchat_options',
794 'option_value' => $serialized_options,
795 'autoload' => 'yes'
796 ),
797 array('%s', '%s', '%s')
798 );
799 }
800
801 // Clear object cache for this option
802 wp_cache_delete('mxchat_options', 'options');
803
804 if ($save_result !== false) {
805 wp_send_json_success(['message' => esc_html__('Chunking setting saved', 'mxchat')]);
806 } else {
807 wp_send_json_error(['message' => esc_html__('Failed to save chunking setting', 'mxchat')]);
808 }
809 return;
810 }
811
812 // Handle ACF field exclusion toggles
813 if (strpos($name, 'mxchat_acf_field_') === 0) {
814 // Extract field name from the input name (e.g., mxchat_acf_field_private_notes -> private_notes)
815 $field_name = str_replace('mxchat_acf_field_', '', $name);
816 $is_enabled = ($value === 'on' || $value === '1');
817
818 // Get current excluded fields
819 $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
820 if (!is_array($excluded_fields)) {
821 $excluded_fields = array();
822 }
823
824 if ($is_enabled) {
825 // Remove from exclusion list (field should be included)
826 $excluded_fields = array_values(array_diff($excluded_fields, array($field_name)));
827 } else {
828 // Add to exclusion list (field should be excluded)
829 if (!in_array($field_name, $excluded_fields)) {
830 $excluded_fields[] = $field_name;
831 }
832 }
833
834 $updated = update_option('mxchat_acf_excluded_fields', $excluded_fields);
835
836 if ($updated || true) { // Always report success since the state may already be correct
837 wp_send_json_success([
838 'message' => $is_enabled
839 ? sprintf(esc_html__('Field "%s" will be included in imports', 'mxchat'), $field_name)
840 : sprintf(esc_html__('Field "%s" will be excluded from imports', 'mxchat'), $field_name)
841 ]);
842 } else {
843 wp_send_json_error(['message' => esc_html__('Failed to save ACF field setting', 'mxchat')]);
844 }
845 return;
846 }
847
848 // Handle custom post meta whitelist
849 if ($name === 'mxchat_custom_meta_whitelist') {
850 $updated = update_option('mxchat_custom_meta_whitelist', sanitize_textarea_field($value));
851
852 if ($updated || true) { // Always report success since the state may already be correct
853 wp_send_json_success([
854 'message' => esc_html__('Custom meta whitelist saved', 'mxchat')
855 ]);
856 } else {
857 wp_send_json_error(['message' => esc_html__('Failed to save custom meta whitelist', 'mxchat')]);
858 }
859 return;
860 }
861
862 // Handle other prompts options
863 $options = get_option('mxchat_prompts_options', []);
864 $options[$name] = $value;
865 $updated = update_option('mxchat_prompts_options', $options);
866
867 if ($updated) {
868 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
869 } else {
870 wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]);
871 }
872 }
873
874
875 /**
876 * Handles AJAX request for Pinecone settings migration
877 */
878 public function ajax_migrate_pinecone_settings() {
879 // Verify nonce
880 if (!wp_verify_nonce($_POST['_ajax_nonce'] ?? '', 'mxchat_save_setting_nonce')) {
881 wp_send_json_error('Invalid nonce');
882 }
883
884 // Check permissions
885 if (!current_user_can('manage_options')) {
886 wp_send_json_error('Unauthorized access');
887 }
888
889 // Check if old Pinecone addon options exist
890 $old_options = get_option('mxchat_pinecone_addon_options', array());
891
892 if (empty($old_options)) {
893 wp_send_json_success(array('migrated' => false, 'message' => 'No old settings found'));
894 }
895
896 // Get current core plugin options
897 $current_options = get_option('mxchat_pinecone_addon_options', array());
898
899 // Only migrate if core options are empty or if explicitly requested
900 $should_migrate = empty($current_options) ||
901 (empty($current_options['mxchat_pinecone_api_key']) && !empty($old_options['mxchat_pinecone_api_key']));
902
903 if ($should_migrate) {
904 // Migrate settings with proper sanitization
905 $migrated_options = array(
906 'mxchat_use_pinecone' => $old_options['mxchat_use_pinecone'] ?? '0',
907 'mxchat_pinecone_api_key' => sanitize_text_field($old_options['mxchat_pinecone_api_key'] ?? ''),
908 'mxchat_pinecone_host' => sanitize_text_field($old_options['mxchat_pinecone_host'] ?? ''),
909 'mxchat_pinecone_index' => sanitize_text_field($old_options['mxchat_pinecone_index'] ?? ''),
910 'mxchat_pinecone_environment' => sanitize_text_field($old_options['mxchat_pinecone_environment'] ?? '')
911 );
912
913 update_option('mxchat_pinecone_addon_options', $migrated_options);
914
915 wp_send_json_success(array(
916 'migrated' => true,
917 'message' => 'Settings migrated successfully from Pinecone add-on'
918 ));
919 } else {
920 wp_send_json_success(array(
921 'migrated' => false,
922 'message' => 'Settings already exist in core plugin'
923 ));
924 }
925 }
926
927
928 // ========================================
929 // LICENSE AJAX HANDLERS
930 // ========================================
931
932 /**
933 * Validates and activates chat license via AJAX
934 */
935 public function mxchat_handle_activate_license() {
936 // Check nonce
937 if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
938 wp_send_json_error(esc_html__('Invalid security token', 'mxchat'));
939 return;
940 }
941
942 // Verify user capabilities
943 if (!current_user_can('manage_options')) {
944 wp_send_json_error(esc_html__('Unauthorized access', 'mxchat'));
945 return;
946 }
947
948 $license_key = isset($_POST['mxchat_activation_key']) ? sanitize_text_field($_POST['mxchat_activation_key']) : '';
949 $customer_email = isset($_POST['mxchat_pro_email']) ? sanitize_email($_POST['mxchat_pro_email']) : '';
950
951 if (empty($license_key) || empty($customer_email)) {
952 wp_send_json_error(esc_html__('Email or License Key is missing', 'mxchat'));
953 return;
954 }
955
956 $product_id = 'MxChatPRO';
957 $domain = parse_url(home_url(), PHP_URL_HOST); // Get the current domain
958
959 // Call WooCommerce Software API for activation (not just validation)
960 $response = wp_remote_get(
961 add_query_arg(
962 array(
963 'wc-api' => 'software-api',
964 'request' => 'activation',
965 'email' => $customer_email,
966 'license_key' => $license_key,
967 'product_id' => $product_id,
968 'instance' => $domain, // THIS IS KEY - include the domain as instance
969 'platform' => 'wordpress' // Optional but good to include
970 ),
971 'https://mxchat.ai/'
972 ),
973 array(
974 'timeout' => 60,
975 'sslverify' => true
976 )
977 );
978
979 if (is_wp_error($response)) {
980 $error_message = $response->get_error_message();
981 //error_log('MxChat License Activation Error: ' . $error_message);
982 wp_send_json_error(esc_html__('Activation failed due to a server error: ', 'mxchat') . $error_message);
983 return;
984 }
985
986 $response_code = wp_remote_retrieve_response_code($response);
987 $body = wp_remote_retrieve_body($response);
988
989 // Log response for debugging
990 //error_log('MxChat License Response Code: ' . $response_code);
991 //error_log('MxChat License Response Body: ' . $body);
992
993 if ($response_code !== 200) {
994 wp_send_json_error(esc_html__('Server returned error code: ', 'mxchat') . $response_code);
995 return;
996 }
997
998 $data = json_decode($body);
999
1000 if ($data && isset($data->activated) && $data->activated) {
1001 // Success - save local options
1002 update_option('mxchat_license_status', 'active');
1003 update_option('mxchat_pro_email', $customer_email);
1004 update_option('mxchat_activation_key', $license_key);
1005 delete_option('mxchat_license_error');
1006
1007 // Also track on your website (this is your existing domain tracking)
1008 $this->track_domain_on_website($license_key, $customer_email, $domain);
1009
1010 wp_send_json_success(array('message' => esc_html__('License activated successfully', 'mxchat')));
1011 } else {
1012 $error_message = isset($data->error) ? $data->error : esc_html__('Activation failed', 'mxchat');
1013 update_option('mxchat_license_status', 'inactive');
1014 update_option('mxchat_license_error', $error_message);
1015
1016 //error_log('MxChat Activation failed: ' . $error_message);
1017 wp_send_json_error($error_message);
1018 }
1019 }
1020
1021 /**
1022 * Track domain on your website (separate from WooCommerce activation)
1023 */
1024 private function track_domain_on_website($license_key, $email, $domain) {
1025 // This calls your website's tracking API
1026 wp_remote_post('https://mxchat.ai/mxchat-api/activate-license', array(
1027 'body' => array(
1028 'mxchat_pro_email' => $email,
1029 'mxchat_activation_key' => $license_key,
1030 'domain' => $domain
1031 ),
1032 'timeout' => 10,
1033 'sslverify' => true
1034 ));
1035 }
1036
1037 /**
1038 * Validates license via AJAX with email and key
1039 */
1040 public function mxchat_check_license_status() {
1041 // Verify nonce
1042 if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
1043 wp_send_json_error('Security check failed');
1044 return;
1045 }
1046
1047 // Add isset checks for safety
1048 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
1049 $key = isset($_POST['key']) ? sanitize_text_field($_POST['key']) : '';
1050
1051 // Check if this license is actually active in your system
1052 $is_active = (get_option('mxchat_license_status') === 'active' &&
1053 get_option('mxchat_pro_email') === $email &&
1054 get_option('mxchat_activation_key') === $key);
1055
1056 wp_send_json(array(
1057 'is_active' => $is_active
1058 ));
1059 }
1060
1061 /**
1062 * Handle license deactivation - Complete version for plugin
1063 */
1064 function mxchat_deactivate_license() {
1065 // Add debugging
1066 //error_log('MxChat deactivate function called');
1067
1068 // Check nonce
1069 if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
1070 //error_log('MxChat deactivate: Nonce check failed');
1071 wp_send_json_error('Security check failed.');
1072 return;
1073 }
1074
1075 //error_log('MxChat deactivate: Nonce check passed');
1076
1077 $license_key = get_option('mxchat_activation_key');
1078 $email = get_option('mxchat_pro_email');
1079 $domain = parse_url(home_url(), PHP_URL_HOST);
1080
1081 //error_log('MxChat deactivate: License: ' . $license_key . ', Email: ' . $email . ', Domain: ' . $domain);
1082
1083 if (empty($license_key) || empty($email)) {
1084 //error_log('MxChat deactivate: No active license found');
1085 wp_send_json_error('No active license found.');
1086 return;
1087 }
1088
1089 // Clear local license data first
1090 delete_option('mxchat_license_status');
1091 delete_option('mxchat_pro_email');
1092 delete_option('mxchat_activation_key');
1093 delete_option('mxchat_license_error');
1094
1095 //error_log('MxChat deactivate: Local data cleared');
1096
1097 // Notify your website's API to properly deactivate
1098 $response = wp_remote_post('https://mxchat.ai/mxchat-api/deactivate-license', array(
1099 'body' => array(
1100 'license_key' => $license_key,
1101 'email' => $email,
1102 'domain' => $domain
1103 ),
1104 'timeout' => 15,
1105 'sslverify' => true
1106 ));
1107
1108 if (is_wp_error($response)) {
1109 //error_log('MxChat deactivate: Server error - ' . $response->get_error_message());
1110 wp_send_json_success(array(
1111 'message' => 'License deactivated locally. Server could not be contacted to free activation slot.',
1112 'server_notified' => false
1113 ));
1114 return;
1115 }
1116
1117 $response_body = wp_remote_retrieve_body($response);
1118 $response_data = json_decode($response_body, true);
1119
1120 //error_log('MxChat deactivate: Server response - ' . $response_body);
1121
1122 if (isset($response_data['success']) && $response_data['success']) {
1123 //error_log('MxChat deactivate: Success with server notification');
1124 wp_send_json_success(array(
1125 'message' => 'License deactivated successfully. Activation slot has been freed up.',
1126 'server_notified' => true
1127 ));
1128 } else {
1129 //error_log('MxChat deactivate: Server responded but deactivation may have failed');
1130 wp_send_json_success(array(
1131 'message' => 'License deactivated locally. Please check your account dashboard to verify the activation was freed.',
1132 'server_notified' => false
1133 ));
1134 }
1135 }
1136
1137
1138 // ========================================
1139 // ACTIONS & INTENTS AJAX HANDLERS
1140 // ========================================
1141
1142 /**
1143 * Validates nonce and returns JSON error on failure
1144 */
1145 public function mxchat_toggle_action() {
1146 // Check nonce
1147 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_actions_nonce')) {
1148 wp_send_json_error(array('message' => 'Security check failed'));
1149 return;
1150 }
1151
1152 // Check permissions
1153 if (!current_user_can('manage_options')) {
1154 wp_send_json_error(array('message' => 'Permission denied'));
1155 return;
1156 }
1157
1158 // Validate params
1159 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
1160 $enabled = isset($_POST['enabled']) ? (bool)$_POST['enabled'] : false;
1161
1162 if (!$intent_id) {
1163 wp_send_json_error(array('message' => 'Invalid action ID'));
1164 return;
1165 }
1166
1167 // Update the intent/action status in the database
1168 global $wpdb;
1169 $table_name = $wpdb->prefix . 'mxchat_intents';
1170
1171 // Using the 'enabled' field - add this field if it doesn't exist
1172 $result = $wpdb->update(
1173 $table_name,
1174 array('enabled' => $enabled ? 1 : 0),
1175 array('id' => $intent_id),
1176 array('%d'),
1177 array('%d')
1178 );
1179
1180 if ($result === false) {
1181 wp_send_json_error(array('message' => 'Database error'));
1182 return;
1183 }
1184
1185 wp_send_json_success();
1186 }
1187
1188
1189 /**
1190 * Validates permissions for AJAX request handling
1191 */
1192 public function mxchat_update_intent_threshold() {
1193 // Check permissions
1194 if (!current_user_can('manage_options')) {
1195 if (wp_doing_ajax()) {
1196 wp_send_json_error(array('message' => 'Unauthorized user'));
1197 return;
1198 }
1199 wp_die(esc_html__('Unauthorized user', 'mxchat'));
1200 }
1201
1202 // Verify nonce
1203 check_admin_referer('mxchat_update_intent_threshold_nonce');
1204
1205 // Process the update if we have valid data
1206 if (isset($_POST['intent_id'], $_POST['intent_threshold'])) {
1207 global $wpdb;
1208 $table_name = $wpdb->prefix . 'mxchat_intents';
1209 $intent_id = intval($_POST['intent_id']);
1210 $threshold_percentage = max(70, min(95, intval($_POST['intent_threshold'])));
1211 $similarity_threshold = $threshold_percentage / 100;
1212
1213 $result = $wpdb->update(
1214 $table_name,
1215 ['similarity_threshold' => $similarity_threshold],
1216 ['id' => $intent_id],
1217 ['%f'],
1218 ['%d']
1219 );
1220
1221 // Handle AJAX requests
1222 if (wp_doing_ajax()) {
1223 if ($result === false) {
1224 wp_send_json_error(array('message' => 'Failed to update threshold'));
1225 } else {
1226 wp_send_json_success(array('threshold' => $threshold_percentage));
1227 }
1228 return;
1229 }
1230 }
1231
1232 // Redirect for regular form submissions
1233 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions&updated=true'));
1234 exit;
1235 }
1236
1237 // ========================================
1238 // HELPER METHODS
1239 // ========================================
1240
1241 /**
1242 * Returns a specific nonce action string
1243 */
1244 private function mxchat_get_nonce_action() {
1245 return 'mxchat_license_nonce';
1246 }
1247
1248 /**
1249 * Check API key status for all providers
1250 */
1251 public function mxchat_check_api_keys() {
1252 // Check nonce
1253 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mxchat_save_setting_nonce')) {
1254 wp_send_json_error('Invalid nonce');
1255 }
1256
1257 // Check permissions
1258 if (!current_user_can('manage_options')) {
1259 wp_send_json_error('Unauthorized');
1260 }
1261
1262 // Get current options
1263 $options = get_option('mxchat_options', array());
1264
1265 // Check which API keys are present
1266 $api_key_status = array(
1267 'openai' => !empty($options['api_key']),
1268 'claude' => !empty($options['claude_api_key']),
1269 'xai' => !empty($options['xai_api_key']),
1270 'deepseek' => !empty($options['deepseek_api_key']),
1271 'gemini' => !empty($options['gemini_api_key']),
1272 'openrouter' => !empty($options['openrouter_api_key']),
1273 'voyage' => !empty($options['voyage_api_key'])
1274 );
1275
1276 wp_send_json_success($api_key_status);
1277 }
1278
1279 // ========================================
1280 // DEBUG & OPTIMIZATION AJAX HANDLERS
1281 // ========================================
1282
1283 /**
1284 * Toggle debug mode on/off
1285 */
1286 public function mxchat_toggle_debug_mode_callback() {
1287 // Verify nonce
1288 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1289 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1290 }
1291
1292 // Check permissions
1293 if ( ! current_user_can( 'manage_options' ) ) {
1294 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1295 }
1296
1297 $enabled = isset( $_POST['enabled'] ) && $_POST['enabled'] === 'on';
1298
1299 $options = get_option( 'mxchat_options', array() );
1300
1301 if ( $enabled ) {
1302 $options['debug_mode'] = 'on';
1303 update_option( 'mxchat_options', $options );
1304 MxChat_Admin::mxchat_log_debug( 'debug_mode', 'Debug mode enabled' );
1305 } else {
1306 // Log before disabling
1307 MxChat_Admin::mxchat_log_debug( 'debug_mode', 'Debug mode disabled' );
1308 $options['debug_mode'] = 'off';
1309 update_option( 'mxchat_options', $options );
1310 }
1311
1312 wp_send_json_success( array(
1313 'message' => $enabled ? esc_html__( 'Debug mode enabled', 'mxchat' ) : esc_html__( 'Debug mode disabled', 'mxchat' ),
1314 'enabled' => $enabled,
1315 ) );
1316 }
1317
1318 /**
1319 * Get the debug log entries
1320 */
1321 public function mxchat_get_debug_log_callback() {
1322 // Verify nonce
1323 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1324 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1325 }
1326
1327 // Check permissions
1328 if ( ! current_user_can( 'manage_options' ) ) {
1329 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1330 }
1331
1332 $log = MxChat_Admin::mxchat_get_debug_log();
1333
1334 wp_send_json_success( array(
1335 'log' => $log,
1336 'count' => count( $log ),
1337 ) );
1338 }
1339
1340 /**
1341 * Clear the debug log
1342 */
1343 public function mxchat_clear_debug_log_callback() {
1344 // Verify nonce
1345 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1346 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1347 }
1348
1349 // Check permissions
1350 if ( ! current_user_can( 'manage_options' ) ) {
1351 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1352 }
1353
1354 MxChat_Admin::mxchat_clear_debug_log();
1355
1356 // Log that the log was cleared (this will be the first entry in the new log)
1357 MxChat_Admin::mxchat_log_debug( 'debug_log', 'Debug log cleared by user' );
1358
1359 wp_send_json_success( array( 'message' => esc_html__( 'Debug log cleared', 'mxchat' ) ) );
1360 }
1361
1362 /**
1363 * Export settings as JSON
1364 */
1365 public function mxchat_export_settings_callback() {
1366 // Verify nonce
1367 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1368 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1369 }
1370
1371 // Check permissions
1372 if ( ! current_user_can( 'manage_options' ) ) {
1373 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1374 }
1375
1376 $export = MxChat_Admin::mxchat_export_settings();
1377
1378 // Log the export
1379 MxChat_Admin::mxchat_log_debug( 'settings_export', 'Settings exported by user' );
1380
1381 wp_send_json_success( array(
1382 'settings' => $export,
1383 'filename' => 'mxchat-settings-' . gmdate( 'Y-m-d-His' ) . '.json',
1384 ) );
1385 }
1386
1387 /**
1388 * Reset all settings to defaults
1389 */
1390 public function mxchat_reset_all_settings_callback() {
1391 // Verify nonce
1392 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1393 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1394 }
1395
1396 // Check permissions
1397 if ( ! current_user_can( 'manage_options' ) ) {
1398 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1399 }
1400
1401 // Require confirmation code
1402 $confirmation = isset( $_POST['confirmation'] ) ? sanitize_text_field( wp_unslash( $_POST['confirmation'] ) ) : '';
1403
1404 if ( strtoupper( $confirmation ) !== 'RESET' ) {
1405 wp_send_json_error( array( 'message' => esc_html__( 'Invalid confirmation code. Please type RESET to confirm.', 'mxchat' ) ) );
1406 }
1407
1408 // Perform the reset
1409 MxChat_Admin::mxchat_reset_all_settings();
1410
1411 wp_send_json_success( array( 'message' => esc_html__( 'All settings have been reset to defaults. The page will reload.', 'mxchat' ) ) );
1412 }
1413
1414 /**
1415 * Reset the global rate-limit usage counter to zero on demand.
1416 *
1417 * Zeroes the WP option mxchat_chat_limit_<bot>_global that the integrator
1418 * increments per message, then returns a freshly-formatted readout string
1419 * so the settings page can update without a reload. Does NOT change any
1420 * enforcement config — purely clears the running counter.
1421 */
1422 public function mxchat_reset_global_rate_limit_callback() {
1423 // Verify nonce
1424 if ( ! check_ajax_referer( 'mxchat_reset_global_usage', '_ajax_nonce', false ) ) {
1425 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1426 }
1427
1428 // Check permissions
1429 if ( ! current_user_can( 'manage_options' ) ) {
1430 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1431 }
1432
1433 // Resolve the per-bot counter key the same way the integrator does.
1434 $bot_id = isset( $_POST['bot_id'] ) ? sanitize_key( wp_unslash( $_POST['bot_id'] ) ) : 'default';
1435 $safe_bot = preg_replace( '/[^a-zA-Z0-9_]/', '_', $bot_id );
1436 if ( $safe_bot === '' ) {
1437 $safe_bot = 'default';
1438 }
1439 $option_key = 'mxchat_chat_limit_' . $safe_bot . '_global';
1440
1441 $now = time();
1442 update_option( $option_key, array( 'count' => 0, 'timestamp' => $now ) );
1443
1444 // Recompute the display string so the front-end can update in place.
1445 $all_options = get_option( 'mxchat_options', array() );
1446 $global_cfg = isset( $all_options['rate_limits_global'] ) && is_array( $all_options['rate_limits_global'] )
1447 ? $all_options['rate_limits_global']
1448 : array();
1449 $limit_raw = isset( $global_cfg['limit'] ) ? (string) $global_cfg['limit'] : 'unlimited';
1450 // Defensive: if a raw __custom__ ever slips through, fall back to the custom value.
1451 if ( ! ctype_digit( $limit_raw ) && isset( $global_cfg['limit_custom'] ) && ctype_digit( (string) $global_cfg['limit_custom'] ) ) {
1452 $limit_raw = (string) $global_cfg['limit_custom'];
1453 }
1454 $timeframe = isset( $global_cfg['timeframe'] ) ? (string) $global_cfg['timeframe'] : 'daily';
1455 $windows = array( 'hourly' => 3600, 'daily' => 86400, 'weekly' => 604800, 'monthly' => 2592000 );
1456 $window = isset( $windows[ $timeframe ] ) ? $windows[ $timeframe ] : 86400;
1457 $reset_at = $now + $window;
1458 $limit_int = ctype_digit( $limit_raw ) ? (int) $limit_raw : 0;
1459
1460 $text = sprintf(
1461 /* translators: 1: used count, 2: limit, 3: remaining, 4: human-readable time until reset */
1462 esc_html__( '%1$s of %2$s used · %3$s left · resets in %4$s', 'mxchat' ),
1463 number_format_i18n( 0 ),
1464 number_format_i18n( $limit_int ),
1465 number_format_i18n( $limit_int ),
1466 human_time_diff( $now, $reset_at )
1467 );
1468
1469 wp_send_json_success( array(
1470 'count' => 0,
1471 'limit' => $limit_int,
1472 'left' => $limit_int,
1473 'reset_at' => $reset_at,
1474 'pct' => 0,
1475 'text' => $text,
1476 'message' => esc_html__( 'Usage counter reset.', 'mxchat' ),
1477 ) );
1478 }
1479
1480 }
1481
1482 // Initialize the AJAX handler
1483 new MxChat_Ajax_Handler();
1484