PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.7
MxChat – AI Chatbot & Content Generation for WordPress v3.2.7
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 | mxchat-basic.php +253 -47 3.2.33.2.7 View file →
@@ -2,9 +2,9 @@
2 2 /**
3 3 * Plugin Name: MxChat
4 4 * Plugin URI: https://mxchat.ai/
5 5 * Description: AI chatbot for WordPress with OpenAI, Claude, xAI, DeepSeek, live agent, PDF uploads, WooCommerce, and training on website data.
6 - * Version: 3.2.3
6 + * Version: 3.2.7
7 7 * Author: MxChat
8 8 * Author URI: https://mxchat.ai
9 9 * License: GPLv2 or later
10 10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
@@ -46,8 +46,51 @@
46 46 }
47 47 add_action('init', 'mxchat_load_textdomain');
48 48
49 49 /**
50 + * One-time migration: gemini-3-pro-preview was shut down by Google on March 9, 2026.
51 + * Existing installs with the dead ID get auto-remapped to gemini-3.1-pro-preview
52 + * (Google's official migration target) the first time admin_init fires after update.
53 + */
54 +add_action('admin_init', function () {
55 + if (get_option('mxchat_gemini_3_remap_done')) {
56 + return;
57 + }
58 + $opts = get_option('mxchat_options');
59 + if (is_array($opts) && isset($opts['model']) && $opts['model'] === 'gemini-3-pro-preview') {
60 + $opts['model'] = 'gemini-3.1-pro-preview';
61 + update_option('mxchat_options', $opts);
62 + }
63 + if (is_array($opts) && isset($opts['content_model']) && $opts['content_model'] === 'gemini-3-pro-preview') {
64 + $opts['content_model'] = 'gemini-3.1-pro-preview';
65 + update_option('mxchat_options', $opts);
66 + }
67 + update_option('mxchat_gemini_3_remap_done', 1);
68 +});
69 +
70 +/**
71 + * One-time migration: the Grok 2 family was retired by xAI (grok-2, grok-2-1212,
72 + * grok-2-latest, grok-2-vision-1212 all return 400 "Model not found"). Existing
73 + * installs with the dead ID get auto-remapped to grok-4-1-fast-non-reasoning
74 + * (modern, fast, broadly available) the first time admin_init fires after update.
75 + */
76 +add_action('admin_init', function () {
77 + if (get_option('mxchat_grok_2_remap_done')) {
78 + return;
79 + }
80 + $opts = get_option('mxchat_options');
81 + if (is_array($opts) && isset($opts['model']) && $opts['model'] === 'grok-2') {
82 + $opts['model'] = 'grok-4-1-fast-non-reasoning';
83 + update_option('mxchat_options', $opts);
84 + }
85 + if (is_array($opts) && isset($opts['content_model']) && $opts['content_model'] === 'grok-2') {
86 + $opts['content_model'] = 'grok-4-1-fast-non-reasoning';
87 + update_option('mxchat_options', $opts);
88 + }
89 + update_option('mxchat_grok_2_remap_done', 1);
90 +});
91 +
92 +/**
50 93 * Exclude MxChat assets from caching plugin optimizations
51 94 *
52 95 * This prevents issues with WP Rocket, LiteSpeed Cache, Autoptimize, WP Super Cache,
53 96 * W3 Total Cache, SG Optimizer, and similar plugins that may break the chatbot by
@@ -250,8 +293,61 @@
250 293 $rejected[] = 'wp-admin/admin-ajax.php';
251 294 return $rejected;
252 295 });
253 296
297 +// ── Page-cache bypass for chat AJAX (companion to the 3.2.6 nonce-race hotfix)
298 +// Each cache plugin gets its own filter export so that visitors hitting an
299 +// edge-cached page never receive cached chat-AJAX responses. The chat send /
300 +// stream send / file upload all POST to /wp-admin/admin-ajax.php with
301 +// `action=mxchat_*`. Without these exports, a cache plugin can stale a response
302 +// and break the per-session nonce flow on the first message.
303 +
304 +// WP Rocket — `rocket_cache_reject_uri` takes a flat array of regex strings.
305 +add_filter('rocket_cache_reject_uri', function($uris) {
306 + if (!is_array($uris)) $uris = array();
307 + $uris[] = '/wp-admin/admin-ajax\.php\?action=mxchat_.*';
308 + return $uris;
309 +});
310 +
311 +// LiteSpeed Cache — `litespeed_cache_no_cache_for_request` short-circuits
312 +// caching when the request matches our chat-AJAX pattern.
313 +add_filter('litespeed_cache_no_cache_for_request', function($no_cache) {
314 + if ($no_cache) return $no_cache;
315 + if (!empty($_SERVER['REQUEST_URI']) &&
316 + strpos($_SERVER['REQUEST_URI'], '/wp-admin/admin-ajax.php') !== false &&
317 + !empty($_REQUEST['action']) &&
318 + strpos((string) $_REQUEST['action'], 'mxchat_') === 0) {
319 + return true;
320 + }
321 + return $no_cache;
322 +});
323 +
324 +// W3 Total Cache — `w3tc_pgcache_request_skip_uri` flips page-cache off when
325 +// the URI matches.
326 +add_filter('w3tc_pgcache_request_skip_uri', function($skip) {
327 + if ($skip) return $skip;
328 + if (!empty($_SERVER['REQUEST_URI']) &&
329 + strpos($_SERVER['REQUEST_URI'], '/wp-admin/admin-ajax.php') !== false &&
330 + !empty($_REQUEST['action']) &&
331 + strpos((string) $_REQUEST['action'], 'mxchat_') === 0) {
332 + return true;
333 + }
334 + return $skip;
335 +});
336 +
337 +// FlyingPress — `flying_press_cacheable` takes a boolean and is run per
338 +// request. Same pattern as LiteSpeed / W3TC.
339 +add_filter('flying_press_cacheable', function($cacheable) {
340 + if (!$cacheable) return $cacheable;
341 + if (!empty($_SERVER['REQUEST_URI']) &&
342 + strpos($_SERVER['REQUEST_URI'], '/wp-admin/admin-ajax.php') !== false &&
343 + !empty($_REQUEST['action']) &&
344 + strpos((string) $_REQUEST['action'], 'mxchat_') === 0) {
345 + return false;
346 + }
347 + return $cacheable;
348 +});
349 +
254 350 // Include classes with error handling
255 351 function mxchat_include_classes() {
256 352 $class_files = array(
257 353 'includes/class-mxchat-integrator.php',
@@ -262,8 +358,9 @@
262 358 'includes/class-mxchat-meta-box.php',
263 359 'includes/class-mxchat-chunker.php',
264 360 'includes/class-mxchat-word-handler.php',
265 361 'includes/class-mxchat-content-generator.php',
362 + 'includes/class-rest-api.php',
266 363 'admin/class-ajax-handler.php',
267 364 'admin/class-pinecone-manager.php',
268 365 'admin/class-knowledge-manager.php'
269 366 );
@@ -275,8 +372,26 @@
275 372 } else {
276 373 //error_log('MxChat: Missing class file - ' . $file);
277 374 }
278 375 }
376 +
377 + // Admin pages that aren't classes (procedural include).
378 + if (is_admin()) {
379 + $admin_api_page = plugin_dir_path(__FILE__) . 'includes/admin-api-page.php';
380 + if (file_exists($admin_api_page)) {
381 + require_once $admin_api_page;
382 + }
383 + // f7c7d4 renamed this file admin-dashboard-page.php → admin-onboarding-page.php.
384 + // The require MUST live here (admin bootstrap) and not just inside
385 + // mxchat_add_plugin_page() on the admin_menu hook — admin_menu does NOT
386 + // fire on admin-ajax.php requests, so the wizard's AJAX handlers
387 + // (plan-905439: mxchat_onboarding_kb_status / save_step / mark_step /
388 + // auto_graduate + the f7c7d4 dismiss handler) would never register.
389 + $admin_onboarding_page = plugin_dir_path(__FILE__) . 'includes/admin-onboarding-page.php';
390 + if (file_exists($admin_onboarding_page)) {
391 + require_once $admin_onboarding_page;
392 + }
393 + }
279 394 }
280 395
281 396 /**
282 397 * Lazy-load the PDF parser library only when needed.
@@ -608,51 +723,30 @@
608 723 update_option('mxchat_content_type_migration_version', '2.5.6');
609 724 }
610 725
611 726 /**
612 - * 3.2.3: Add embedding_model column to KB and intent tables so we can detect
613 - * when the active embedding model is changed while content is already embedded.
727 + * 3.2.4: Backfill the active embedding model option for installs that already
728 + * have KB content but no stamped model. The mismatch warning compares this
729 + * against the user's currently selected model — no per-row column needed.
614 730 */
615 -function mxchat_add_embedding_model_columns() {
731 +function mxchat_backfill_active_embedding_model() {
616 732 global $wpdb;
617 733
618 - $migration_version = get_option('mxchat_embedding_model_migration_version', '0');
619 - if (version_compare($migration_version, '3.2.3', '>=')) {
734 + if (get_option('mxchat_active_embedding_model', '') !== '') {
620 735 return;
621 736 }
622 737
623 - $tables = array(
624 - $wpdb->prefix . 'mxchat_system_prompt_content',
625 - $wpdb->prefix . 'mxchat_intents',
626 - $wpdb->prefix . 'mxchat_intent_phrases',
627 - );
628 -
629 - foreach ($tables as $table) {
630 - if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") !== $table) {
631 - continue;
632 - }
633 - $col_exists = $wpdb->get_results("SHOW COLUMNS FROM {$table} LIKE 'embedding_model'");
634 - if (empty($col_exists)) {
635 - $wpdb->query("ALTER TABLE {$table} ADD COLUMN embedding_model VARCHAR(64) DEFAULT NULL");
636 - }
738 + $kb_table = $wpdb->prefix . 'mxchat_system_prompt_content';
739 + if ($wpdb->get_var("SHOW TABLES LIKE '{$kb_table}'") !== $kb_table) {
740 + return;
637 741 }
638 742
639 - // Best-effort backfill: if any existing content lacks a stamped model, assume
640 - // it was embedded with whatever model is currently selected. This is the
641 - // safest assumption for installations upgrading to 3.2.3 — if it was wrong
642 - // already, the mismatch detection will surface it on the next switch.
643 - $options = get_option('mxchat_options', array());
644 - $current_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
645 -
646 - if (get_option('mxchat_active_embedding_model', '') === '') {
647 - $kb_table = $wpdb->prefix . 'mxchat_system_prompt_content';
648 - $kb_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$kb_table}");
649 - if ($kb_count > 0) {
650 - update_option('mxchat_active_embedding_model', $current_model, false);
651 - }
743 + $kb_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$kb_table}");
744 + if ($kb_count > 0) {
745 + $options = get_option('mxchat_options', array());
746 + $current_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
747 + update_option('mxchat_active_embedding_model', $current_model, false);
652 748 }
653 -
654 - update_option('mxchat_embedding_model_migration_version', '3.2.3');
655 749 }
656 750
657 751 /**
658 752 * 2.5.2: Create queue processing tables for reliable background processing
@@ -727,8 +821,34 @@
727 821 dbDelta($sql);
728 822 }
729 823
730 824 /**
825 + * Create per-session satisfaction ratings table (v3.2.6)
826 + * Stores one 👍/👎 rating + optional feedback per chat session.
827 + */
828 +function mxchat_create_session_ratings_table() {
829 + global $wpdb;
830 + $charset_collate = $wpdb->get_charset_collate();
831 +
832 + $table_name = $wpdb->prefix . 'mxchat_session_ratings';
833 + $sql = "CREATE TABLE $table_name (
834 + id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
835 + session_id varchar(255) NOT NULL,
836 + bot_id varchar(50) NOT NULL DEFAULT 'default',
837 + rating_value tinyint(1) NOT NULL,
838 + rating_feedback text DEFAULT NULL,
839 + created_at datetime NOT NULL,
840 + PRIMARY KEY (id),
841 + UNIQUE KEY session_id (session_id),
842 + KEY bot_id (bot_id),
843 + KEY created_at (created_at)
844 + ) $charset_collate;";
845 +
846 + require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
847 + dbDelta($sql);
848 +}
849 +
850 +/**
731 851 * 2.5.2: Fix URL column size to support long URLs (especially with UTF-8 encoding)
732 852 * This fixes "url, source_url. The supplied values may be too long" errors
733 853 */
734 854 function mxchat_fix_url_column_size() {
@@ -864,9 +984,8 @@
864 984 embedding_vector LONGTEXT,
865 985 source_url TEXT DEFAULT NULL,
866 986 role_restriction VARCHAR(50) DEFAULT 'public',
867 987 content_type VARCHAR(50) DEFAULT 'content',
868 - embedding_model VARCHAR(64) DEFAULT NULL,
869 988 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
870 989 PRIMARY KEY (id),
871 990 KEY content_type (content_type)
872 991 ) $charset_collate;";
@@ -881,9 +1000,8 @@
881 1000 callback_function VARCHAR(255) NOT NULL,
882 1001 similarity_threshold FLOAT DEFAULT 0.85,
883 1002 enabled TINYINT(1) NOT NULL DEFAULT 1,
884 1003 enabled_bots LONGTEXT DEFAULT NULL,
885 - embedding_model VARCHAR(64) DEFAULT NULL,
886 1004 PRIMARY KEY (id)
887 1005 ) $charset_collate;";
888 1006
889 1007 // Individual Intent Phrases Table - each phrase gets its own embedding vector
@@ -892,9 +1010,8 @@
892 1010 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
893 1011 intent_id BIGINT(20) UNSIGNED NOT NULL,
894 1012 phrase TEXT NOT NULL,
895 1013 embedding_vector LONGTEXT NOT NULL,
896 - embedding_model VARCHAR(64) DEFAULT NULL,
897 1014 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
898 1015 PRIMARY KEY (id),
899 1016 KEY intent_id (intent_id)
900 1017 ) $charset_collate;";
@@ -917,8 +1034,11 @@
917 1034
918 1035 // Create transcript translations table
919 1036 mxchat_create_translations_table();
920 1037
1038 + // Create per-session satisfaction ratings table (v3.2.6)
1039 + mxchat_create_session_ratings_table();
1040 +
921 1041 // Ensure additional columns in system prompt table
922 1042 $existing_system_columns = $wpdb->get_results("SHOW COLUMNS FROM $system_prompt_table");
923 1043 if (!empty($existing_system_columns)) {
924 1044 $existing_system_column_names = array_column($existing_system_columns, 'Field');
@@ -961,10 +1081,10 @@
961 1081
962 1082 // Run migration for existing installations
963 1083 mxchat_migrate_pinecone_roles_add_bot_id();
964 1084
965 - // 3.2.3: Add embedding_model column to KB and intent tables
966 - mxchat_add_embedding_model_columns();
1085 + // 3.2.4: Backfill active embedding model option (replaces 3.2.3 column-based tracking)
1086 + mxchat_backfill_active_embedding_model();
967 1087
968 1088 // Setup cron jobs
969 1089 mxchat_setup_cron_jobs();
970 1090
@@ -1000,13 +1120,14 @@
1000 1120 // Clear fallback flags if cron scheduling succeeded
1001 1121 delete_option('mxchat_use_fallback_rate_limits');
1002 1122 }
1003 1123
1004 - // Schedule transcript cleanup if configured
1124 + // Schedule transcript cleanup if configured (bucket dropdown OR custom retention-days > 0)
1005 1125 $transcript_options = get_option('mxchat_transcripts_options', array());
1006 1126 $cleanup_interval = isset($transcript_options['mxchat_auto_delete_transcripts']) ? $transcript_options['mxchat_auto_delete_transcripts'] : 'never';
1007 -
1008 - if ($cleanup_interval !== 'never') {
1127 + $custom_retention = isset($transcript_options['mxchat_retention_days']) ? (int) $transcript_options['mxchat_retention_days'] : 0;
1128 +
1129 + if ($cleanup_interval !== 'never' || $custom_retention > 0) {
1009 1130 // Check if not already scheduled
1010 1131 if (!wp_next_scheduled('mxchat_cleanup_old_transcripts')) {
1011 1132 // Schedule to run daily at 3 AM
1012 1133 $next_run = strtotime('tomorrow 3:00 AM');
@@ -1140,11 +1261,12 @@
1140 1261 delete_option('mxchat_email_null');
1141 1262 delete_option('mxchat_name_null');
1142 1263 }
1143 1264
1144 - // 3.2.3: Add embedding_model tracking columns and backfill active model
1145 - if (version_compare($current_version, '3.2.3', '<')) {
1146 - mxchat_add_embedding_model_columns();
1265 + // 3.2.4: Backfill active embedding model option for the warning UI
1266 + // (replaces the per-row column tracking from 3.2.3, which was reverted)
1267 + if (version_compare($current_version, '3.2.4', '<')) {
1268 + mxchat_backfill_active_embedding_model();
1147 1269 }
1148 1270
1149 1271 // Run full activation to ensure everything is up to date
1150 1272 mxchat_activate();
@@ -1371,8 +1493,16 @@
1371 1493 if (class_exists('MxChat_Content_Generator')) {
1372 1494 new MxChat_Content_Generator();
1373 1495 }
1374 1496
1497 + // Initialize REST API globally — endpoints must be registered on
1498 + // every request (admin and frontend) so they're reachable via /wp-json/.
1499 + // Endpoints are auth-gated and locked until the site owner generates
1500 + // a token in MxChat → API Access.
1501 + if (class_exists('MxChat_Rest_Api')) {
1502 + new MxChat_Rest_Api();
1503 + }
1504 +
1375 1505 // Initialize public classes
1376 1506 if (class_exists('MxChat_Public')) {
1377 1507 $mxchat_public = new MxChat_Public();
1378 1508 }
@@ -1417,11 +1547,25 @@
1417 1547
1418 1548 mxchat_migrate_pinecone_roles_add_bot_id();
1419 1549 mxchat_migrate_add_content_type_column();
1420 1550 mxchat_migrate_add_translations_table();
1551 + mxchat_migrate_add_session_ratings_table();
1421 1552 }
1422 1553
1423 1554 /**
1555 + * Migration: Create per-session satisfaction ratings table (v3.2.6)
1556 + * For users upgrading from versions before 3.2.6
1557 + */
1558 +function mxchat_migrate_add_session_ratings_table() {
1559 + $migration_key = 'mxchat_session_ratings_table_created';
1560 + if (get_option($migration_key)) {
1561 + return;
1562 + }
1563 + mxchat_create_session_ratings_table();
1564 + update_option($migration_key, '3.2.6');
1565 +}
1566 +
1567 +/**
1424 1568 * Migration: Create transcript translations table (v3.0.4)
1425 1569 * For users upgrading from versions before 3.0.4
1426 1570 */
1427 1571 function mxchat_migrate_add_translations_table() {
@@ -1464,5 +1608,67 @@
1464 1608 return $schedules;
1465 1609 });
1466 1610
1467 1611 // Register deactivation hook
1468 -register_deactivation_hook(__FILE__, 'mxchat_deactivate');
1612 +register_deactivation_hook(__FILE__, 'mxchat_deactivate');
1613 +
1614 +/**
1615 + * Per-session satisfaction rating: AJAX save handler (v3.2.6).
1616 + * Records one 👍/👎 + optional feedback per chat session. The UNIQUE KEY on
1617 + * session_id makes this naturally idempotent — only the first rating per
1618 + * session is stored; duplicate POSTs are silent no-ops.
1619 + */
1620 +function mxchat_save_session_rating() {
1621 + global $wpdb;
1622 +
1623 + $session_id = isset($_POST['session_id']) ? sanitize_text_field(wp_unslash($_POST['session_id'])) : '';
1624 + $bot_id = isset($_POST['bot_id']) ? sanitize_text_field(wp_unslash($_POST['bot_id'])) : 'default';
1625 + $rating_raw = isset($_POST['rating']) ? (int) $_POST['rating'] : 0;
1626 + $feedback = isset($_POST['feedback']) ? sanitize_textarea_field(wp_unslash($_POST['feedback'])) : '';
1627 +
1628 + if ($session_id === '' || ($rating_raw !== 1 && $rating_raw !== -1)) {
1629 + wp_send_json_error(array('message' => 'invalid_input'), 400);
1630 + }
1631 +
1632 + if (strlen($feedback) > 1000) {
1633 + $feedback = substr($feedback, 0, 1000);
1634 + }
1635 +
1636 + $table_name = $wpdb->prefix . 'mxchat_session_ratings';
1637 + $existing = $wpdb->get_var($wpdb->prepare(
1638 + "SELECT id FROM $table_name WHERE session_id = %s LIMIT 1",
1639 + $session_id
1640 + ));
1641 +
1642 + if ($existing) {
1643 + if ($feedback !== '') {
1644 + $wpdb->update(
1645 + $table_name,
1646 + array('rating_feedback' => $feedback),
1647 + array('id' => (int) $existing),
1648 + array('%s'),
1649 + array('%d')
1650 + );
1651 + }
1652 + wp_send_json_success(array('updated' => true));
1653 + }
1654 +
1655 + $inserted = $wpdb->insert(
1656 + $table_name,
1657 + array(
1658 + 'session_id' => $session_id,
1659 + 'bot_id' => $bot_id !== '' ? $bot_id : 'default',
1660 + 'rating_value' => $rating_raw,
1661 + 'rating_feedback' => $feedback !== '' ? $feedback : null,
1662 + 'created_at' => current_time('mysql'),
1663 + ),
1664 + array('%s', '%s', '%d', '%s', '%s')
1665 + );
1666 +
1667 + if ($inserted === false) {
1668 + wp_send_json_error(array('message' => 'db_insert_failed'), 500);
1669 + }
1670 +
1671 + wp_send_json_success(array('saved' => true));
1672 +}
1673 +add_action('wp_ajax_mxchat_save_rating', 'mxchat_save_session_rating');
1674 +add_action('wp_ajax_nopriv_mxchat_save_rating', 'mxchat_save_session_rating');