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

1,803 lines 79.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 // Editor Assistant enable toggle (plan-8cb0cb). STANDALONE option — NOT part
391 // of mxchat_options, so it skips the mxchat_sanitize strip-trap entirely. Save
392 // it directly and short-circuit (mirrors the mxchat_transcripts_options pattern
393 // below); never falls through to the generic mxchat_options save. Default OFF.
394 case 'mxchat_editor_assistant_enabled':
395 $ea_value = ($value === 'on' || $value === '1') ? 'on' : 'off';
396 update_option('mxchat_editor_assistant_enabled', $ea_value);
397 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
398 return;
399
400 // Smart asset loading toggle (plan-915355). STANDALONE option, same
401 // reasoning as the Editor Assistant case above — saved directly and
402 // short-circuited so it never touches mxchat_options / mxchat_sanitize.
403 // Default OFF (opt-in performance optimization).
404 case 'mxchat_smart_asset_loading':
405 $sal_value = ($value === 'on' || $value === '1') ? 'on' : 'off';
406 update_option('mxchat_smart_asset_loading', $sal_value);
407 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
408 return;
409
410 // Hybrid keyword boost toggle (plan-38ffa1). STANDALONE option, same
411 // pattern. Enabling runs capability detection HERE, at admin-save time —
412 // building the FULLTEXT index during a visitor's chat request is not
413 // acceptable, and detection is a one-time cost the admin can wait on.
414 case 'mxchat_hybrid_keyword_toggle':
415 $hkb_value = ($value === 'on' || $value === '1') ? 'on' : 'off';
416 update_option('mxchat_hybrid_keyword_toggle', $hkb_value);
417 if ($hkb_value === 'on') {
418 MxChat_Utils::mxchat_hybrid_detect_capability(true);
419 }
420 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
421 return;
422
423 // ACF→PDF import-time extraction (plan 11720c). STANDALONE option, same
424 // pattern. Moved from a per-import modal checkbox to an install-level
425 // setting on Knowledge → ACF Fields. Stored '1'/'0' to match the
426 // knowledge page's sibling toggles. Default OFF.
427 case 'mxchat_acf_pdf_extraction':
428 $apx_value = ($value === 'on' || $value === '1') ? '1' : '0';
429 update_option('mxchat_acf_pdf_extraction', $apx_value);
430 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
431 return;
432
433 // Live-agent availability schedules (plans 8ccaa2 + 99d7a4). STANDALONE
434 // options, same reasoning as the Editor Assistant case above — nested
435 // structures that mxchat_sanitize() would strip on the next autosave of any
436 // other field. Each channel's value arrives as JSON from its own hidden
437 // input, which that channel's schedule editor keeps in sync; the class owns
438 // all validation. The bare legacy name is kept as a defense against a
439 // browser still running pre-split cached admin JS: that UI edited "both
440 // channels" as one, so its save writes both.
441 case 'live_agent_schedule_slack':
442 case 'live_agent_schedule_telegram':
443 case 'live_agent_schedule':
444 if (!class_exists('MxChat_Live_Agent_Schedule')) {
445 wp_send_json_error(['message' => esc_html__('Schedule unavailable', 'mxchat')]);
446 return;
447 }
448 $decoded = json_decode($value, true);
449 if (!is_array($decoded)) {
450 wp_send_json_error(['message' => esc_html__('Invalid schedule', 'mxchat')]);
451 return;
452 }
453 $channels = ($field_name === 'live_agent_schedule')
454 ? array('slack', 'telegram')
455 : array(substr($field_name, strlen('live_agent_schedule_')));
456 $saved_schedule = null;
457 foreach ($channels as $schedule_channel) {
458 $saved_schedule = MxChat_Live_Agent_Schedule::save($schedule_channel, $decoded);
459 }
460 // Echo the normalized result so the editor can reconcile if it ever
461 // disagrees with the server (e.g. a time the class rejected).
462 wp_send_json_success([
463 'message' => esc_html__('Setting saved', 'mxchat'),
464 'schedule' => $saved_schedule,
465 ]);
466 return;
467
468 case 'model':
469 //error_log('MXChat Save: Processing model selection');
470 //error_log('MXChat Save: Model value received: ' . $value);
471 //error_log('MXChat Save: Value type: ' . gettype($value));
472 //error_log('MXChat Save: Value length: ' . strlen($value));
473 //error_log('MXChat Save: Value === "openrouter": ' . ($value === 'openrouter' ? 'YES' : 'NO'));
474
475 // Allow 'openrouter' or validate against whitelist
476 if ($value === 'openrouter') {
477 //error_log('MXChat Save: Setting model to openrouter');
478 $options['model'] = 'openrouter';
479 } else {
480 //error_log('MXChat Save: Checking against whitelist');
481 // Catalog refactor (plan-d14e89): canonical allowlist lives in
482 // includes/class-mxchat-model-catalog.php. A new chat model
483 // added there is automatically accepted by autosave.
484 if (!class_exists('MxChat_Model_Catalog')) {
485 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-mxchat-model-catalog.php';
486 }
487 $allowed_models = MxChat_Model_Catalog::chat_model_ids();
488
489 //error_log('MXChat Save: in_array result: ' . (in_array($value, $allowed_models) ? 'YES' : 'NO'));
490
491 if (in_array($value, $allowed_models)) {
492 //error_log('MXChat Save: Model is in whitelist, saving');
493 $options['model'] = sanitize_text_field($value);
494 } else {
495 //error_log('MXChat Save: Invalid model rejected: ' . $value);
496 //error_log('MXChat Save: Allowed models: ' . print_r($allowed_models, true));
497 wp_send_json_error(['message' => esc_html__('Invalid model selected', 'mxchat')]);
498 return;
499 }
500 }
501 break;
502
503 case 'openrouter_selected_model':
504 //error_log('MXChat Save: Processing OpenRouter model: ' . $value);
505 $options['openrouter_selected_model'] = sanitize_text_field($value);
506 // Force immediate save for new keys
507 //error_log('MXChat Save: OpenRouter model saved immediately');
508 break;
509
510 case 'openrouter_selected_model_name':
511 //error_log('MXChat Save: Processing OpenRouter model name: ' . $value);
512 $options['openrouter_selected_model_name'] = sanitize_text_field($value);
513 // Force immediate save for new keys
514 //error_log('MXChat Save: OpenRouter model name saved immediately');
515 break;
516
517 case 'openrouter_api_key':
518 //error_log('MXChat Save: Processing OpenRouter API key');
519 $options['openrouter_api_key'] = sanitize_text_field($value);
520 break;
521
522 // REMOVED DUPLICATE case 'openrouter_selected_model_name' HERE!
523
524 case 'additional_popular_questions':
525 //error_log('MXChat Save: Processing additional_popular_questions');
526 $questions = json_decode($value, true); // No need for stripslashes here
527 if (is_array($questions)) {
528 $options[$field_name] = $questions;
529 // Also update old option for backwards compatibility
530 update_option('additional_popular_questions', $questions);
531 //error_log('MXChat Save: Saved ' . count($questions) . ' additional questions');
532 } else {
533 //error_log('MXChat Save: Failed to decode questions JSON');
534 }
535 break;
536 case 'email_blocker_header_content':
537 //error_log('MXChat Save: Processing email_blocker_header_content');
538 // Allow HTML content but sanitize it safely
539 $options[$field_name] = wp_kses_post($value);
540 break;
541 case 'intro_message':
542 // Stored-XSS hardening (Wordfence CWE-79, plan-3f8158): sanitize on save as
543 // defense in depth. wp_kses_post mirrors mxchat_sanitize() (the options.php
544 // save path) so both save routes treat intro_message identically and strip
545 // <script>/</textarea> breakout while keeping basic formatting + {visitor_name}.
546 $options[$field_name] = wp_kses_post($value);
547 break;
548 case 'email_blocker_button_text':
549 //error_log('MXChat Save: Processing email_blocker_button_text');
550 $options[$field_name] = sanitize_text_field($value);
551 break;
552 case 'name_field_placeholder':
553 //error_log('MXChat Save: Processing name_field_placeholder');
554 $options[$field_name] = sanitize_text_field($value);
555 break;
556 case 'similarity_threshold':
557 //error_log('MXChat Save: Processing similarity_threshold');
558 // Validate and save - enforce min 20, max 85
559 $threshold = intval($value);
560 if ($threshold < 20) $threshold = 20;
561 if ($threshold > 85) $threshold = 85;
562 $options[$field_name] = $threshold;
563 break;
564 case 'rag_sources_limit':
565 //error_log('MXChat Save: Processing rag_sources_limit');
566 // Validate and save - enforce min 3, max 10, default 6
567 $rag_limit = intval($value);
568 if ($rag_limit < 3) $rag_limit = 3;
569 if ($rag_limit > 10) $rag_limit = 10;
570 $options[$field_name] = $rag_limit;
571 break;
572 case 'rag_chunks_limit':
573 // Validate and save - enforce min 8, max 20, default 15
574 $chunks_limit = intval($value);
575 if ($chunks_limit < 8) $chunks_limit = 8;
576 if ($chunks_limit > 20) $chunks_limit = 20;
577 $options[$field_name] = $chunks_limit;
578 break;
579 case 'live_agent_status':
580 //error_log('MXChat Save: Processing live_agent_status');
581 // Set the new value
582 $options[$field_name] = ($value === 'on') ? 'on' : 'off';
583 break;
584 case 'enable_web_search':
585 //error_log('MXChat Save: Processing enable_web_search');
586 $options[$field_name] = ($value === 'on') ? 'on' : 'off';
587 break;
588 case 'enable_woocommerce_integration':
589 //error_log('MXChat Save: Processing enable_woocommerce_integration');
590 // Handle values that used to be 1/0
591 $options[$field_name] = ($value === 'on' || $value === '1') ? 'on' : 'off';
592 break;
593 case 'post_type_visibility_mode':
594 // Validate mode value
595 $allowed_modes = array('all', 'include', 'exclude');
596 $options[$field_name] = in_array($value, $allowed_modes) ? $value : 'all';
597 break;
598 case 'post_type_visibility_list':
599 // Handle JSON array of post types
600 $post_types = json_decode($value, true);
601 if (is_array($post_types)) {
602 // Sanitize each post type slug
603 $options[$field_name] = array_map('sanitize_key', $post_types);
604 } else {
605 $options[$field_name] = array();
606 }
607 break;
608 case 'script_loading_strategy':
609 // Validate script loading strategy value
610 $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
611 $options[$field_name] = in_array($value, $allowed_strategies) ? $value : 'default';
612 break;
613 case 'auto_retry_on_transient_error':
614 // Boolean toggle — accept 1/0/on/off, default to '1' if any truthy value.
615 $options[$field_name] = ($value === '1' || $value === 'on' || $value === 1 || $value === true) ? '1' : '0';
616 break;
617 default:
618 // Handle transcripts options
619 if (strpos($name, 'mxchat_transcripts_options') !== false) {
620 // Extract field name from mxchat_transcripts_options[field_name]
621 if (preg_match('/mxchat_transcripts_options\[([^\]]+)\]/', $name, $matches)) {
622 $field_name = $matches[1];
623
624 // Get current transcripts options
625 $transcripts_options = get_option('mxchat_transcripts_options', array());
626
627 // Ensure it's an array
628 if (!is_array($transcripts_options)) {
629 $transcripts_options = array();
630 }
631
632 // Handle checkbox values (convert 'on'/'off' to 1/0)
633 if ($value === 'on' || $value === '1') {
634 $transcripts_options[$field_name] = 1;
635 } else if ($value === 'off' || $value === '0' || $value === '') {
636 $transcripts_options[$field_name] = 0;
637 } else {
638 // For text/select fields, sanitize appropriately
639 if ($field_name === 'mxchat_notification_email') {
640 $transcripts_options[$field_name] = sanitize_email($value);
641 } else {
642 $transcripts_options[$field_name] = sanitize_text_field($value);
643 }
644 }
645
646 // Use direct database update to bypass any filters
647 global $wpdb;
648
649 // Serialize the options array
650 $serialized = maybe_serialize($transcripts_options);
651
652 // Check if the option already exists in the database
653 $existing = $wpdb->get_var("SELECT option_id FROM {$wpdb->options} WHERE option_name = 'mxchat_transcripts_options'");
654
655 if ($existing) {
656 // Option exists, do an update
657 $result = $wpdb->update(
658 $wpdb->options,
659 array('option_value' => $serialized),
660 array('option_name' => 'mxchat_transcripts_options'),
661 array('%s'),
662 array('%s')
663 );
664 } else {
665 // Option doesn't exist (new install), do an insert
666 $result = $wpdb->insert(
667 $wpdb->options,
668 array(
669 'option_name' => 'mxchat_transcripts_options',
670 'option_value' => $serialized,
671 'autoload' => 'yes'
672 ),
673 array('%s', '%s', '%s')
674 );
675 }
676
677 // Clear all caches after direct DB update
678 wp_cache_delete('mxchat_transcripts_options', 'options');
679 wp_cache_delete('alloptions', 'options');
680 wp_cache_flush();
681
682 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
683 return;
684 }
685 }
686 // Whole-chatbot global cap (sits in mxchat_options['rate_limits_global']).
687 // Field names: mxchat_options[rate_limits_global][limit|timeframe|limit_custom]
688 else if (strpos($name, 'mxchat_options[rate_limits_global]') !== false) {
689 preg_match('/\[rate_limits_global\]\[(.*?)\]/', $name, $matches);
690 if (isset($matches[1])) {
691 $setting_key = $matches[1];
692 if (!isset($options['rate_limits_global']) || !is_array($options['rate_limits_global'])) {
693 $options['rate_limits_global'] = array('limit' => 'unlimited', 'timeframe' => 'daily');
694 }
695 if ($setting_key === 'limit') {
696 // Selection from the preset dropdown. If __custom__, resolve from limit_custom; otherwise store directly.
697 if ($value === '__custom__') {
698 $custom = isset($options['rate_limits_global']['limit_custom']) ? (string) $options['rate_limits_global']['limit_custom'] : '';
699 if ($custom !== '' && ctype_digit($custom) && (int) $custom >= 1) {
700 $options['rate_limits_global']['limit'] = $custom;
701 }
702 // else leave existing limit untouched until the custom value arrives
703 } else {
704 $options['rate_limits_global']['limit'] = $value;
705 }
706 } elseif ($setting_key === 'limit_custom') {
707 $clean = preg_replace('/[^0-9]/', '', (string) $value);
708 $options['rate_limits_global']['limit_custom'] = $clean;
709 // Mirror a valid custom value into limit UNCONDITIONALLY (plan-74eb86).
710 // The custom number input is only editable when the dropdown is on
711 // "Custom…" (the toggle JS hides it for presets/unlimited) and autosave
712 // sends one field per change event, so a limit_custom change only fires
713 // in custom mode — there is no preset to clobber. The old guard required
714 // limit to already be non-preset, which it isn't on a first-time custom
715 // entry (the limit=__custom__ event arrives before limit_custom is set),
716 // so the value never landed in limit on the first save and reverted on refresh.
717 if ($clean !== '' && (int) $clean >= 1) {
718 $options['rate_limits_global']['limit'] = $clean;
719 }
720 } elseif ($setting_key === 'timeframe') {
721 $allowed_tf = array('hourly','daily','weekly','monthly');
722 $options['rate_limits_global']['timeframe'] = in_array($value, $allowed_tf, true) ? $value : 'daily';
723 }
724 }
725 }
726 // First check for rate limits settings
727 else if (strpos($name, 'mxchat_options[rate_limits]') !== false) {
728 //error_log('MXChat Save: Detected rate_limits field: ' . $name);
729
730 // Extract role ID and setting from the name
731 preg_match('/\[rate_limits\]\[(.*?)\]\[(.*?)\]/', $name, $matches);
732 //error_log('MXChat Save: Regex matches: ' . print_r($matches, true));
733
734 if (isset($matches[1]) && isset($matches[2])) {
735 $role_id = $matches[1];
736 $setting_key = $matches[2]; // limit, timeframe, message, or limit_custom
737
738 //error_log('MXChat Save: Role ID = ' . $role_id . ', Setting Key = ' . $setting_key);
739
740 // Initialize rate_limits if it doesn't exist
741 if (!isset($options['rate_limits'])) {
742 // //error_log('MXChat Save: Initializing rate_limits array');
743 $options['rate_limits'] = [];
744 }
745
746 // Initialize role settings if it doesn't exist
747 if (!isset($options['rate_limits'][$role_id])) {
748 //error_log('MXChat Save: Initializing rate_limits for role: ' . $role_id);
749 $options['rate_limits'][$role_id] = [
750 'limit' => ($role_id === 'logged_out') ? '10' : '100',
751 'timeframe' => 'daily',
752 'message' => 'Rate limit exceeded. Please try again later.'
753 ];
754 }
755
756 if ($setting_key === 'limit') {
757 if ($value === '__custom__') {
758 // Pull the integer from limit_custom that may have arrived (or will arrive).
759 $custom = isset($options['rate_limits'][$role_id]['limit_custom']) ? (string) $options['rate_limits'][$role_id]['limit_custom'] : '';
760 if ($custom !== '' && ctype_digit($custom) && (int) $custom >= 1) {
761 $options['rate_limits'][$role_id]['limit'] = $custom;
762 }
763 } else {
764 $options['rate_limits'][$role_id]['limit'] = $value;
765 }
766 } elseif ($setting_key === 'limit_custom') {
767 $clean = preg_replace('/[^0-9]/', '', (string) $value);
768 $options['rate_limits'][$role_id]['limit_custom'] = $clean;
769 // Mirror a valid custom value into limit UNCONDITIONALLY — same reasoning
770 // as the global branch above (plan-74eb86). The per-role custom input is
771 // only editable in custom mode and autosave is one-field-per-change, so
772 // this never clobbers a preset; it fixes the first-time-save revert.
773 if ($clean !== '' && (int) $clean >= 1) {
774 $options['rate_limits'][$role_id]['limit'] = $clean;
775 }
776 } else {
777 // Update the specific setting (timeframe, message)
778 $options['rate_limits'][$role_id][$setting_key] = $value;
779 }
780 //error_log('MXChat Save: Updated rate_limits[' . $role_id . '][' . $setting_key . '] = ' . $value);
781 } else {
782 //error_log('MXChat Save: Failed to parse rate_limits pattern: ' . $name);
783 }
784 }
785 // Then check for role rate limits (old format)
786 else if (strpos($name, 'mxchat_options[role_rate_limits]') !== false) {
787 //error_log('MXChat Save: Processing role_rate_limits field: ' . $name);
788 // Extract role ID from the name
789 preg_match('/\[role_rate_limits\]\[(.*?)\]/', $name, $matches);
790 //error_log('MXChat Save: Regex matches: ' . print_r($matches, true));
791
792 if (isset($matches[1])) {
793 $role_id = $matches[1];
794 // Initialize role_rate_limits if it doesn't exist
795 if (!isset($options['role_rate_limits'])) {
796 //error_log('MXChat Save: Initializing role_rate_limits array');
797 $options['role_rate_limits'] = [];
798 }
799 // Update the specific role's rate limit
800 $options['role_rate_limits'][$role_id] = sanitize_text_field($value);
801 //error_log('MXChat Save: Updated role_rate_limits[' . $role_id . '] = ' . $value);
802 } else {
803 //error_log('MXChat Save: Failed to parse role_rate_limits pattern: ' . $name);
804 }
805 }
806 // Handle toggles - check both extracted field_name and original name for toggle detection
807 else if (strpos($field_name, 'toggle') !== false || in_array($field_name, [
808 'chat_persistence_toggle',
809 'privacy_toggle',
810 'complianz_toggle',
811 'chat_toolbar_toggle',
812 'show_pdf_upload_button',
813 'show_word_upload_button',
814 'enable_streaming_toggle',
815 'contextual_awareness_toggle',
816 'citation_links_toggle',
817 'enable_email_block',
818 'enable_name_field',
819 'custom_provider_for_embeddings',
820 'custom_provider_for_images',
821 'print_button_enabled',
822 'reset_chat_enabled'
823 ])) {
824 //error_log('MXChat Save: Processing toggle: ' . $field_name);
825 $options[$field_name] = ($value === 'on') ? 'on' : 'off';
826 } else {
827 //error_log('MXChat Save: Processing standard field: ' . $field_name);
828 // Store all other values directly using the extracted field name
829 $options[$field_name] = $value;
830 }
831 break;
832 }
833
834 // Save all updates to the options array
835 $updated = update_option('mxchat_options', $options);
836 //error_log('MXChat Save: Update result: ' . ($updated ? 'success' : 'unchanged') . ' for field: ' . $name);
837 //error_log('MXChat Save: Updated options array: ' . print_r($options, true));
838
839 // Log the save action if debug mode is enabled
840 if ( class_exists( 'MxChat_Admin' ) ) {
841 MxChat_Admin::mxchat_log_debug(
842 'settings_save',
843 sprintf( 'Field saved: %s', $field_name ),
844 array(
845 'field' => $field_name,
846 'updated' => $updated,
847 )
848 );
849 }
850
851 // Always return success even if WordPress says nothing changed
852 // (which happens when the value is the same as before)
853 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
854 }
855
856 /**
857 * Save the selected bot for knowledge base operations
858 */
859 public function mxchat_save_selected_bot() {
860 // Check nonce
861 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mxchat_save_setting_nonce')) {
862 wp_send_json_error('Invalid nonce');
863 }
864
865 // Check permissions
866 if (!current_user_can('manage_options')) {
867 wp_send_json_error('Unauthorized');
868 }
869
870 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
871
872 // Save as user meta for the current user
873 $user_id = get_current_user_id();
874 update_user_meta($user_id, 'mxchat_selected_knowledge_bot', $bot_id);
875
876 // Also save as an option for site-wide default
877 update_option('mxchat_current_knowledge_bot', $bot_id);
878
879 // No cache clearing needed since we removed caching
880
881 wp_send_json_success(array(
882 'message' => 'Bot selection saved',
883 'bot_id' => $bot_id
884 ));
885 }
886
887 /**
888 * Handles AJAX request for saving chat settings
889 */
890 public function mxchat_save_prompts_setting_callback() {
891 check_ajax_referer('mxchat_prompts_setting_nonce');
892
893 if (!current_user_can('manage_options')) {
894 wp_send_json_error(['message' => esc_html__('Unauthorized', 'mxchat')]);
895 }
896
897 $name = isset($_POST['name']) ? $_POST['name'] : '';
898 $value = isset($_POST['value']) ? stripslashes($_POST['value']) : '';
899
900 //error_log('[MXCHAT-PROMPTS] Saving setting: ' . $name . ' = ' . $value);
901
902 if (empty($name)) {
903 wp_send_json_error(['message' => esc_html__('Invalid field name', 'mxchat')]);
904 }
905
906 // Handle Pinecone settings - BYPASS WORDPRESS SANITIZATION
907 if (strpos($name, 'mxchat_pinecone_addon_options') !== false) {
908 //error_log('[MXCHAT-PROMPTS] Processing Pinecone setting: ' . $name);
909
910 // Extract the field name
911 if (preg_match('/mxchat_pinecone_addon_options\[([^\]]+)\]/', $name, $matches)) {
912 $field_name = $matches[1];
913 //error_log('[MXCHAT-PROMPTS] Extracted field name: ' . $field_name);
914
915 // Get current options directly from database - NO WordPress filters
916 global $wpdb;
917 $current_options_raw = $wpdb->get_var(
918 $wpdb->prepare(
919 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
920 'mxchat_pinecone_addon_options'
921 )
922 );
923
924 // FIX: Handle the case where the option doesn't exist yet
925 if ($current_options_raw === null) {
926 // Option doesn't exist, create it with default values
927 $current_options = array(
928 'mxchat_use_pinecone' => '0',
929 'mxchat_pinecone_api_key' => '',
930 'mxchat_pinecone_host' => '',
931 'mxchat_pinecone_index' => '',
932 'mxchat_pinecone_environment' => ''
933 );
934 //error_log('[MXCHAT-PROMPTS] Option does not exist, creating with defaults');
935 } else {
936 // Unserialize the raw data
937 $current_options = maybe_unserialize($current_options_raw);
938 if (!is_array($current_options)) {
939 // Fallback to defaults if unserialization fails
940 $current_options = array(
941 'mxchat_use_pinecone' => '0',
942 'mxchat_pinecone_api_key' => '',
943 'mxchat_pinecone_host' => '',
944 'mxchat_pinecone_index' => '',
945 'mxchat_pinecone_environment' => ''
946 );
947 //error_log('[MXCHAT-PROMPTS] Failed to unserialize, using defaults');
948 }
949 }
950
951 //error_log('[MXCHAT-PROMPTS] Current options from DB: ' . print_r($current_options, true));
952
953 // Update the specific field with proper sanitization
954 switch ($field_name) {
955 case 'mxchat_use_pinecone':
956 $new_value = ($value === '1') ? '1' : '0';
957 break;
958 case 'mxchat_pinecone_api_key':
959 case 'mxchat_pinecone_host':
960 case 'mxchat_pinecone_index':
961 case 'mxchat_pinecone_environment':
962 $new_value = sanitize_text_field($value);
963 if ($field_name === 'mxchat_pinecone_host') {
964 $new_value = str_replace(['https://', 'http://'], '', $new_value);
965 }
966 break;
967 default:
968 wp_send_json_error(['message' => esc_html__('Unknown Pinecone field', 'mxchat')]);
969 }
970
971 $current_options[$field_name] = $new_value;
972 //error_log('[MXCHAT-PROMPTS] New value for ' . $field_name . ': "' . $new_value . '"');
973 //error_log('[MXCHAT-PROMPTS] Updated options: ' . print_r($current_options, true));
974
975 // Save directly to database to bypass WordPress sanitization
976 $serialized_options = maybe_serialize($current_options);
977
978 // FIX: Use INSERT ... ON DUPLICATE KEY UPDATE or separate INSERT/UPDATE logic
979 $option_exists = $wpdb->get_var(
980 $wpdb->prepare(
981 "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s",
982 'mxchat_pinecone_addon_options'
983 )
984 );
985
986 if ($option_exists > 0) {
987 // Update existing option
988 $save_result = $wpdb->update(
989 $wpdb->options,
990 array('option_value' => $serialized_options),
991 array('option_name' => 'mxchat_pinecone_addon_options'),
992 array('%s'),
993 array('%s')
994 );
995 //error_log('[MXCHAT-PROMPTS] Updated existing option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED'));
996 } else {
997 // Insert new option
998 // Credential option — must NOT autoload (holds the Pinecone secret;
999 // autoloaded rows are read into memory on every request).
1000 $save_result = $wpdb->insert(
1001 $wpdb->options,
1002 array(
1003 'option_name' => 'mxchat_pinecone_addon_options',
1004 'option_value' => $serialized_options,
1005 'autoload' => 'off'
1006 ),
1007 array('%s', '%s', '%s')
1008 );
1009 //error_log('[MXCHAT-PROMPTS] Inserted new option, result: ' . ($save_result !== false ? 'SUCCESS' : 'FAILED'));
1010 }
1011
1012 // Clear any WordPress option cache to ensure get_option() returns fresh data
1013 wp_cache_delete('mxchat_pinecone_addon_options', 'options');
1014
1015 // IMPROVED VERIFICATION - Check if the database operation succeeded
1016 if ($save_result !== false) {
1017 // Double-check by reading fresh from database
1018 $verification_raw = $wpdb->get_var(
1019 $wpdb->prepare(
1020 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
1021 'mxchat_pinecone_addon_options'
1022 )
1023 );
1024 $verification_options = maybe_unserialize($verification_raw);
1025 $verified_value = isset($verification_options[$field_name]) ? $verification_options[$field_name] : 'NOT_FOUND';
1026
1027 //error_log('[MXCHAT-PROMPTS] Final verification - Expected: "' . $new_value . '", Got: "' . $verified_value . '"');
1028
1029 // Use loose comparison (==) instead of strict (===) to avoid type issues
1030 if ($verified_value == $new_value || $save_result > 0) {
1031 wp_send_json_success(['message' => esc_html__('Pinecone setting saved', 'mxchat')]);
1032 } else {
1033 // Still return success if the DB operation worked, even if verification is quirky
1034 //error_log('[MXCHAT-PROMPTS] Verification mismatch but DB operation succeeded');
1035 wp_send_json_success(['message' => esc_html__('Pinecone setting saved (DB success)', 'mxchat')]);
1036 }
1037 } else {
1038 wp_send_json_error(['message' => esc_html__('Database save failed', 'mxchat')]);
1039 }
1040 } else {
1041 wp_send_json_error(['message' => esc_html__('Invalid field name format', 'mxchat')]);
1042 }
1043
1044 return; // Exit here for Pinecone settings
1045 }
1046 // Handle auto-sync settings (existing functionality)
1047 if (strpos($name, 'mxchat_auto_sync_') === 0) {
1048 $value = ($value === 'on' || $value === '1') ? '1' : '0';
1049 $updated = update_option($name, $value);
1050
1051 if ($updated || get_option($name) === $value) {
1052 wp_send_json_success(['message' => esc_html__('Auto-sync setting saved', 'mxchat')]);
1053 } else {
1054 wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]);
1055 }
1056 }
1057
1058 // Handle chunking settings - use direct DB access to bypass WordPress filters
1059 if (strpos($name, 'mxchat_chunk') === 0 || $name === 'mxchat_chunking_enabled') {
1060 global $wpdb;
1061
1062 // Get current options directly from database
1063 $current_options_raw = $wpdb->get_var(
1064 $wpdb->prepare(
1065 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
1066 'mxchat_options'
1067 )
1068 );
1069
1070 $options = $current_options_raw !== null ? maybe_unserialize($current_options_raw) : array();
1071 if (!is_array($options)) {
1072 $options = array();
1073 }
1074
1075 // Update the specific chunking field
1076 if ($name === 'mxchat_chunking_enabled') {
1077 $options['chunking_enabled'] = in_array($value, array('on', '1', 'true', true), true);
1078 } elseif ($name === 'mxchat_chunk_size') {
1079 $options['chunk_size'] = max(1000, min(10000, intval($value)));
1080 }
1081
1082 // Save directly to database
1083 $serialized_options = maybe_serialize($options);
1084
1085 $option_exists = $wpdb->get_var(
1086 $wpdb->prepare(
1087 "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name = %s",
1088 'mxchat_options'
1089 )
1090 );
1091
1092 if ($option_exists > 0) {
1093 $save_result = $wpdb->update(
1094 $wpdb->options,
1095 array('option_value' => $serialized_options),
1096 array('option_name' => 'mxchat_options'),
1097 array('%s'),
1098 array('%s')
1099 );
1100 } else {
1101 $save_result = $wpdb->insert(
1102 $wpdb->options,
1103 array(
1104 'option_name' => 'mxchat_options',
1105 'option_value' => $serialized_options,
1106 'autoload' => 'yes'
1107 ),
1108 array('%s', '%s', '%s')
1109 );
1110 }
1111
1112 // Clear object cache for this option
1113 wp_cache_delete('mxchat_options', 'options');
1114
1115 if ($save_result !== false) {
1116 wp_send_json_success(['message' => esc_html__('Chunking setting saved', 'mxchat')]);
1117 } else {
1118 wp_send_json_error(['message' => esc_html__('Failed to save chunking setting', 'mxchat')]);
1119 }
1120 return;
1121 }
1122
1123 // Handle ACF field exclusion toggles
1124 if (strpos($name, 'mxchat_acf_field_') === 0) {
1125 // Extract field name from the input name (e.g., mxchat_acf_field_private_notes -> private_notes)
1126 $field_name = str_replace('mxchat_acf_field_', '', $name);
1127 $is_enabled = ($value === 'on' || $value === '1');
1128
1129 // Get current excluded fields
1130 $excluded_fields = get_option('mxchat_acf_excluded_fields', array());
1131 if (!is_array($excluded_fields)) {
1132 $excluded_fields = array();
1133 }
1134
1135 if ($is_enabled) {
1136 // Remove from exclusion list (field should be included)
1137 $excluded_fields = array_values(array_diff($excluded_fields, array($field_name)));
1138 } else {
1139 // Add to exclusion list (field should be excluded)
1140 if (!in_array($field_name, $excluded_fields)) {
1141 $excluded_fields[] = $field_name;
1142 }
1143 }
1144
1145 $updated = update_option('mxchat_acf_excluded_fields', $excluded_fields);
1146
1147 if ($updated || true) { // Always report success since the state may already be correct
1148 wp_send_json_success([
1149 'message' => $is_enabled
1150 ? sprintf(esc_html__('Field "%s" will be included in imports', 'mxchat'), $field_name)
1151 : sprintf(esc_html__('Field "%s" will be excluded from imports', 'mxchat'), $field_name)
1152 ]);
1153 } else {
1154 wp_send_json_error(['message' => esc_html__('Failed to save ACF field setting', 'mxchat')]);
1155 }
1156 return;
1157 }
1158
1159 // Handle custom post meta whitelist
1160 if ($name === 'mxchat_custom_meta_whitelist') {
1161 $updated = update_option('mxchat_custom_meta_whitelist', sanitize_textarea_field($value));
1162
1163 if ($updated || true) { // Always report success since the state may already be correct
1164 wp_send_json_success([
1165 'message' => esc_html__('Custom meta whitelist saved', 'mxchat')
1166 ]);
1167 } else {
1168 wp_send_json_error(['message' => esc_html__('Failed to save custom meta whitelist', 'mxchat')]);
1169 }
1170 return;
1171 }
1172
1173 // Handle other prompts options (autoload false — can hold the Pinecone secret)
1174 $options = get_option('mxchat_prompts_options', []);
1175 $options[$name] = $value;
1176 $updated = update_option('mxchat_prompts_options', $options, false);
1177
1178 if ($updated) {
1179 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
1180 } else {
1181 wp_send_json_error(['message' => esc_html__('No changes detected', 'mxchat')]);
1182 }
1183 }
1184
1185
1186 /**
1187 * Handles AJAX request for Pinecone settings migration
1188 */
1189 public function ajax_migrate_pinecone_settings() {
1190 // Verify nonce
1191 if (!wp_verify_nonce($_POST['_ajax_nonce'] ?? '', 'mxchat_save_setting_nonce')) {
1192 wp_send_json_error('Invalid nonce');
1193 }
1194
1195 // Check permissions
1196 if (!current_user_can('manage_options')) {
1197 wp_send_json_error('Unauthorized access');
1198 }
1199
1200 // Check if old Pinecone addon options exist
1201 $old_options = get_option('mxchat_pinecone_addon_options', array());
1202
1203 if (empty($old_options)) {
1204 wp_send_json_success(array('migrated' => false, 'message' => 'No old settings found'));
1205 }
1206
1207 // Get current core plugin options
1208 $current_options = get_option('mxchat_pinecone_addon_options', array());
1209
1210 // Only migrate if core options are empty or if explicitly requested
1211 $should_migrate = empty($current_options) ||
1212 (empty($current_options['mxchat_pinecone_api_key']) && !empty($old_options['mxchat_pinecone_api_key']));
1213
1214 if ($should_migrate) {
1215 // Migrate settings with proper sanitization
1216 $migrated_options = array(
1217 'mxchat_use_pinecone' => $old_options['mxchat_use_pinecone'] ?? '0',
1218 'mxchat_pinecone_api_key' => sanitize_text_field($old_options['mxchat_pinecone_api_key'] ?? ''),
1219 'mxchat_pinecone_host' => sanitize_text_field($old_options['mxchat_pinecone_host'] ?? ''),
1220 'mxchat_pinecone_index' => sanitize_text_field($old_options['mxchat_pinecone_index'] ?? ''),
1221 'mxchat_pinecone_environment' => sanitize_text_field($old_options['mxchat_pinecone_environment'] ?? '')
1222 );
1223
1224 update_option('mxchat_pinecone_addon_options', $migrated_options, false);
1225
1226 wp_send_json_success(array(
1227 'migrated' => true,
1228 'message' => 'Settings migrated successfully from Pinecone add-on'
1229 ));
1230 } else {
1231 wp_send_json_success(array(
1232 'migrated' => false,
1233 'message' => 'Settings already exist in core plugin'
1234 ));
1235 }
1236 }
1237
1238
1239 // ========================================
1240 // LICENSE AJAX HANDLERS
1241 // ========================================
1242
1243 /**
1244 * Validates and activates chat license via AJAX
1245 */
1246 public function mxchat_handle_activate_license() {
1247 // Check nonce
1248 if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
1249 wp_send_json_error(esc_html__('Invalid security token', 'mxchat'));
1250 return;
1251 }
1252
1253 // Verify user capabilities
1254 if (!current_user_can('manage_options')) {
1255 wp_send_json_error(esc_html__('Unauthorized access', 'mxchat'));
1256 return;
1257 }
1258
1259 $license_key = isset($_POST['mxchat_activation_key']) ? sanitize_text_field($_POST['mxchat_activation_key']) : '';
1260 $customer_email = isset($_POST['mxchat_pro_email']) ? sanitize_email($_POST['mxchat_pro_email']) : '';
1261
1262 if (empty($license_key) || empty($customer_email)) {
1263 wp_send_json_error(esc_html__('Email or License Key is missing', 'mxchat'));
1264 return;
1265 }
1266
1267 $product_id = 'MxChatPRO';
1268 $domain = parse_url(home_url(), PHP_URL_HOST); // Get the current domain
1269
1270 // Call WooCommerce Software API for activation (not just validation)
1271 $response = wp_remote_get(
1272 add_query_arg(
1273 array(
1274 'wc-api' => 'software-api',
1275 'request' => 'activation',
1276 'email' => $customer_email,
1277 'license_key' => $license_key,
1278 'product_id' => $product_id,
1279 'instance' => $domain, // THIS IS KEY - include the domain as instance
1280 'platform' => 'wordpress' // Optional but good to include
1281 ),
1282 'https://mxchat.ai/'
1283 ),
1284 array(
1285 'timeout' => 60,
1286 'sslverify' => true
1287 )
1288 );
1289
1290 if (is_wp_error($response)) {
1291 $error_message = $response->get_error_message();
1292 //error_log('MxChat License Activation Error: ' . $error_message);
1293 wp_send_json_error(esc_html__('Activation failed due to a server error: ', 'mxchat') . $error_message);
1294 return;
1295 }
1296
1297 $response_code = wp_remote_retrieve_response_code($response);
1298 $body = wp_remote_retrieve_body($response);
1299
1300 // Log response for debugging
1301 //error_log('MxChat License Response Code: ' . $response_code);
1302 //error_log('MxChat License Response Body: ' . $body);
1303
1304 if ($response_code !== 200) {
1305 wp_send_json_error(esc_html__('Server returned error code: ', 'mxchat') . $response_code);
1306 return;
1307 }
1308
1309 $data = json_decode($body);
1310
1311 if ($data && isset($data->activated) && $data->activated) {
1312 // Success - save local options
1313 update_option('mxchat_license_status', 'active');
1314 update_option('mxchat_pro_email', $customer_email);
1315 update_option('mxchat_activation_key', $license_key);
1316 delete_option('mxchat_license_error');
1317
1318 // Also track on your website (this is your existing domain tracking)
1319 $this->track_domain_on_website($license_key, $customer_email, $domain);
1320
1321 wp_send_json_success(array('message' => esc_html__('License activated successfully', 'mxchat')));
1322 } else {
1323 $error_message = isset($data->error) ? $data->error : esc_html__('Activation failed', 'mxchat');
1324 update_option('mxchat_license_status', 'inactive');
1325 update_option('mxchat_license_error', $error_message);
1326
1327 //error_log('MxChat Activation failed: ' . $error_message);
1328 wp_send_json_error($error_message);
1329 }
1330 }
1331
1332 /**
1333 * Track domain on your website (separate from WooCommerce activation)
1334 */
1335 private function track_domain_on_website($license_key, $email, $domain) {
1336 // This calls your website's tracking API
1337 wp_remote_post('https://mxchat.ai/mxchat-api/activate-license', array(
1338 'body' => array(
1339 'mxchat_pro_email' => $email,
1340 'mxchat_activation_key' => $license_key,
1341 'domain' => $domain
1342 ),
1343 'timeout' => 10,
1344 'sslverify' => true
1345 ));
1346 }
1347
1348 /**
1349 * Validates license via AJAX with email and key
1350 */
1351 public function mxchat_check_license_status() {
1352 // Verify nonce
1353 if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
1354 wp_send_json_error('Security check failed');
1355 return;
1356 }
1357
1358 // Add isset checks for safety
1359 $email = isset($_POST['email']) ? sanitize_email($_POST['email']) : '';
1360 $key = isset($_POST['key']) ? sanitize_text_field($_POST['key']) : '';
1361
1362 // Check if this license is actually active in your system
1363 $is_active = (get_option('mxchat_license_status') === 'active' &&
1364 get_option('mxchat_pro_email') === $email &&
1365 get_option('mxchat_activation_key') === $key);
1366
1367 wp_send_json(array(
1368 'is_active' => $is_active
1369 ));
1370 }
1371
1372 /**
1373 * Handle license deactivation - Complete version for plugin
1374 */
1375 function mxchat_deactivate_license() {
1376 // Add debugging
1377 //error_log('MxChat deactivate function called');
1378
1379 // Check nonce
1380 if (!check_ajax_referer('mxchat_activate_license_nonce', 'security', false)) {
1381 //error_log('MxChat deactivate: Nonce check failed');
1382 wp_send_json_error('Security check failed.');
1383 return;
1384 }
1385
1386 // plan-mxchat-20260731-c63fb6 — nonce is not authorization. Without this,
1387 // any authenticated user holding the nonce could revoke the site's PRO
1388 // licence. Every sibling handler in this file already checks.
1389 if (!current_user_can('manage_options')) {
1390 wp_send_json_error(esc_html__('Unauthorized', 'mxchat'), 403);
1391 return;
1392 }
1393
1394 //error_log('MxChat deactivate: Nonce check passed');
1395
1396 $license_key = get_option('mxchat_activation_key');
1397 $email = get_option('mxchat_pro_email');
1398 $domain = parse_url(home_url(), PHP_URL_HOST);
1399
1400 //error_log('MxChat deactivate: License: ' . $license_key . ', Email: ' . $email . ', Domain: ' . $domain);
1401
1402 if (empty($license_key) || empty($email)) {
1403 //error_log('MxChat deactivate: No active license found');
1404 wp_send_json_error('No active license found.');
1405 return;
1406 }
1407
1408 // Clear local license data first
1409 delete_option('mxchat_license_status');
1410 delete_option('mxchat_pro_email');
1411 delete_option('mxchat_activation_key');
1412 delete_option('mxchat_license_error');
1413
1414 //error_log('MxChat deactivate: Local data cleared');
1415
1416 // Notify your website's API to properly deactivate
1417 $response = wp_remote_post('https://mxchat.ai/mxchat-api/deactivate-license', array(
1418 'body' => array(
1419 'license_key' => $license_key,
1420 'email' => $email,
1421 'domain' => $domain
1422 ),
1423 'timeout' => 15,
1424 'sslverify' => true
1425 ));
1426
1427 if (is_wp_error($response)) {
1428 //error_log('MxChat deactivate: Server error - ' . $response->get_error_message());
1429 wp_send_json_success(array(
1430 'message' => 'License deactivated locally. Server could not be contacted to free activation slot.',
1431 'server_notified' => false
1432 ));
1433 return;
1434 }
1435
1436 $response_body = wp_remote_retrieve_body($response);
1437 $response_data = json_decode($response_body, true);
1438
1439 //error_log('MxChat deactivate: Server response - ' . $response_body);
1440
1441 if (isset($response_data['success']) && $response_data['success']) {
1442 //error_log('MxChat deactivate: Success with server notification');
1443 wp_send_json_success(array(
1444 'message' => 'License deactivated successfully. Activation slot has been freed up.',
1445 'server_notified' => true
1446 ));
1447 } else {
1448 //error_log('MxChat deactivate: Server responded but deactivation may have failed');
1449 wp_send_json_success(array(
1450 'message' => 'License deactivated locally. Please check your account dashboard to verify the activation was freed.',
1451 'server_notified' => false
1452 ));
1453 }
1454 }
1455
1456
1457 // ========================================
1458 // ACTIONS & INTENTS AJAX HANDLERS
1459 // ========================================
1460
1461 /**
1462 * Validates nonce and returns JSON error on failure
1463 */
1464 public function mxchat_toggle_action() {
1465 // Check nonce
1466 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mxchat_actions_nonce')) {
1467 wp_send_json_error(array('message' => 'Security check failed'));
1468 return;
1469 }
1470
1471 // Check permissions
1472 if (!current_user_can('manage_options')) {
1473 wp_send_json_error(array('message' => 'Permission denied'));
1474 return;
1475 }
1476
1477 // Validate params
1478 $intent_id = isset($_POST['intent_id']) ? intval($_POST['intent_id']) : 0;
1479 $enabled = isset($_POST['enabled']) ? (bool)$_POST['enabled'] : false;
1480
1481 if (!$intent_id) {
1482 wp_send_json_error(array('message' => 'Invalid action ID'));
1483 return;
1484 }
1485
1486 // Update the intent/action status in the database
1487 global $wpdb;
1488 $table_name = $wpdb->prefix . 'mxchat_intents';
1489
1490 // Using the 'enabled' field - add this field if it doesn't exist
1491 $result = $wpdb->update(
1492 $table_name,
1493 array('enabled' => $enabled ? 1 : 0),
1494 array('id' => $intent_id),
1495 array('%d'),
1496 array('%d')
1497 );
1498
1499 if ($result === false) {
1500 wp_send_json_error(array('message' => 'Database error'));
1501 return;
1502 }
1503
1504 wp_send_json_success();
1505 }
1506
1507
1508 /**
1509 * Validates permissions for AJAX request handling
1510 */
1511 public function mxchat_update_intent_threshold() {
1512 // Check permissions
1513 if (!current_user_can('manage_options')) {
1514 if (wp_doing_ajax()) {
1515 wp_send_json_error(array('message' => 'Unauthorized user'));
1516 return;
1517 }
1518 wp_die(esc_html__('Unauthorized user', 'mxchat'));
1519 }
1520
1521 // Verify nonce
1522 check_admin_referer('mxchat_update_intent_threshold_nonce');
1523
1524 // Process the update if we have valid data
1525 if (isset($_POST['intent_id'], $_POST['intent_threshold'])) {
1526 global $wpdb;
1527 $table_name = $wpdb->prefix . 'mxchat_intents';
1528 $intent_id = intval($_POST['intent_id']);
1529 $threshold_percentage = max(70, min(95, intval($_POST['intent_threshold'])));
1530 $similarity_threshold = $threshold_percentage / 100;
1531
1532 $result = $wpdb->update(
1533 $table_name,
1534 ['similarity_threshold' => $similarity_threshold],
1535 ['id' => $intent_id],
1536 ['%f'],
1537 ['%d']
1538 );
1539
1540 // Handle AJAX requests
1541 if (wp_doing_ajax()) {
1542 if ($result === false) {
1543 wp_send_json_error(array('message' => 'Failed to update threshold'));
1544 } else {
1545 wp_send_json_success(array('threshold' => $threshold_percentage));
1546 }
1547 return;
1548 }
1549 }
1550
1551 // Redirect for regular form submissions
1552 wp_safe_redirect(admin_url('admin.php?page=mxchat-actions&updated=true'));
1553 exit;
1554 }
1555
1556 // ========================================
1557 // HELPER METHODS
1558 // ========================================
1559
1560 /**
1561 * Returns a specific nonce action string
1562 */
1563 private function mxchat_get_nonce_action() {
1564 return 'mxchat_license_nonce';
1565 }
1566
1567 /**
1568 * Check API key status for all providers
1569 */
1570 public function mxchat_check_api_keys() {
1571 // Check nonce
1572 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'mxchat_save_setting_nonce')) {
1573 wp_send_json_error('Invalid nonce');
1574 }
1575
1576 // Check permissions
1577 if (!current_user_can('manage_options')) {
1578 wp_send_json_error('Unauthorized');
1579 }
1580
1581 // Get current options
1582 $options = get_option('mxchat_options', array());
1583
1584 // Check which API keys are present
1585 $api_key_status = array(
1586 'openai' => !empty($options['api_key']),
1587 'claude' => !empty($options['claude_api_key']),
1588 'xai' => !empty($options['xai_api_key']),
1589 'deepseek' => !empty($options['deepseek_api_key']),
1590 'gemini' => !empty($options['gemini_api_key']),
1591 'openrouter' => !empty($options['openrouter_api_key']),
1592 'voyage' => !empty($options['voyage_api_key'])
1593 );
1594
1595 wp_send_json_success($api_key_status);
1596 }
1597
1598 // ========================================
1599 // DEBUG & OPTIMIZATION AJAX HANDLERS
1600 // ========================================
1601
1602 /**
1603 * Toggle debug mode on/off
1604 */
1605 public function mxchat_toggle_debug_mode_callback() {
1606 // Verify nonce
1607 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1608 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1609 }
1610
1611 // Check permissions
1612 if ( ! current_user_can( 'manage_options' ) ) {
1613 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1614 }
1615
1616 $enabled = isset( $_POST['enabled'] ) && $_POST['enabled'] === 'on';
1617
1618 $options = get_option( 'mxchat_options', array() );
1619
1620 if ( $enabled ) {
1621 $options['debug_mode'] = 'on';
1622 update_option( 'mxchat_options', $options );
1623 MxChat_Admin::mxchat_log_debug( 'debug_mode', 'Debug mode enabled' );
1624 } else {
1625 // Log before disabling
1626 MxChat_Admin::mxchat_log_debug( 'debug_mode', 'Debug mode disabled' );
1627 $options['debug_mode'] = 'off';
1628 update_option( 'mxchat_options', $options );
1629 }
1630
1631 wp_send_json_success( array(
1632 'message' => $enabled ? esc_html__( 'Debug mode enabled', 'mxchat' ) : esc_html__( 'Debug mode disabled', 'mxchat' ),
1633 'enabled' => $enabled,
1634 ) );
1635 }
1636
1637 /**
1638 * Get the debug log entries
1639 */
1640 public function mxchat_get_debug_log_callback() {
1641 // Verify nonce
1642 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1643 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1644 }
1645
1646 // Check permissions
1647 if ( ! current_user_can( 'manage_options' ) ) {
1648 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1649 }
1650
1651 $log = MxChat_Admin::mxchat_get_debug_log();
1652
1653 wp_send_json_success( array(
1654 'log' => $log,
1655 'count' => count( $log ),
1656 ) );
1657 }
1658
1659 /**
1660 * Clear the debug log
1661 */
1662 public function mxchat_clear_debug_log_callback() {
1663 // Verify nonce
1664 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1665 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1666 }
1667
1668 // Check permissions
1669 if ( ! current_user_can( 'manage_options' ) ) {
1670 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1671 }
1672
1673 MxChat_Admin::mxchat_clear_debug_log();
1674
1675 // Log that the log was cleared (this will be the first entry in the new log)
1676 MxChat_Admin::mxchat_log_debug( 'debug_log', 'Debug log cleared by user' );
1677
1678 wp_send_json_success( array( 'message' => esc_html__( 'Debug log cleared', 'mxchat' ) ) );
1679 }
1680
1681 /**
1682 * Export settings as JSON
1683 */
1684 public function mxchat_export_settings_callback() {
1685 // Verify nonce
1686 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1687 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1688 }
1689
1690 // Check permissions
1691 if ( ! current_user_can( 'manage_options' ) ) {
1692 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1693 }
1694
1695 $export = MxChat_Admin::mxchat_export_settings();
1696
1697 // Log the export
1698 MxChat_Admin::mxchat_log_debug( 'settings_export', 'Settings exported by user' );
1699
1700 wp_send_json_success( array(
1701 'settings' => $export,
1702 'filename' => 'mxchat-settings-' . gmdate( 'Y-m-d-His' ) . '.json',
1703 ) );
1704 }
1705
1706 /**
1707 * Reset all settings to defaults
1708 */
1709 public function mxchat_reset_all_settings_callback() {
1710 // Verify nonce
1711 if ( ! check_ajax_referer( 'mxchat_save_setting_nonce', '_ajax_nonce', false ) ) {
1712 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1713 }
1714
1715 // Check permissions
1716 if ( ! current_user_can( 'manage_options' ) ) {
1717 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1718 }
1719
1720 // Require confirmation code
1721 $confirmation = isset( $_POST['confirmation'] ) ? sanitize_text_field( wp_unslash( $_POST['confirmation'] ) ) : '';
1722
1723 if ( strtoupper( $confirmation ) !== 'RESET' ) {
1724 wp_send_json_error( array( 'message' => esc_html__( 'Invalid confirmation code. Please type RESET to confirm.', 'mxchat' ) ) );
1725 }
1726
1727 // Perform the reset
1728 MxChat_Admin::mxchat_reset_all_settings();
1729
1730 wp_send_json_success( array( 'message' => esc_html__( 'All settings have been reset to defaults. The page will reload.', 'mxchat' ) ) );
1731 }
1732
1733 /**
1734 * Reset the global rate-limit usage counter to zero on demand.
1735 *
1736 * Zeroes the WP option mxchat_chat_limit_<bot>_global that the integrator
1737 * increments per message, then returns a freshly-formatted readout string
1738 * so the settings page can update without a reload. Does NOT change any
1739 * enforcement config — purely clears the running counter.
1740 */
1741 public function mxchat_reset_global_rate_limit_callback() {
1742 // Verify nonce
1743 if ( ! check_ajax_referer( 'mxchat_reset_global_usage', '_ajax_nonce', false ) ) {
1744 wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1745 }
1746
1747 // Check permissions
1748 if ( ! current_user_can( 'manage_options' ) ) {
1749 wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1750 }
1751
1752 // Resolve the per-bot counter key the same way the integrator does.
1753 $bot_id = isset( $_POST['bot_id'] ) ? sanitize_key( wp_unslash( $_POST['bot_id'] ) ) : 'default';
1754 $safe_bot = preg_replace( '/[^a-zA-Z0-9_]/', '_', $bot_id );
1755 if ( $safe_bot === '' ) {
1756 $safe_bot = 'default';
1757 }
1758 $option_key = 'mxchat_chat_limit_' . $safe_bot . '_global';
1759
1760 $now = time();
1761 update_option( $option_key, array( 'count' => 0, 'timestamp' => $now ) );
1762
1763 // Recompute the display string so the front-end can update in place.
1764 $all_options = get_option( 'mxchat_options', array() );
1765 $global_cfg = isset( $all_options['rate_limits_global'] ) && is_array( $all_options['rate_limits_global'] )
1766 ? $all_options['rate_limits_global']
1767 : array();
1768 $limit_raw = isset( $global_cfg['limit'] ) ? (string) $global_cfg['limit'] : 'unlimited';
1769 // Defensive: if a raw __custom__ ever slips through, fall back to the custom value.
1770 if ( ! ctype_digit( $limit_raw ) && isset( $global_cfg['limit_custom'] ) && ctype_digit( (string) $global_cfg['limit_custom'] ) ) {
1771 $limit_raw = (string) $global_cfg['limit_custom'];
1772 }
1773 $timeframe = isset( $global_cfg['timeframe'] ) ? (string) $global_cfg['timeframe'] : 'daily';
1774 $windows = array( 'hourly' => 3600, 'daily' => 86400, 'weekly' => 604800, 'monthly' => 2592000 );
1775 $window = isset( $windows[ $timeframe ] ) ? $windows[ $timeframe ] : 86400;
1776 $reset_at = $now + $window;
1777 $limit_int = ctype_digit( $limit_raw ) ? (int) $limit_raw : 0;
1778
1779 $text = sprintf(
1780 /* translators: 1: used count, 2: limit, 3: remaining, 4: human-readable time until reset */
1781 esc_html__( '%1$s of %2$s used · %3$s left · resets in %4$s', 'mxchat' ),
1782 number_format_i18n( 0 ),
1783 number_format_i18n( $limit_int ),
1784 number_format_i18n( $limit_int ),
1785 human_time_diff( $now, $reset_at )
1786 );
1787
1788 wp_send_json_success( array(
1789 'count' => 0,
1790 'limit' => $limit_int,
1791 'left' => $limit_int,
1792 'reset_at' => $reset_at,
1793 'pct' => 0,
1794 'text' => $text,
1795 'message' => esc_html__( 'Usage counter reset.', 'mxchat' ),
1796 ) );
1797 }
1798
1799 }
1800
1801 // Initialize the AJAX handler
1802 new MxChat_Ajax_Handler();
1803