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

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