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

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