PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.8
MxChat – AI Chatbot & Content Generation for WordPress v3.2.8
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | admin/class-ajax-handler.php +221 -16 3.2.33.2.8 View file →
@@ -47,10 +47,86 @@
47 47 add_action('wp_ajax_mxchat_get_debug_log', array($this, 'mxchat_get_debug_log_callback'));
48 48 add_action('wp_ajax_mxchat_clear_debug_log', array($this, 'mxchat_clear_debug_log_callback'));
49 49 add_action('wp_ajax_mxchat_export_settings', array($this, 'mxchat_export_settings_callback'));
50 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'));
51 57 }
52 58
59 +/**
60 + * Test connection to a Custom (OpenAI-compatible) provider by hitting its /models endpoint
61 + * with whichever auth scheme the user configured. Reports model count or a clean error.
62 + */
63 +public function mxchat_test_custom_provider_callback() {
64 + check_ajax_referer('mxchat_test_custom_provider');
65 + if (!current_user_can('manage_options')) {
66 + wp_send_json_error(array('message' => esc_html__('Unauthorized', 'mxchat')));
67 + }
68 +
69 + $options = get_option('mxchat_options', array());
70 + $base_url = isset($options['custom_provider_base_url']) ? trim((string) $options['custom_provider_base_url']) : '';
71 + $api_key = isset($options['custom_provider_api_key']) ? trim((string) $options['custom_provider_api_key']) : '';
72 + $auth = isset($options['custom_provider_auth_scheme']) ? $options['custom_provider_auth_scheme'] : 'bearer';
73 + $api_version = isset($options['custom_provider_api_version']) ? trim((string) $options['custom_provider_api_version']) : '';
74 +
75 + if (empty($base_url)) {
76 + wp_send_json_error(array('message' => esc_html__('Base URL is empty. Save it first.', 'mxchat')));
77 + }
78 +
79 + $url = rtrim($base_url, '/') . '/models';
80 + if (!empty($api_version)) {
81 + $url = add_query_arg('api-version', $api_version, $url);
82 + }
83 +
84 + $headers = array('Content-Type' => 'application/json');
85 + if (!empty($api_key)) {
86 + if ($auth === 'api-key') {
87 + $headers['api-key'] = $api_key;
88 + } else {
89 + $headers['Authorization'] = 'Bearer ' . $api_key;
90 + }
91 + }
92 +
93 + $response = wp_remote_get($url, array(
94 + 'headers' => $headers,
95 + 'timeout' => 10,
96 + ));
97 +
98 + if (is_wp_error($response)) {
99 + wp_send_json_error(array('message' => sprintf(esc_html__('Network error: %s', 'mxchat'), esc_html($response->get_error_message()))));
100 + }
101 +
102 + $code = (int) wp_remote_retrieve_response_code($response);
103 + if ($code === 401 || $code === 403) {
104 + wp_send_json_error(array('message' => sprintf(esc_html__('Auth rejected (HTTP %d). Check API key and auth scheme.', 'mxchat'), $code)));
105 + }
106 + if ($code === 404) {
107 + wp_send_json_error(array('message' => esc_html__('Endpoint not found (HTTP 404). Check the Base URL.', 'mxchat')));
108 + }
109 + if ($code < 200 || $code >= 300) {
110 + wp_send_json_error(array('message' => sprintf(esc_html__('Upstream returned HTTP %d.', 'mxchat'), $code)));
111 + }
112 +
113 + $body = json_decode(wp_remote_retrieve_body($response), true);
114 + $count = 0;
115 + if (is_array($body)) {
116 + if (isset($body['data']) && is_array($body['data'])) {
117 + $count = count($body['data']);
118 + } elseif (isset($body['models']) && is_array($body['models'])) {
119 + $count = count($body['models']);
120 + }
121 + }
122 +
123 + wp_send_json_success(array(
124 + 'message' => sprintf(esc_html__('Connection OK — %d model(s) reported.', 'mxchat'), $count),
125 + 'count' => $count,
126 + ));
127 +}
128 +
53 129 // ========================================
54 130 // SETTINGS AJAX HANDLERS
55 131 // ========================================
56 132
@@ -101,20 +177,15 @@
101 177 //error_log('MXChat Save: Setting model to openrouter');
102 178 $options['model'] = 'openrouter';
103 179 } else {
104 180 //error_log('MXChat Save: Checking against whitelist');
105 - $allowed_models = array(
106 - 'gemini-3-pro-preview', 'gemini-3-flash-preview', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite',
107 - 'gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-1.5-pro', 'gemini-1.5-flash',
108 - 'grok-4-0709', 'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning', 'grok-3-beta', 'grok-3-fast-beta', 'grok-3-mini-beta',
109 - 'grok-3-mini-fast-beta', 'grok-2',
110 - 'deepseek-chat',
111 - 'claude-opus-4-6', 'claude-opus-4-5', 'claude-sonnet-4-6',
112 - 'claude-sonnet-4-5-20250929', 'claude-opus-4-1-20250805', 'claude-haiku-4-5-20251001',
113 - 'claude-opus-4-20250514', 'claude-sonnet-4-20250514',
114 - 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gpt-5.3-chat-latest',
115 - 'gpt-5.2', 'gpt-5.1-chat-latest', 'gpt-5.1-2025-11-13', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano',
116 - );
181 + // Catalog refactor (plan-d14e89): canonical allowlist lives in
182 + // includes/class-mxchat-model-catalog.php. A new chat model
183 + // added there is automatically accepted by autosave.
184 + if (!class_exists('MxChat_Model_Catalog')) {
185 + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-mxchat-model-catalog.php';
186 + }
187 + $allowed_models = MxChat_Model_Catalog::chat_model_ids();
117 188
118 189 //error_log('MXChat Save: in_array result: ' . (in_array($value, $allowed_models) ? 'YES' : 'NO'));
119 190
120 191 if (in_array($value, $allowed_models)) {
@@ -231,8 +302,12 @@
231 302 // Validate script loading strategy value
232 303 $allowed_strategies = array('default', 'defer', 'delay_1s', 'delay_3s', 'delay_5s', 'on_interaction');
233 304 $options[$field_name] = in_array($value, $allowed_strategies) ? $value : 'default';
234 305 break;
306 + case 'auto_retry_on_transient_error':
307 + // Boolean toggle — accept 1/0/on/off, default to '1' if any truthy value.
308 + $options[$field_name] = ($value === '1' || $value === 'on' || $value === 1 || $value === true) ? '1' : '0';
309 + break;
235 310 default:
236 311 // Handle transcripts options
237 312 if (strpos($name, 'mxchat_transcripts_options') !== false) {
238 313 // Extract field name from mxchat_transcripts_options[field_name]
@@ -300,8 +375,48 @@
300 375 wp_send_json_success(['message' => esc_html__('Setting saved', 'mxchat')]);
301 376 return;
302 377 }
303 378 }
379 + // Whole-chatbot global cap (sits in mxchat_options['rate_limits_global']).
380 + // Field names: mxchat_options[rate_limits_global][limit|timeframe|limit_custom]
381 + else if (strpos($name, 'mxchat_options[rate_limits_global]') !== false) {
382 + preg_match('/\[rate_limits_global\]\[(.*?)\]/', $name, $matches);
383 + if (isset($matches[1])) {
384 + $setting_key = $matches[1];
385 + if (!isset($options['rate_limits_global']) || !is_array($options['rate_limits_global'])) {
386 + $options['rate_limits_global'] = array('limit' => 'unlimited', 'timeframe' => 'daily');
387 + }
388 + if ($setting_key === 'limit') {
389 + // Selection from the preset dropdown. If __custom__, resolve from limit_custom; otherwise store directly.
390 + if ($value === '__custom__') {
391 + $custom = isset($options['rate_limits_global']['limit_custom']) ? (string) $options['rate_limits_global']['limit_custom'] : '';
392 + if ($custom !== '' && ctype_digit($custom) && (int) $custom >= 1) {
393 + $options['rate_limits_global']['limit'] = $custom;
394 + }
395 + // else leave existing limit untouched until the custom value arrives
396 + } else {
397 + $options['rate_limits_global']['limit'] = $value;
398 + }
399 + } elseif ($setting_key === 'limit_custom') {
400 + $clean = preg_replace('/[^0-9]/', '', (string) $value);
401 + $options['rate_limits_global']['limit_custom'] = $clean;
402 + // Mirror a valid custom value into limit UNCONDITIONALLY (plan-74eb86).
403 + // The custom number input is only editable when the dropdown is on
404 + // "Custom…" (the toggle JS hides it for presets/unlimited) and autosave
405 + // sends one field per change event, so a limit_custom change only fires
406 + // in custom mode — there is no preset to clobber. The old guard required
407 + // limit to already be non-preset, which it isn't on a first-time custom
408 + // entry (the limit=__custom__ event arrives before limit_custom is set),
409 + // so the value never landed in limit on the first save and reverted on refresh.
410 + if ($clean !== '' && (int) $clean >= 1) {
411 + $options['rate_limits_global']['limit'] = $clean;
412 + }
413 + } elseif ($setting_key === 'timeframe') {
414 + $allowed_tf = array('hourly','daily','weekly','monthly');
415 + $options['rate_limits_global']['timeframe'] = in_array($value, $allowed_tf, true) ? $value : 'daily';
416 + }
417 + }
418 + }
304 419 // First check for rate limits settings
305 420 else if (strpos($name, 'mxchat_options[rate_limits]') !== false) {
306 421 //error_log('MXChat Save: Detected rate_limits field: ' . $name);
307 422
@@ -310,9 +425,9 @@
310 425 //error_log('MXChat Save: Regex matches: ' . print_r($matches, true));
311 426
312 427 if (isset($matches[1]) && isset($matches[2])) {
313 428 $role_id = $matches[1];
314 - $setting_key = $matches[2]; // limit, timeframe, or message
429 + $setting_key = $matches[2]; // limit, timeframe, message, or limit_custom
315 430
316 431 //error_log('MXChat Save: Role ID = ' . $role_id . ', Setting Key = ' . $setting_key);
317 432
318 433 // Initialize rate_limits if it doesn't exist
@@ -330,10 +445,32 @@
330 445 'message' => 'Rate limit exceeded. Please try again later.'
331 446 ];
332 447 }
333 448
334 - // Update the specific setting
335 - $options['rate_limits'][$role_id][$setting_key] = $value;
449 + if ($setting_key === 'limit') {
450 + if ($value === '__custom__') {
451 + // Pull the integer from limit_custom that may have arrived (or will arrive).
452 + $custom = isset($options['rate_limits'][$role_id]['limit_custom']) ? (string) $options['rate_limits'][$role_id]['limit_custom'] : '';
453 + if ($custom !== '' && ctype_digit($custom) && (int) $custom >= 1) {
454 + $options['rate_limits'][$role_id]['limit'] = $custom;
455 + }
456 + } else {
457 + $options['rate_limits'][$role_id]['limit'] = $value;
458 + }
459 + } elseif ($setting_key === 'limit_custom') {
460 + $clean = preg_replace('/[^0-9]/', '', (string) $value);
461 + $options['rate_limits'][$role_id]['limit_custom'] = $clean;
462 + // Mirror a valid custom value into limit UNCONDITIONALLY — same reasoning
463 + // as the global branch above (plan-74eb86). The per-role custom input is
464 + // only editable in custom mode and autosave is one-field-per-change, so
465 + // this never clobbers a preset; it fixes the first-time-save revert.
466 + if ($clean !== '' && (int) $clean >= 1) {
467 + $options['rate_limits'][$role_id]['limit'] = $clean;
468 + }
469 + } else {
470 + // Update the specific setting (timeframe, message)
471 + $options['rate_limits'][$role_id][$setting_key] = $value;
472 + }
336 473 //error_log('MXChat Save: Updated rate_limits[' . $role_id . '][' . $setting_key . '] = ' . $value);
337 474 } else {
338 475 //error_log('MXChat Save: Failed to parse rate_limits pattern: ' . $name);
339 476 }
@@ -370,9 +507,11 @@
370 507 'enable_streaming_toggle',
371 508 'contextual_awareness_toggle',
372 509 'citation_links_toggle',
373 510 'enable_email_block',
374 - 'enable_name_field'
511 + 'enable_name_field',
512 + 'custom_provider_for_embeddings',
513 + 'custom_provider_for_images'
375 514 ])) {
376 515 //error_log('MXChat Save: Processing toggle: ' . $field_name);
377 516 $options[$field_name] = ($value === 'on') ? 'on' : 'off';
378 517 } else {
@@ -1269,8 +1408,74 @@
1269 1408 // Perform the reset
1270 1409 MxChat_Admin::mxchat_reset_all_settings();
1271 1410
1272 1411 wp_send_json_success( array( 'message' => esc_html__( 'All settings have been reset to defaults. The page will reload.', 'mxchat' ) ) );
1412 + }
1413 +
1414 + /**
1415 + * Reset the global rate-limit usage counter to zero on demand.
1416 + *
1417 + * Zeroes the WP option mxchat_chat_limit_<bot>_global that the integrator
1418 + * increments per message, then returns a freshly-formatted readout string
1419 + * so the settings page can update without a reload. Does NOT change any
1420 + * enforcement config — purely clears the running counter.
1421 + */
1422 + public function mxchat_reset_global_rate_limit_callback() {
1423 + // Verify nonce
1424 + if ( ! check_ajax_referer( 'mxchat_reset_global_usage', '_ajax_nonce', false ) ) {
1425 + wp_send_json_error( array( 'message' => esc_html__( 'Security check failed', 'mxchat' ) ) );
1426 + }
1427 +
1428 + // Check permissions
1429 + if ( ! current_user_can( 'manage_options' ) ) {
1430 + wp_send_json_error( array( 'message' => esc_html__( 'Unauthorized', 'mxchat' ) ) );
1431 + }
1432 +
1433 + // Resolve the per-bot counter key the same way the integrator does.
1434 + $bot_id = isset( $_POST['bot_id'] ) ? sanitize_key( wp_unslash( $_POST['bot_id'] ) ) : 'default';
1435 + $safe_bot = preg_replace( '/[^a-zA-Z0-9_]/', '_', $bot_id );
1436 + if ( $safe_bot === '' ) {
1437 + $safe_bot = 'default';
1438 + }
1439 + $option_key = 'mxchat_chat_limit_' . $safe_bot . '_global';
1440 +
1441 + $now = time();
1442 + update_option( $option_key, array( 'count' => 0, 'timestamp' => $now ) );
1443 +
1444 + // Recompute the display string so the front-end can update in place.
1445 + $all_options = get_option( 'mxchat_options', array() );
1446 + $global_cfg = isset( $all_options['rate_limits_global'] ) && is_array( $all_options['rate_limits_global'] )
1447 + ? $all_options['rate_limits_global']
1448 + : array();
1449 + $limit_raw = isset( $global_cfg['limit'] ) ? (string) $global_cfg['limit'] : 'unlimited';
1450 + // Defensive: if a raw __custom__ ever slips through, fall back to the custom value.
1451 + if ( ! ctype_digit( $limit_raw ) && isset( $global_cfg['limit_custom'] ) && ctype_digit( (string) $global_cfg['limit_custom'] ) ) {
1452 + $limit_raw = (string) $global_cfg['limit_custom'];
1453 + }
1454 + $timeframe = isset( $global_cfg['timeframe'] ) ? (string) $global_cfg['timeframe'] : 'daily';
1455 + $windows = array( 'hourly' => 3600, 'daily' => 86400, 'weekly' => 604800, 'monthly' => 2592000 );
1456 + $window = isset( $windows[ $timeframe ] ) ? $windows[ $timeframe ] : 86400;
1457 + $reset_at = $now + $window;
1458 + $limit_int = ctype_digit( $limit_raw ) ? (int) $limit_raw : 0;
1459 +
1460 + $text = sprintf(
1461 + /* translators: 1: used count, 2: limit, 3: remaining, 4: human-readable time until reset */
1462 + esc_html__( '%1$s of %2$s used · %3$s left · resets in %4$s', 'mxchat' ),
1463 + number_format_i18n( 0 ),
1464 + number_format_i18n( $limit_int ),
1465 + number_format_i18n( $limit_int ),
1466 + human_time_diff( $now, $reset_at )
1467 + );
1468 +
1469 + wp_send_json_success( array(
1470 + 'count' => 0,
1471 + 'limit' => $limit_int,
1472 + 'left' => $limit_int,
1473 + 'reset_at' => $reset_at,
1474 + 'pct' => 0,
1475 + 'text' => $text,
1476 + 'message' => esc_html__( 'Usage counter reset.', 'mxchat' ),
1477 + ) );
1273 1478 }
1274 1479
1275 1480 }
1276 1481