PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.10
MxChat – AI Chatbot & Content Generation for WordPress v3.2.10
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.10, at admin/class-ajax-handler.php

1,486 lines 64.8 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 'print_button_enabled',
515 'reset_chat_enabled'
516 ])) {
517 //error_log('MXChat Save: Processing toggle: ' . $field_name);
518 $options[$field_name] = ($value === 'on') ? 'on' : 'off';
519 } else {
520 //error_log('MXChat Save: Processing standard field: ' . $field_name);
521 // Store all other values directly using the extracted field name
522 $options[$field_name] = $value;
523 }
524 break;
525 }
526
527 // Save all updates to the options array
528 $updated = update_option('mxchat_options', $options);
529 //error_log('MXChat Save: Update result: ' . ($updated ? 'success' : 'unchanged') . ' for field: ' . $name);
530 //error_log('MXChat Save: Updated options array: ' . print_r($options, true));
531
532 // Log the save action if debug mode is enabled
533 if ( class_exists( 'MxChat_Admin' ) ) {
534 MxChat_Admin::mxchat_log_debug(
535 'settings_save',
536 sprintf( 'Field saved: %s', $field_name ),
537 array(
538 'field' => $field_name,
539 'updated' => $updated,
540 )
541 );
542 }
543
544 // Always return success even if WordPress says nothing changed
545 // (which happens when the value is the same as before)
546 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
547 }
548
549 /**
550 * Save the selected bot for knowledge base operations
551 */
552 public function mxchat_save_selected_bot() {
553 // Check nonce
554 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mxchat_save_setting_nonce')) {
555 wp_send_json_error('Invalid nonce');
556 }
557
558 // Check permissions
559 if (!current_user_can('manage_options')) {
560 wp_send_json_error('Unauthorized');
561 }
562
563 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
564
565 // Save as user meta for the current user
566 $user_id = get_current_user_id();
567 update_user_meta($user_id, 'mxchat_selected_knowledge_bot', $bot_id);
568
569 // Also save as an option for site-wide default
570 update_option('mxchat_current_knowledge_bot', $bot_id);
571
572 // No cache clearing needed since we removed caching
573
574 wp_send_json_success(array(
575 'message' => 'Bot selection saved',
576 'bot_id' => $bot_id
577 ));
578 }
579
580 /**
581 * Handles AJAX request for saving chat settings
582 */
583 public function mxchat_save_prompts_setting_callback() {
584 check_ajax_referer('mxchat_prompts_setting_nonce');
585
586 if (!current_user_can('manage_options')) {
587 wp_send_json_error(['message' => esc_html__('Unauthorized', 'mxchat')]);
588 }
589
590 $name = isset($_POST['name']) ? $_POST['name'] : '';
591 $value = isset($_POST['value']) ? stripslashes($_POST['value']) : '';
592
593 //error_log('[MXCHAT-PROMPTS] Saving setting: ' . $name . ' = ' . $value);
594
595 if (empty($name)) {
596 wp_send_json_error(['message' => esc_html__('Invalid field name', 'mxchat')]);
597 }
598
599 // Handle Pinecone settings - BYPASS WORDPRESS SANITIZATION
600 if (strpos($name, 'mxchat_pinecone_addon_options') !== false) {
601 //error_log('[MXCHAT-PROMPTS] Processing Pinecone setting: ' . $name);
602
603 // Extract the field name
604 if (preg_match('/mxchat_pinecone_addon_options\[([^\]]+)\]/', $name, $matches)) {
605 $field_name = $matches[1];
606 //error_log('[MXCHAT-PROMPTS] Extracted field name: ' . $field_name);
607
608 // Get current options directly from database - NO WordPress filters
609 global $wpdb;
610 $current_options_raw = $wpdb->get_var(
611 $wpdb->prepare(
612 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
613 'mxchat_pinecone_addon_options'
614 )
615 );
616
617 // FIX: Handle the case where the option doesn't exist yet
618 if ($current_options_raw === null) {
619 // Option doesn't exist, create it with default values
620 $current_options = array(
621 'mxchat_use_pinecone' => '0',
622 'mxchat_pinecone_api_key' => '',
623 'mxchat_pinecone_host' => '',
624 'mxchat_pinecone_index' => '',
625 'mxchat_pinecone_environment' => ''
626 );
627 //error_log('[MXCHAT-PROMPTS] Option does not exist, creating with defaults');
628 } else {
629 // Unserialize the raw data
630 $current_options = maybe_unserialize($current_options_raw);
631 if (!is_array($current_options)) {
632 // Fallback to defaults if unserialization fails
633 $current_options = array(
634 'mxchat_use_pinecone' => '0',
635 'mxchat_pinecone_api_key' => '',
636 'mxchat_pinecone_host' => '',
637 'mxchat_pinecone_index' => '',
638 'mxchat_pinecone_environment' => ''
639 );
640 //error_log('[MXCHAT-PROMPTS] Failed to unserialize, using defaults');
641 }
642 }
643
644 //error_log('[MXCHAT-PROMPTS] Current options from DB: ' . print_r($current_options, true));
645
646 // Update the specific field with proper sanitization
647 switch ($field_name) {
648 case 'mxchat_use_pinecone':
649 $new_value = ($value === '1') ? '1' : '0';
650 break;
651 case 'mxchat_pinecone_api_key':
652 case 'mxchat_pinecone_host':
653 case 'mxchat_pinecone_index':
654 case 'mxchat_pinecone_environment':
655 $new_value = sanitize_text_field($value);
656 if ($field_name === 'mxchat_pinecone_host') {
657 $new_value = str_replace(['https://', 'http://'], '', $new_value);
658 }
659 break;
660 default:
661 wp_send_json_error(['message' => esc_html__('Unknown Pinecone field', 'mxchat')]);
662 }
663
664 $current_options[$field_name] = $new_value;
665 //error_log('[MXCHAT-PROMPTS] New value for ' . $field_name . ': "' . $new_value . '"');
666 //error_log('[MXCHAT-PROMPTS] Updated options: ' . print_r($current_options, true));
667
668 // Save directly to database to bypass WordPress sanitization
669 $serialized_options = maybe_serialize($current_options);
670
671 // FIX: Use INSERT ... ON DUPLICATE KEY UPDATE or separate INSERT/UPDATE logic
672 $option_exists = $wpdb->get_var(
673 $wpdb->prepare(
674 "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s",
675 'mxchat_pinecone_addon_options'
676 )
677 );
678
679 if ($option_exists > 0) {
680 // Update existing option
681 $save_result = $wpdb->update(
682 $wpdb->options,
683 array('option_value' => $serialized_options),
684 array('option_name' => 'mxchat_pinecone_addon_options'),
685 array('%s'),
686 array('%s')
687 );
688 //error_log('[MXCHAT-PROMPTS] Updated existing option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED'));
689 } else {
690 // Insert new option
691 $save_result = $wpdb->insert(
692 $wpdb->options,
693 array(
694 'option_name' => 'mxchat_pinecone_addon_options',
695 'option_value' => $serialized_options,
696 'autoload' => 'yes'
697 ),
698 array('%s', '%s', '%s')
699 );
700 //error_log('[MXCHAT-PROMPTS] Inserted new option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED'));
701 }
702
703 // Clear any WordPress option cache to ensure get_option() returns fresh data
704 wp_cache_delete('mxchat_pinecone_addon_options', 'options');
705
706 // IMPROVED VERIFICATION - Check if the database operation succeeded
707 if ($save_result !== false) {
708 // Double-check by reading fresh from database
709 $verification_raw = $wpdb->get_var(
710 $wpdb->prepare(
711 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
712 'mxchat_pinecone_addon_options'
713 )
714 );
715 $verification_options = maybe_unserialize($verification_raw);
716 $verified_value = isset($verification_options[$field_name]) ? $verification_options[$field_name] : 'NOT_FOUND';
717
718 //error_log('[MXCHAT-PROMPTS] Final verification - Expected: "' . $new_value . '", Got: "' . $verified_value . '"');
719
720 // Use loose comparison (==) instead of strict (===) to avoid type issues
721 if ($verified_value == $new_value || $save_result > 0) {
722 wp_send_json_success(['message' => esc_html__('Pinecone setting saved', 'mxchat')]);
723 } else {
724 // Still return success if the DB operation worked, even if verification is quirky
725 //error_log('[MXCHAT-PROMPTS] Verification mismatch but DB operation succeeded');
726 wp_send_json_success(['message' => esc_html__('Pinecone setting saved (DB success)', 'mxchat')]);
727 }
728 } else {
729 wp_send_json_error(['message' => esc_html__('Database save failed', 'mxchat')]);
730 }
731 } else {
732 wp_send_json_error(['message' => esc_html__('Invalid field name format', 'mxchat')]);
733 }
734
735 return; // Exit here for Pinecone settings
736 }
737 // Handle auto-sync settings (existing functionality)
738 if (strpos($name, 'mxchat_auto_sync_') === 0) {
739 $value = ($value === 'on' || $value === '1') ? '1' : '0';
740 $updated = update_option($name, $value);
741
742 if ($updated || get_option($name) === $value) {
743 wp_send_json_success(['message' => esc_html__('Auto-sync setting saved', 'mxchat')]);
744 } else {
745 wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]);
746 }
747 }
748
749 // Handle chunking settings - use direct DB access to bypass WordPress filters
750 if (strpos($name, 'mxchat_chunk') === 0 || $name === 'mxchat_chunking_enabled') {
751 global $wpdb;
752
753 // Get current options directly from database
754 $current_options_raw = $wpdb->get_var(
755 $wpdb->prepare(
756 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
757 'mxchat_options'
758 )
759 );
760
761 $options = $current_options_raw !== null ? maybe_unserialize($current_options_raw) : array();
762 if (!is_array($options)) {
763 $options = array();
764 }
765
766 // Update the specific chunking field
767 if ($name === 'mxchat_chunking_enabled') {
768 $options['chunking_enabled'] = in_array($value, array('on', '1', 'true', true), true);
769 } elseif ($name === 'mxchat_chunk_size') {
770 $options['chunk_size'] = max(1000, min(10000, intval($value)));
771 }
772
773 // Save directly to database
774 $serialized_options = maybe_serialize($options);
775
776 $option_exists = $wpdb->get_var(
777 $wpdb->prepare(
778 "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s",
779 'mxchat_options'
780 )
781 );
782
783 if ($option_exists > 0) {
784 $save_result = $wpdb->update(
785 $wpdb->options,
786 array('option_value' => $serialized_options),
787 array('option_name' => 'mxchat_options'),
788 array('%s'),
789 array('%s')
790 );
791 } else {
792 $save_result = $wpdb->insert(
793 $wpdb->options,
794 array(
795 'option_name' => 'mxchat_options',
796 'option_value' => $serialized_options,
797 'autoload' => 'yes'
798 ),
799 array('%s', '%s', '%s')
800 );
801 }
802
803 // Clear object cache for this option
804 wp_cache_delete('mxchat_options', 'options');
805
806 if ($save_result !== false) {
807 wp_send_json_success(['message' => esc_html__('Chunking setting saved', 'mxchat')]);
808 } else {
809 wp_send_json_error(['message' => esc_html__('Failed to save chunking setting', 'mxchat')]);
810 }
811 return;
812 }
813
814 // Handle ACF field exclusion toggles
815 if (strpos($name, 'mxchat_acf_field_') === 0) {
816 // Extract field name from the input name (e.g., mxchat_acf_field_private_notes -> private_notes)
817 $field_name = str_replace('mxchat_acf_field_', '', $name);
818 $is_enabled = ($value === 'on' || $value === '1');
819
820 // Get current excluded fields
821 $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
822 if (!is_array($excluded_fields)) {
823 $excluded_fields = array();
824 }
825
826 if ($is_enabled) {
827 // Remove from exclusion list (field should be included)
828 $excluded_fields = array_values(array_diff($excluded_fields, array($field_name)));
829 } else {
830 // Add to exclusion list (field should be excluded)
831 if (!in_array($field_name, $excluded_fields)) {
832 $excluded_fields[] = $field_name;
833 }
834 }
835
836 $updated = update_option('mxchat_acf_excluded_fields', $excluded_fields);
837
838 if ($updated || true) { // Always report success since the state may already be correct
839 wp_send_json_success([
840 'message' => $is_enabled
841 ? sprintf(esc_html__('Field "%s" will be included in imports', 'mxchat'), $field_name)
842 : sprintf(esc_html__('Field "%s" will be excluded from imports', 'mxchat'), $field_name)
843 ]);
844 } else {
845 wp_send_json_error(['message' => esc_html__('Failed to save ACF field setting', 'mxchat')]);
846 }
847 return;
848 }
849
850 // Handle custom post meta whitelist
851 if ($name === 'mxchat_custom_meta_whitelist') {
852 $updated = update_option('mxchat_custom_meta_whitelist', sanitize_textarea_field($value));
853
854 if ($updated || true) { // Always report success since the state may already be correct
855 wp_send_json_success([
856 'message' => esc_html__('Custom meta whitelist saved', 'mxchat')
857 ]);
858 } else {
859 wp_send_json_error(['message' => esc_html__('Failed to save custom meta whitelist', 'mxchat')]);
860 }
861 return;
862 }
863
864 // Handle other prompts options
865 $options = get_option('mxchat_prompts_options', []);
866 $options[$name] = $value;
867 $updated = update_option('mxchat_prompts_options', $options);
868
869 if ($updated) {
870 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
871 } else {
872 wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]);
873 }
874 }
875
876
877 /**
878 * Handles AJAX request for Pinecone settings migration
879 */
880 public function ajax_migrate_pinecone_settings() {
881 // Verify nonce
882 if (!wp_verify_nonce($_POST['_ajax_nonce'] ?? '', 'mxchat_save_setting_nonce')) {
883 wp_send_json_error('Invalid nonce');
884 }
885
886 // Check permissions
887 if (!current_user_can('manage_options')) {
888 wp_send_json_error('Unauthorized access');
889 }
890
891 // Check if old Pinecone addon options exist
892 $old_options = get_option('mxchat_pinecone_addon_options', array());
893
894 if (empty($old_options)) {
895 wp_send_json_success(array('migrated' => false, 'message' => 'No old settings found'));
896 }
897
898 // Get current core plugin options
899 $current_options = get_option('mxchat_pinecone_addon_options', array());
900
901 // Only migrate if core options are empty or if explicitly requested
902 $should_migrate = empty($current_options) ||
903 (empty($current_options['mxchat_pinecone_api_key']) && !empty($old_options['mxchat_pinecone_api_key']));
904
905 if ($should_migrate) {
906 // Migrate settings with proper sanitization
907 $migrated_options = array(
908 'mxchat_use_pinecone' => $old_options['mxchat_use_pinecone'] ?? '0',
909 'mxchat_pinecone_api_key' => sanitize_text_field($old_options['mxchat_pinecone_api_key'] ?? ''),
910 'mxchat_pinecone_host' => sanitize_text_field($old_options['mxchat_pinecone_host'] ?? ''),
911 'mxchat_pinecone_index' => sanitize_text_field($old_options['mxchat_pinecone_index'] ?? ''),
912 'mxchat_pinecone_environment' => sanitize_text_field($old_options['mxchat_pinecone_environment'] ?? '')
913 );
914
915 update_option('mxchat_pinecone_addon_options', $migrated_options);
916
917 wp_send_json_success(array(
918 'migrated' => true,
919 'message' => 'Settings migrated successfully from Pinecone add-on'
920 ));
921 } else {
922 wp_send_json_success(array(
923 'migrated' => false,
924 'message' => 'Settings already exist in core plugin'
925 ));
926 }
927 }
928
929
930 // ========================================
931 // LICENSE AJAX HANDLERS
932 // ========================================
933
934 /**
935 * Validates and activates chat license via AJAX
936 */
937 public function mxchat_handle_activate_license() {
938 // Check nonce
939 if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
940 wp_send_json_error(esc_html__('Invalid security token', 'mxchat'));
941 return;
942 }
943
944 // Verify user capabilities
945 if (!current_user_can('manage_options')) {
946 wp_send_json_error(esc_html__('Unauthorized access', 'mxchat'));
947 return;
948 }
949
950 $license_key = isset($_POST['mxchat_activation_key']) ? sanitize_text_field($_POST['mxchat_activation_key']) : '';
951 $customer_email = isset($_POST['mxchat_pro_email']) ? sanitize_email($_POST['mxchat_pro_email']) : '';
952
953 if (empty($license_key) || empty($customer_email)) {
954 wp_send_json_error(esc_html__('Email or License Key is missing', 'mxchat'));
955 return;
956 }
957
958 $product_id = 'MxChatPRO';
959 $domain = parse_url(home_url(), PHP_URL_HOST); // Get the current domain
960
961 // Call WooCommerce Software API for activation (not just validation)
962 $response = wp_remote_get(
963 add_query_arg(
964 array(
965 'wc-api' => 'software-api',
966 'request' => 'activation',
967 'email' => $customer_email,
968 'license_key' => $license_key,
969 'product_id' => $product_id,
970 'instance' => $domain, // THIS IS KEY - include the domain as instance
971 'platform' => 'wordpress' // Optional but good to include
972 ),
973 'https://mxchat.ai/'
974 ),
975 array(
976 'timeout' => 60,
977 'sslverify' => true
978 )
979 );
980
981 if (is_wp_error($response)) {
982 $error_message = $response->get_error_message();
983 //error_log('MxChat License Activation Error: ' . $error_message);
984 wp_send_json_error(esc_html__('Activation failed due to a server error: ', 'mxchat') . $error_message);
985 return;
986 }
987
988 $response_code = wp_remote_retrieve_response_code($response);
989 $body = wp_remote_retrieve_body($response);
990
991 // Log response for debugging
992 //error_log('MxChat License Response Code: ' . $response_code);
993 //error_log('MxChat License Response Body: ' . $body);
994
995 if ($response_code !== 200) {
996 wp_send_json_error(esc_html__('Server returned error code: ', 'mxchat') . $response_code);
997 return;
998 }
999
1000 $data = json_decode($body);
1001
1002 if ($data && isset($data->activated) && $data->activated) {
1003 // Success - save local options
1004 update_option('mxchat_license_status', 'active');
1005 update_option('mxchat_pro_email', $customer_email);
1006 update_option('mxchat_activation_key', $license_key);
1007 delete_option('mxchat_license_error');
1008
1009 // Also track on your website (this is your existing domain tracking)
1010 $this->track_domain_on_website($license_key, $customer_email, $domain);
1011
1012 wp_send_json_success(array('message' => esc_html__('License activated successfully', 'mxchat')));
1013 } else {
1014 $error_message = isset($data->error) ? $data->error : esc_html__('Activation failed', 'mxchat');
1015 update_option('mxchat_license_status', 'inactive');
1016 update_option('mxchat_license_error', $error_message);
1017
1018 //error_log('MxChat Activation failed: ' . $error_message);
1019 wp_send_json_error($error_message);
1020 }
1021 }
1022
1023 /**
1024 * Track domain on your website (separate from WooCommerce activation)
1025 */
1026 private function track_domain_on_website($license_key, $email, $domain) {
1027 // This calls your website's tracking API
1028 wp_remote_post('https://mxchat.ai/mxchat-api/activate-license', array(
1029 'body' => array(
1030 'mxchat_pro_email' => $email,
1031 'mxchat_activation_key' => $license_key,
1032 'domain' => $domain
1033 ),
1034 'timeout' => 10,
1035 'sslverify' => true
1036 ));
1037 }
1038
1039 /**
1040 * Validates license via AJAX with email and key
1041 */
1042 public function mxchat_check_license_status() {
1043 // Verify nonce
1044 if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
1045 wp_send_json_error('Security check failed');
1046 return;
1047 }
1048
1049 // Add isset checks for safety
1050 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
1051 $key = isset($_POST['key']) ? sanitize_text_field($_POST['key']) : '';
1052
1053 // Check if this license is actually active in your system
1054 $is_active = (get_option('mxchat_license_status') === 'active' &&
1055 get_option('mxchat_pro_email') === $email &&
1056 get_option('mxchat_activation_key') === $key);
1057
1058 wp_send_json(array(
1059 'is_active' => $is_active
1060 ));
1061 }
1062
1063 /**
1064 * Handle license deactivation - Complete version for plugin
1065 */
1066 function mxchat_deactivate_license() {
1067 // Add debugging
1068 //error_log('MxChat deactivate function called');
1069
1070 // Check nonce
1071 if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
1072 //error_log('MxChat deactivate: Nonce check failed');
1073 wp_send_json_error('Security check failed.');
1074 return;
1075 }
1076
1077 //error_log('MxChat deactivate: Nonce check passed');
1078
1079 $license_key = get_option('mxchat_activation_key');
1080 $email = get_option('mxchat_pro_email');
1081 $domain = parse_url(home_url(), PHP_URL_HOST);
1082
1083 //error_log('MxChat deactivate: License: ' . $license_key . ', Email: ' . $email . ', Domain: ' . $domain);
1084
1085 if (empty($license_key) || empty($email)) {
1086 //error_log('MxChat deactivate: No active license found');
1087 wp_send_json_error('No active license found.');
1088 return;
1089 }
1090
1091 // Clear local license data first
1092 delete_option('mxchat_license_status');
1093 delete_option('mxchat_pro_email');
1094 delete_option('mxchat_activation_key');
1095 delete_option('mxchat_license_error');
1096
1097 //error_log('MxChat deactivate: Local data cleared');
1098
1099 // Notify your website's API to properly deactivate
1100 $response = wp_remote_post('https://mxchat.ai/mxchat-api/deactivate-license', array(
1101 'body' => array(
1102 'license_key' => $license_key,
1103 'email' => $email,
1104 'domain' => $domain
1105 ),
1106 'timeout' => 15,
1107 'sslverify' => true
1108 ));
1109
1110 if (is_wp_error($response)) {
1111 //error_log('MxChat deactivate: Server error - ' . $response->get_error_message());
1112 wp_send_json_success(array(
1113 'message' => 'License deactivated locally. Server could not be contacted to free activation slot.',
1114 'server_notified' => false
1115 ));
1116 return;
1117 }
1118
1119 $response_body = wp_remote_retrieve_body($response);
1120 $response_data = json_decode($response_body, true);
1121
1122 //error_log('MxChat deactivate: Server response - ' . $response_body);
1123
1124 if (isset($response_data['success']) && $response_data['success']) {
1125 //error_log('MxChat deactivate: Success with server notification');
1126 wp_send_json_success(array(
1127 'message' => 'License deactivated successfully. Activation slot has been freed up.',
1128 'server_notified' => true
1129 ));
1130 } else {
1131 //error_log('MxChat deactivate: Server responded but deactivation may have failed');
1132 wp_send_json_success(array(
1133 'message' => 'License deactivated locally. Please check your account dashboard to verify the activation was freed.',
1134 'server_notified' => false
1135 ));
1136 }
1137 }
1138
1139
1140 // ========================================
1141 // ACTIONS & INTENTS AJAX HANDLERS
1142 // ========================================
1143
1144 /**
1145 * Validates nonce and returns JSON error on failure
1146 */
1147 public function mxchat_toggle_action() {
1148 // Check nonce
1149 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_actions_nonce')) {
1150 wp_send_json_error(array('message' => 'Security check failed'));
1151 return;
1152 }
1153
1154 // Check permissions
1155 if (!current_user_can('manage_options')) {
1156 wp_send_json_error(array('message' => 'Permission denied'));
1157 return;
1158 }
1159
1160 // Validate params
1161 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
1162 $enabled = isset($_POST['enabled']) ? (bool)$_POST['enabled'] : false;
1163
1164 if (!$intent_id) {
1165 wp_send_json_error(array('message' => 'Invalid action ID'));
1166 return;
1167 }
1168
1169 // Update the intent/action status in the database
1170 global $wpdb;
1171 $table_name = $wpdb->prefix . 'mxchat_intents';
1172
1173 // Using the 'enabled' field - add this field if it doesn't exist
1174 $result = $wpdb->update(
1175 $table_name,
1176 array('enabled' => $enabled ? 1 : 0),
1177 array('id' => $intent_id),
1178 array('%d'),
1179 array('%d')
1180 );
1181
1182 if ($result === false) {
1183 wp_send_json_error(array('message' => 'Database error'));
1184 return;
1185 }
1186
1187 wp_send_json_success();
1188 }
1189
1190
1191 /**
1192 * Validates permissions for AJAX request handling
1193 */
1194 public function mxchat_update_intent_threshold() {
1195 // Check permissions
1196 if (!current_user_can('manage_options')) {
1197 if (wp_doing_ajax()) {
1198 wp_send_json_error(array('message' => 'Unauthorized user'));
1199 return;
1200 }
1201 wp_die(esc_html__('Unauthorized user', 'mxchat'));
1202 }
1203
1204 // Verify nonce
1205 check_admin_referer('mxchat_update_intent_threshold_nonce');
1206
1207 // Process the update if we have valid data
1208 if (isset($_POST['intent_id'], $_POST['intent_threshold'])) {
1209 global $wpdb;
1210 $table_name = $wpdb->prefix . 'mxchat_intents';
1211 $intent_id = intval($_POST['intent_id']);
1212 $threshold_percentage = max(70, min(95, intval($_POST['intent_threshold'])));
1213 $similarity_threshold = $threshold_percentage / 100;
1214
1215 $result = $wpdb->update(
1216 $table_name,
1217 ['similarity_threshold' => $similarity_threshold],
1218 ['id' => $intent_id],
1219 ['%f'],
1220 ['%d']
1221 );
1222
1223 // Handle AJAX requests
1224 if (wp_doing_ajax()) {
1225 if ($result === false) {
1226 wp_send_json_error(array('message' => 'Failed to update threshold'));
1227 } else {
1228 wp_send_json_success(array('threshold' => $threshold_percentage));
1229 }
1230 return;
1231 }
1232 }
1233
1234 // Redirect for regular form submissions
1235 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions&updated=true'));
1236 exit;
1237 }
1238
1239 // ========================================
1240 // HELPER METHODS
1241 // ========================================
1242
1243 /**
1244 * Returns a specific nonce action string
1245 */
1246 private function mxchat_get_nonce_action() {
1247 return 'mxchat_license_nonce';
1248 }
1249
1250 /**
1251 * Check API key status for all providers
1252 */
1253 public function mxchat_check_api_keys() {
1254 // Check nonce
1255 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mxchat_save_setting_nonce')) {
1256 wp_send_json_error('Invalid nonce');
1257 }
1258
1259 // Check permissions
1260 if (!current_user_can('manage_options')) {
1261 wp_send_json_error('Unauthorized');
1262 }
1263
1264 // Get current options
1265 $options = get_option('mxchat_options', array());
1266
1267 // Check which API keys are present
1268 $api_key_status = array(
1269 'openai' => !empty($options['api_key']),
1270 'claude' => !empty($options['claude_api_key']),
1271 'xai' => !empty($options['xai_api_key']),
1272 'deepseek' => !empty($options['deepseek_api_key']),
1273 'gemini' => !empty($options['gemini_api_key']),
1274 'openrouter' => !empty($options['openrouter_api_key']),
1275 'voyage' => !empty($options['voyage_api_key'])
1276 );
1277
1278 wp_send_json_success($api_key_status);
1279 }
1280
1281 // ========================================
1282 // DEBUG & OPTIMIZATION AJAX HANDLERS
1283 // ========================================
1284
1285 /**
1286 * Toggle debug mode on/off
1287 */
1288 public function mxchat_toggle_debug_mode_callback() {
1289 // Verify nonce
1290 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1291 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1292 }
1293
1294 // Check permissions
1295 if ( ! current_user_can( 'manage_options' ) ) {
1296 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1297 }
1298
1299 $enabled = isset( $_POST['enabled'] ) && $_POST['enabled'] === 'on';
1300
1301 $options = get_option( 'mxchat_options', array() );
1302
1303 if ( $enabled ) {
1304 $options['debug_mode'] = 'on';
1305 update_option( 'mxchat_options', $options );
1306 MxChat_Admin::mxchat_log_debug( 'debug_mode', 'Debug mode enabled' );
1307 } else {
1308 // Log before disabling
1309 MxChat_Admin::mxchat_log_debug( 'debug_mode', 'Debug mode disabled' );
1310 $options['debug_mode'] = 'off';
1311 update_option( 'mxchat_options', $options );
1312 }
1313
1314 wp_send_json_success( array(
1315 'message' => $enabled ? esc_html__( 'Debug mode enabled', 'mxchat' ) : esc_html__( 'Debug mode disabled', 'mxchat' ),
1316 'enabled' => $enabled,
1317 ) );
1318 }
1319
1320 /**
1321 * Get the debug log entries
1322 */
1323 public function mxchat_get_debug_log_callback() {
1324 // Verify nonce
1325 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1326 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1327 }
1328
1329 // Check permissions
1330 if ( ! current_user_can( 'manage_options' ) ) {
1331 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1332 }
1333
1334 $log = MxChat_Admin::mxchat_get_debug_log();
1335
1336 wp_send_json_success( array(
1337 'log' => $log,
1338 'count' => count( $log ),
1339 ) );
1340 }
1341
1342 /**
1343 * Clear the debug log
1344 */
1345 public function mxchat_clear_debug_log_callback() {
1346 // Verify nonce
1347 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1348 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1349 }
1350
1351 // Check permissions
1352 if ( ! current_user_can( 'manage_options' ) ) {
1353 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1354 }
1355
1356 MxChat_Admin::mxchat_clear_debug_log();
1357
1358 // Log that the log was cleared (this will be the first entry in the new log)
1359 MxChat_Admin::mxchat_log_debug( 'debug_log', 'Debug log cleared by user' );
1360
1361 wp_send_json_success( array( 'message' => esc_html__( 'Debug log cleared', 'mxchat' ) ) );
1362 }
1363
1364 /**
1365 * Export settings as JSON
1366 */
1367 public function mxchat_export_settings_callback() {
1368 // Verify nonce
1369 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1370 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1371 }
1372
1373 // Check permissions
1374 if ( ! current_user_can( 'manage_options' ) ) {
1375 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1376 }
1377
1378 $export = MxChat_Admin::mxchat_export_settings();
1379
1380 // Log the export
1381 MxChat_Admin::mxchat_log_debug( 'settings_export', 'Settings exported by user' );
1382
1383 wp_send_json_success( array(
1384 'settings' => $export,
1385 'filename' => 'mxchat-settings-' . gmdate( 'Y-m-d-His' ) . '.json',
1386 ) );
1387 }
1388
1389 /**
1390 * Reset all settings to defaults
1391 */
1392 public function mxchat_reset_all_settings_callback() {
1393 // Verify nonce
1394 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1395 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1396 }
1397
1398 // Check permissions
1399 if ( ! current_user_can( 'manage_options' ) ) {
1400 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1401 }
1402
1403 // Require confirmation code
1404 $confirmation = isset( $_POST['confirmation'] ) ? sanitize_text_field( wp_unslash( $_POST['confirmation'] ) ) : '';
1405
1406 if ( strtoupper( $confirmation ) !== 'RESET' ) {
1407 wp_send_json_error( array( 'message' => esc_html__( 'Invalid confirmation code. Please type RESET to confirm.', 'mxchat' ) ) );
1408 }
1409
1410 // Perform the reset
1411 MxChat_Admin::mxchat_reset_all_settings();
1412
1413 wp_send_json_success( array( 'message' => esc_html__( 'All settings have been reset to defaults. The page will reload.', 'mxchat' ) ) );
1414 }
1415
1416 /**
1417 * Reset the global rate-limit usage counter to zero on demand.
1418 *
1419 * Zeroes the WP option mxchat_chat_limit_<bot>_global that the integrator
1420 * increments per message, then returns a freshly-formatted readout string
1421 * so the settings page can update without a reload. Does NOT change any
1422 * enforcement config — purely clears the running counter.
1423 */
1424 public function mxchat_reset_global_rate_limit_callback() {
1425 // Verify nonce
1426 if ( ! check_ajax_referer( 'mxchat_reset_global_usage', '_ajax_nonce', false ) ) {
1427 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1428 }
1429
1430 // Check permissions
1431 if ( ! current_user_can( 'manage_options' ) ) {
1432 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1433 }
1434
1435 // Resolve the per-bot counter key the same way the integrator does.
1436 $bot_id = isset( $_POST['bot_id'] ) ? sanitize_key( wp_unslash( $_POST['bot_id'] ) ) : 'default';
1437 $safe_bot = preg_replace( '/[^a-zA-Z0-9_]/', '_', $bot_id );
1438 if ( $safe_bot === '' ) {
1439 $safe_bot = 'default';
1440 }
1441 $option_key = 'mxchat_chat_limit_' . $safe_bot . '_global';
1442
1443 $now = time();
1444 update_option( $option_key, array( 'count' => 0, 'timestamp' => $now ) );
1445
1446 // Recompute the display string so the front-end can update in place.
1447 $all_options = get_option( 'mxchat_options', array() );
1448 $global_cfg = isset( $all_options['rate_limits_global'] ) && is_array( $all_options['rate_limits_global'] )
1449 ? $all_options['rate_limits_global']
1450 : array();
1451 $limit_raw = isset( $global_cfg['limit'] ) ? (string) $global_cfg['limit'] : 'unlimited';
1452 // Defensive: if a raw __custom__ ever slips through, fall back to the custom value.
1453 if ( ! ctype_digit( $limit_raw ) && isset( $global_cfg['limit_custom'] ) && ctype_digit( (string) $global_cfg['limit_custom'] ) ) {
1454 $limit_raw = (string) $global_cfg['limit_custom'];
1455 }
1456 $timeframe = isset( $global_cfg['timeframe'] ) ? (string) $global_cfg['timeframe'] : 'daily';
1457 $windows = array( 'hourly' => 3600, 'daily' => 86400, 'weekly' => 604800, 'monthly' => 2592000 );
1458 $window = isset( $windows[ $timeframe ] ) ? $windows[ $timeframe ] : 86400;
1459 $reset_at = $now + $window;
1460 $limit_int = ctype_digit( $limit_raw ) ? (int) $limit_raw : 0;
1461
1462 $text = sprintf(
1463 /* translators: 1: used count, 2: limit, 3: remaining, 4: human-readable time until reset */
1464 esc_html__( '%1$s of %2$s used · %3$s left · resets in %4$s', 'mxchat' ),
1465 number_format_i18n( 0 ),
1466 number_format_i18n( $limit_int ),
1467 number_format_i18n( $limit_int ),
1468 human_time_diff( $now, $reset_at )
1469 );
1470
1471 wp_send_json_success( array(
1472 'count' => 0,
1473 'limit' => $limit_int,
1474 'left' => $limit_int,
1475 'reset_at' => $reset_at,
1476 'pct' => 0,
1477 'text' => $text,
1478 'message' => esc_html__( 'Usage counter reset.', 'mxchat' ),
1479 ) );
1480 }
1481
1482 }
1483
1484 // Initialize the AJAX handler
1485 new MxChat_Ajax_Handler();
1486