PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.23
MxChat – AI Chatbot & Content Generation for WordPress v3.2.23
3.2.25 3.2.24 3.2.23 3.2.22 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 All 156 releases
← All changes | mxchat-basic.php +382 -91 3.2.19 → 3.2.23 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.19
6 + * Version: 3.2.23
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
@@ -38,8 +38,20 @@
38 38 define('MXCHAT_VERSION', $version);
39 39 }
40 40
41 41 /**
42 + * Toolbar items API level (plan-mxchat-20260916-65c9b6). Defined only by cores
43 + * that apply the `mxchat_chat_toolbar_items` filter while rendering the chat
44 + * toolbar and fire the `mxchat:toolbar-ready` / `mxchat:opened` document events.
45 + * Add-ons test `defined('MXCHAT_TOOLBAR_ITEMS_API')` and fall back to their
46 + * older DOM-injection path on cores without it, so a button is never rendered
47 + * twice. Bump the value only if the item shape (id / html / order) changes.
48 + */
49 +if (!defined('MXCHAT_TOOLBAR_ITEMS_API')) {
50 + define('MXCHAT_TOOLBAR_ITEMS_API', 1);
51 +}
52 +
53 +/**
42 54 * Default confidence floor for the in-chat YouTube card, as a percentage
43 55 * (plan-mxchat-20260813-f52492). Higher than the site-wide Similarity
44 56 * Threshold default of 35 by design — see MxChat_Utils::video_embed_threshold().
45 57 * Declared here so the gate, the admin field and the tests all read ONE number.
@@ -48,8 +60,55 @@
48 60 define('MXCHAT_VIDEO_EMBED_THRESHOLD_DEFAULT', 55);
49 61 }
50 62
51 63 /**
64 + * One-time install stamp: records the plain version this site first ran, so
65 + * behavior defaults can differ between fresh installs and upgrades without
66 + * touching anyone's stored settings. Mirrors mxchat-mcp's 1.0.7 stamp shape
67 + * (plan 7b578e); first consumer is the "Strip unapproved links" default
68 + * (plan 58f8b4). Runs at init priority 1 — BEFORE initialize_default_options
69 + * (init 20) writes mxchat_options on a fresh site's first request, because
70 + * that option's pre-existing presence is how an upgrade is recognized.
71 + */
72 +function mxchat_stamp_install_version() {
73 + if (get_option('mxchat_installed_at_version', '') !== '') {
74 + return;
75 + }
76 + $existing = get_option('mxchat_options', false) !== false;
77 + $version = defined('MXCHAT_BASE_VERSION') ? MXCHAT_BASE_VERSION : '0.0.0';
78 + update_option('mxchat_installed_at_version', $existing ? 'legacy' : $version, false);
79 +}
80 +add_action('init', 'mxchat_stamp_install_version', 1);
81 +
82 +/**
83 + * Stamp-derived default for the "Strip unapproved links" toggle (plan 58f8b4,
84 + * option-c split of the old Citation Links conflation): 'on' only for installs
85 + * born at 3.2.20+. A missing or 'legacy' stamp means the site predates the
86 + * setting — keep 'off' so no existing site's links start vanishing on update.
87 + */
88 +function mxchat_strip_unapproved_links_default() {
89 + $stamp = get_option('mxchat_installed_at_version', '');
90 + if ($stamp === '' || $stamp === 'legacy') {
91 + return 'off';
92 + }
93 + return version_compare($stamp, '3.2.20', '>=') ? 'on' : 'off';
94 +}
95 +
96 +/**
97 + * Effective state of "Strip unapproved links": an explicitly saved option
98 + * always wins; until one exists the install-stamp default governs. Read via
99 + * a fresh get_option on purpose — the response URL guard runs late in the
100 + * request and must see a value saved moments earlier.
101 + */
102 +function mxchat_strip_unapproved_links_enabled() {
103 + $opts = get_option('mxchat_options', array());
104 + if (is_array($opts) && isset($opts['strip_unapproved_links_toggle'])) {
105 + return $opts['strip_unapproved_links_toggle'] === 'on';
106 + }
107 + return mxchat_strip_unapproved_links_default() === 'on';
108 +}
109 +
110 +/**
52 111 * Honest, versioned User-Agent for MXChat remote-content ingestion fetches
53 112 * (Knowledge Base PDF import, URL import, sitemap / website crawl).
54 113 *
55 114 * WAF rulesets (SiteGround/ModSecurity, Wordfence, Cloudflare managed rules)
@@ -427,8 +486,10 @@
427 486 'includes/class-mxchat-tool-registry.php',
428 487 'includes/class-mxchat-integrator.php',
429 488 'includes/class-mxchat-admin.php',
430 489 'includes/class-mxchat-public.php',
490 + 'includes/class-mxchat-block.php',
491 + 'includes/class-mxchat-elementor.php',
431 492 'includes/class-mxchat-utils.php',
432 493 'includes/class-mxchat-user.php',
433 494 'includes/class-mxchat-privacy.php',
434 495 'includes/class-mxchat-meta-box.php',
@@ -439,9 +500,11 @@
439 500 'includes/class-mxchat-editor-assistant.php',
440 501 'includes/class-rest-api.php',
441 502 'admin/class-ajax-handler.php',
442 503 'admin/class-pinecone-manager.php',
443 - 'admin/class-knowledge-manager.php'
504 + 'admin/class-pinecone-documents.php',
505 + 'admin/class-knowledge-manager.php',
506 + 'admin/class-vectorstore-manager.php'
444 507 );
445 508
446 509 foreach ($class_files as $file) {
447 510 $file_path = plugin_dir_path(__FILE__) . $file;
@@ -474,8 +537,21 @@
474 537 if (class_exists('MxChat_Model_Liveness')) {
475 538 MxChat_Model_Liveness::init();
476 539 }
477 540
541 + // Gutenberg chatbot block (plan-95dd1e): a click-to-place wrapper over the
542 + // [mxchat_chatbot] shortcode. Registers on init; no-op below WP 5.0.
543 + if (class_exists('MxChat_Block')) {
544 + MxChat_Block::init();
545 + }
546 +
547 + // Elementor chatbot widget (plan-95dd1e part 2): registered ONLY inside
548 + // elementor/widgets/register (Elementor >= 3.5) — with Elementor absent or
549 + // older, the hook never fires and nothing further loads.
550 + if (class_exists('MxChat_Elementor')) {
551 + MxChat_Elementor::init();
552 + }
553 +
478 554 // Editor Assistant — free, OFF-by-default block-editor AI actions (plan-8cb0cb).
479 555 // init() wires REST + streaming AJAX + sidebar enqueue ONLY when the
480 556 // mxchat_editor_assistant_enabled option is 'on'; otherwise zero footprint.
481 557 if (class_exists('MxChat_Editor_Assistant')) {
@@ -481,8 +557,16 @@
481 557 if (class_exists('MxChat_Editor_Assistant')) {
482 558 MxChat_Editor_Assistant::init();
483 559 }
484 560
561 + // OpenAI Vector Store write path (plan-15b5c6): import/sync AJAX, the
562 + // import cron tick, the pending-delete sweeper, and the WP-CLI command
563 + // all register in the constructor. The sync itself only runs when the
564 + // sync toggle + store ID + OpenAI key are all present.
565 + if (class_exists('MxChat_Vectorstore_Manager')) {
566 + MxChat_Vectorstore_Manager::get_instance();
567 + }
568 +
485 569 // Admin pages that aren't classes (procedural include).
486 570 if (is_admin()) {
487 571 $admin_api_page = plugin_dir_path(__FILE__) . 'includes/admin-api-page.php';
488 572 if (file_exists($admin_api_page)) {
@@ -874,8 +958,66 @@
874 958 }
875 959 }
876 960
877 961 /**
962 + * 3.2.20: One-time backfill of the caee10 catalog usage-hint defaults
963 + * (plan 64c1ad). persist() writes usage_hint for EVERY shown tool on every
964 + * autosave — as '' when the box is untouched — and resolve_tool_setting()
965 + * seeds a catalog default_hint ONLY onto an ABSENT key. So any install that
966 + * saved the AI Tools screen before 3.2.20 holds '' everywhere and the shipped
967 + * defaults can never reach it. On an upgrade from < 3.2.20 an empty stored
968 + * hint cannot be a deliberate clear of a rendered default (those builds never
969 + * rendered one), so seeding is safe. From 3.2.20 on, empty means the owner
970 + * cleared a visible default and is respected — this never runs again (version
971 + * gate + its own marker, deliberately not caee10's flag).
972 + *
973 + * Never touched: entries with owner text, legacy bare-bool entries and
974 + * absent-key entries (both already resolve to the default at read time), and
975 + * tools whose catalog entry ships no default_hint.
976 + */
977 +function mxchat_backfill_tool_hint_defaults() {
978 + if (get_option('mxchat_tool_hint_backfill_64c1ad') === '1') {
979 + return;
980 + }
981 +
982 + if (class_exists('MxChat_Tool_Registry')) {
983 + $map = get_option('mxchat_function_calling_tools', array());
984 + if (is_array($map) && !empty($map)) {
985 + $defaults = array();
986 + foreach (MxChat_Tool_Registry::core_tool_catalog() as $fn => $meta) {
987 + if (!empty($meta['default_hint'])) {
988 + $defaults[$fn] = (string) $meta['default_hint'];
989 + }
990 + }
991 +
992 + $changed = false;
993 + foreach ($map as $fn => $entry) {
994 + if (!isset($defaults[$fn])) {
995 + continue; // no catalog default — nothing to seed
996 + }
997 + if (!is_array($entry)) {
998 + continue; // legacy bare bool: key absent, resolves to the default already
999 + }
1000 + if (!array_key_exists('usage_hint', $entry)) {
1001 + continue; // absent key gets the default at read time — must stay absent
1002 + }
1003 + if (trim((string) $entry['usage_hint']) !== '') {
1004 + continue; // owner text — never touch
1005 + }
1006 + $map[$fn]['usage_hint'] = $defaults[$fn];
1007 + $changed = true;
1008 + }
1009 +
1010 + if ($changed) {
1011 + update_option('mxchat_function_calling_tools', $map);
1012 + }
1013 + }
1014 + }
1015 +
1016 + update_option('mxchat_tool_hint_backfill_64c1ad', '1', false);
1017 +}
1018 +
1019 +/**
878 1020 * 2.5.2: Create queue processing tables for reliable background processing
879 1021 */
880 1022 function mxchat_create_queue_tables() {
881 1023 global $wpdb;
@@ -973,8 +1115,41 @@
973 1115 dbDelta($sql);
974 1116 }
975 1117
976 1118 /**
1119 + * Create the OpenAI Vector Store file-mapping table (v3.2.20, plan 15b5c6).
1120 + * One row per mirrored KB entry: which store, which bot, which OpenAI file id,
1121 + * and a hash of the uploaded body for change detection. Vector store files are
1122 + * not patchable in place — without this mapping an update cannot find its
1123 + * predecessor, and the store silently accumulates stale duplicates.
1124 + * status: 'live' (serving) or 'pending_delete' (condemned, swept later).
1125 + */
1126 +function mxchat_create_vectorstore_files_table() {
1127 + global $wpdb;
1128 + $charset_collate = $wpdb->get_charset_collate();
1129 +
1130 + $table_name = $wpdb->prefix . 'mxchat_vectorstore_files';
1131 + $sql = "CREATE TABLE $table_name (
1132 + id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
1133 + store_id varchar(64) NOT NULL,
1134 + bot_id varchar(64) NOT NULL DEFAULT 'default',
1135 + entry_key char(32) NOT NULL,
1136 + source_url text,
1137 + file_id varchar(64) NOT NULL,
1138 + content_hash char(32) NOT NULL DEFAULT '',
1139 + status varchar(20) NOT NULL DEFAULT 'live',
1140 + last_error text,
1141 + updated_at datetime DEFAULT NULL,
1142 + PRIMARY KEY (id),
1143 + KEY store_entry (store_id, bot_id, entry_key, status),
1144 + KEY status (status)
1145 + ) $charset_collate;";
1146 +
1147 + require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1148 + dbDelta($sql);
1149 +}
1150 +
1151 +/**
977 1152 * 2.5.2: Fix URL column size to support long URLs (especially with UTF-8 encoding)
978 1153 * This fixes "url, source_url. The supplied values may be too long" errors
979 1154 */
980 1155 function mxchat_fix_url_column_size() {
@@ -1018,110 +1193,47 @@
1018 1193 if (!isset($options['model'])) {
1019 1194 return;
1020 1195 }
1021 1196
1022 - $current_model = $options['model'];
1023 -
1024 - // Migrate deprecated/retired Claude models BY TIER so a rescued site lands on
1025 - // the current generation, not an older intermediate (plan dc91bd — the previous
1026 - // single target, claude-opus-4-6, is now two Opus generations behind).
1027 - $deprecated_claude_opus = array(
1028 - 'claude-3-opus-20240229', // Retired Jan 5, 2026
1029 - 'claude-opus-4-20250514', // Deprecated
1030 - 'claude-opus-4-1-20250805', // Retired Aug 5, 2026 (first-party API)
1031 - );
1032 - $deprecated_claude_sonnet = array(
1033 - 'claude-3-5-sonnet-20240620', // Retired Oct 28, 2025
1034 - 'claude-3-5-sonnet-20241022', // Retired Oct 28, 2025
1035 - 'claude-3-7-sonnet-20250219', // Retired Feb 19, 2026
1036 - 'claude-3-sonnet-20240229', // Legacy
1037 - 'claude-sonnet-4-20250514', // Deprecated
1038 - );
1039 - $deprecated_claude_haiku = array(
1040 - 'claude-3-haiku-20240307', // Legacy
1041 - 'claude-3-5-haiku-20241022', // Deprecated
1042 - );
1043 - if (in_array($current_model, $deprecated_claude_opus, true)) {
1044 - $options['model'] = 'claude-opus-5';
1045 - $migrated = true;
1046 - $migration_message = sprintf(
1047 - 'Your chatbot model has been automatically updated from %s to Claude Opus 5 because Anthropic retired the older Claude model.',
1048 - $current_model
1049 - );
1050 - } elseif (in_array($current_model, $deprecated_claude_sonnet, true)) {
1051 - $options['model'] = 'claude-sonnet-5';
1052 - $migrated = true;
1053 - $migration_message = sprintf(
1054 - 'Your chatbot model has been automatically updated from %s to Claude Sonnet 5 because Anthropic retired the older Claude model.',
1055 - $current_model
1056 - );
1057 - } elseif (in_array($current_model, $deprecated_claude_haiku, true)) {
1058 - $options['model'] = 'claude-haiku-4-5-20251001';
1059 - $migrated = true;
1060 - $migration_message = sprintf(
1061 - 'Your chatbot model has been automatically updated from %s to Claude Haiku 4.5 because Anthropic retired the older Claude model.',
1062 - $current_model
1063 - );
1197 + // The retired-id → replacement mapping lives in ONE place:
1198 + // MxChat_Model_Catalog::retired_model_map() (plan 202df5). This function
1199 + // and every add-on that stores model ids consume that map — do not add a
1200 + // deprecation list here again.
1201 + if (!class_exists('MxChat_Model_Catalog') || !method_exists('MxChat_Model_Catalog', 'retired_model_map')) {
1202 + return; // catalog not loaded (defensive) — the next admin load retries
1064 1203 }
1204 + $map = MxChat_Model_Catalog::retired_model_map();
1065 1205
1066 - // Migrate deprecated GPT-4 series and GPT-3.5 Turbo to GPT-5.6 Sol.
1067 - // (Previous target gpt-5.1-chat-latest itself retires 2026-08-10 — never
1068 - // migrate onto a model that is already on a deprecation list.)
1069 - if (in_array($current_model, array('gpt-4o', 'gpt-4.1-2025-04-14', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'), true)) {
1070 - $options['model'] = 'gpt-5.6-sol';
1206 + $current_model = $options['model'];
1207 + if (isset($map[$current_model])) {
1208 + $entry = $map[$current_model];
1209 + $options['model'] = $entry['to'];
1071 1210 $migrated = true;
1072 1211 $migration_message = sprintf(
1073 - 'Your chatbot model has been automatically updated from %s to GPT-5.6 Sol due to OpenAI deprecating older models.',
1074 - $current_model
1212 + 'Your chatbot model has been automatically updated from %s to %s %s.',
1213 + $current_model,
1214 + $entry['label'],
1215 + $entry['reason']
1075 1216 );
1076 1217 }
1077 1218
1078 - // Migrate the gpt-5.x-chat-latest aliases OpenAI retires on August 10, 2026.
1079 - // Replacement per OpenAI's deprecations page: gpt-5.6-sol (plan e46b8f).
1080 - if (in_array($current_model, array('gpt-5.1-chat-latest', 'gpt-5.3-chat-latest'), true)) {
1081 - $options['model'] = 'gpt-5.6-sol';
1082 - $migrated = true;
1083 - $migration_message = sprintf(
1084 - 'Your chatbot model has been automatically updated from %s to GPT-5.6 Sol because OpenAI retires the GPT-5.x Chat Latest models on August 10, 2026.',
1085 - $current_model
1086 - );
1087 - }
1088 -
1089 - // The content generator has its own model option — same retirement applies.
1090 - if (isset($options['content_model'])
1091 - && in_array($options['content_model'], array('gpt-5.1-chat-latest', 'gpt-5.3-chat-latest'), true)) {
1219 + // The content generator has its own model option — the same map applies.
1220 + // (Pre-202df5 this branch covered only the two gpt-5.x-chat-latest aliases;
1221 + // it now covers every retired id, so e.g. a content model stranded on a
1222 + // retired Claude id is rescued the same way the chat model is.)
1223 + if (isset($options['content_model']) && isset($map[$options['content_model']])) {
1092 1224 $old_content_model = $options['content_model'];
1093 - $options['content_model'] = 'gpt-5.6-sol';
1225 + $entry = $map[$old_content_model];
1226 + $options['content_model'] = $entry['to'];
1094 1227 $migrated = true;
1095 1228 $migration_message = trim($migration_message . ' ' . sprintf(
1096 - 'Your content generation model has also been updated from %s to GPT-5.6 Sol for the same OpenAI retirement.',
1097 - $old_content_model
1229 + 'Your content generation model has also been automatically updated from %s to %s %s.',
1230 + $old_content_model,
1231 + $entry['label'],
1232 + $entry['reason']
1098 1233 ));
1099 1234 }
1100 1235
1101 - // Migrate deprecated GPT-4o Mini and GPT-4.1 Mini to GPT-5 Mini
1102 - if (in_array($current_model, array('gpt-4o-mini', 'gpt-4.1-mini'), true)) {
1103 - $options['model'] = 'gpt-5-mini';
1104 - $migrated = true;
1105 - $migration_message = sprintf(
1106 - 'Your chatbot model has been automatically updated from %s to GPT-5 Mini due to OpenAI deprecating GPT-4 series models.',
1107 - $current_model
1108 - );
1109 - }
1110 -
1111 - // Migrate retired DeepSeek ids to DeepSeek V4 Flash — the vendor removed
1112 - // deepseek-chat and deepseek-reasoner on 2026-07-24 (hard cutoff, every
1113 - // request 400s). V4 Flash is DeepSeek's designated successor for the
1114 - // legacy deepseek-chat alias.
1115 - if (in_array($current_model, array('deepseek-chat', 'deepseek-reasoner'), true)) {
1116 - $options['model'] = 'deepseek-v4-flash';
1117 - $migrated = true;
1118 - $migration_message = sprintf(
1119 - 'Your chatbot model has been automatically updated from %s to DeepSeek V4 Flash because DeepSeek retired its older API models on July 24, 2026.',
1120 - $current_model
1121 - );
1122 - }
1123 -
1124 1236 if ($migrated) {
1125 1237 update_option('mxchat_options', $options);
1126 1238 update_option('mxchat_model_migrated_notice', true);
1127 1239 update_option('mxchat_model_migration_message', $migration_message);
@@ -1147,8 +1259,62 @@
1147 1259 }
1148 1260 }
1149 1261
1150 1262 /**
1263 + * One-time recommendation on EXISTING installs (stamp 'legacy') that the new
1264 + * "Strip unapproved links" guard exists and is worth turning on (plan 58f8b4).
1265 + * Fresh 3.2.20+ installs default it on and never see this. Shown only on
1266 + * MxChat admin pages, gone for good once dismissed or once the site saves an
1267 + * explicit value for the toggle either way.
1268 + */
1269 +function mxchat_show_strip_links_notice() {
1270 + if (!current_user_can('manage_options')) {
1271 + return;
1272 + }
1273 + $page = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
1274 + if (strpos($page, 'mxchat') !== 0) {
1275 + return;
1276 + }
1277 + if (get_option('mxchat_strip_links_notice_dismissed', '') === '1') {
1278 + return;
1279 + }
1280 + if (get_option('mxchat_installed_at_version', '') !== 'legacy') {
1281 + return;
1282 + }
1283 + $opts = get_option('mxchat_options', array());
1284 + if (is_array($opts) && isset($opts['strip_unapproved_links_toggle'])) {
1285 + return; // The site already made its choice — stop recommending.
1286 + }
1287 + $dismiss_url = wp_nonce_url(
1288 + admin_url('admin-post.php?action=mxchat_dismiss_strip_links_notice'),
1289 + 'mxchat_dismiss_strip_links_notice'
1290 + );
1291 + ?>
1292 + <div class="notice notice-info">
1293 + <p>
1294 + <strong><?php esc_html_e('MxChat: new link protection available', 'mxchat'); ?></strong><br>
1295 + <?php esc_html_e('The new "Strip Unapproved Links" setting removes links the AI invents from its answers even when Citation Links is off — links to real pages on your site and links your integrations return are always kept. It is off on existing sites so nothing changes without you; we recommend turning it on under MxChat → Settings → Chatbot Behavior.', 'mxchat'); ?>
1296 + <a href="<?php echo esc_url($dismiss_url); ?>"><?php esc_html_e('Dismiss', 'mxchat'); ?></a>
1297 + </p>
1298 + </div>
1299 + <?php
1300 +}
1301 +add_action('admin_notices', 'mxchat_show_strip_links_notice');
1302 +
1303 +/** Dismiss handler for the strip-links recommendation notice (plan 58f8b4). */
1304 +function mxchat_dismiss_strip_links_notice() {
1305 + if (!current_user_can('manage_options')) {
1306 + wp_die(esc_html__('Insufficient permissions.', 'mxchat'), '', array('response' => 403));
1307 + }
1308 + check_admin_referer('mxchat_dismiss_strip_links_notice');
1309 + update_option('mxchat_strip_links_notice_dismissed', '1', false);
1310 + $referer = wp_get_referer();
1311 + wp_safe_redirect($referer ? $referer : admin_url('admin.php?page=mxchat-max'));
1312 + exit;
1313 +}
1314 +add_action('admin_post_mxchat_dismiss_strip_links_notice', 'mxchat_dismiss_strip_links_notice');
1315 +
1316 +/**
1151 1317 * Persistent admin notice when the provider rejected the configured model
1152 1318 * (model_not_found / no access). Set by mxchat_friendly_chat_error() in the
1153 1319 * integrator whenever a chat request fails on a model-access error — including
1154 1320 * requests from anonymous visitors, which is the case that otherwise stays
@@ -1205,8 +1371,9 @@
1205 1371 url TEXT NOT NULL,
1206 1372 article_content LONGTEXT NOT NULL,
1207 1373 embedding_vector LONGTEXT,
1208 1374 source_url TEXT DEFAULT NULL,
1375 + title VARCHAR(255) DEFAULT NULL,
1209 1376 role_restriction VARCHAR(50) DEFAULT 'public',
1210 1377 content_type VARCHAR(50) DEFAULT 'content',
1211 1378 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1212 1379 PRIMARY KEY (id),
@@ -1260,8 +1427,11 @@
1260 1427
1261 1428 // Create per-session satisfaction ratings table (v3.2.6)
1262 1429 mxchat_create_session_ratings_table();
1263 1430
1431 + // Create Vector Store file-mapping table (v3.2.20, plan 15b5c6)
1432 + mxchat_create_vectorstore_files_table();
1433 +
1264 1434 // Ensure additional columns in system prompt table
1265 1435 $existing_system_columns = $wpdb->get_results("SHOW COLUMNS FROM $system_prompt_table");
1266 1436 if (!empty($existing_system_columns)) {
1267 1437 $existing_system_column_names = array_column($existing_system_columns, 'Field');
@@ -1274,8 +1444,13 @@
1274 1444 }
1275 1445 if (!in_array('role_restriction', $existing_system_column_names)) {
1276 1446 $wpdb->query("ALTER TABLE $system_prompt_table ADD COLUMN role_restriction VARCHAR(50) DEFAULT 'public' AFTER source_url");
1277 1447 }
1448 + // Page title captured at import time (3.2.23, plan d9ee66) - read by
1449 + // add-ons that name a source, e.g. AI Search's source cards.
1450 + if (!in_array('title', $existing_system_column_names)) {
1451 + $wpdb->query("ALTER TABLE $system_prompt_table ADD COLUMN title VARCHAR(255) DEFAULT NULL AFTER source_url");
1452 + }
1278 1453 }
1279 1454
1280 1455 // Set default thresholds for existing intents
1281 1456 $wpdb->query("UPDATE {$intents_table} SET similarity_threshold = 0.85 WHERE similarity_threshold IS NULL");
@@ -1544,8 +1719,21 @@
1544 1719 if (version_compare($current_version, '3.2.19', '<')) {
1545 1720 mxchat_migrate_deprecated_models();
1546 1721 }
1547 1722
1723 + // 3.2.20: Seed the caee10 usage-hint defaults onto tool entries a
1724 + // pre-3.2.20 autosave stamped with '' (plan 64c1ad). The gate value
1725 + // equals the version this block ships in (a5a598 rule); the
1726 + // function carries its own one-time marker on top.
1727 + // Also re-run the deprecated-models migration: 202df5 widened it to
1728 + // rescue a content_model stranded on ANY retired id (previously
1729 + // only the two gpt-5.x-chat-latest aliases). Idempotent — only
1730 + // rewrites models on the catalog's retired_model_map().
1731 + if (version_compare($current_version, '3.2.20', '<')) {
1732 + mxchat_backfill_tool_hint_defaults();
1733 + mxchat_migrate_deprecated_models();
1734 + }
1735 +
1548 1736 // Run full activation to ensure everything is up to date
1549 1737 mxchat_activate();
1550 1738
1551 1739 // Run migration functions
@@ -1624,8 +1812,111 @@
1624 1812 unset($mxchat_options['acf_pdf_extract_default']);
1625 1813 update_option('mxchat_options', $mxchat_options);
1626 1814 }
1627 1815 }
1816 +
1817 +/**
1818 + * One-time migration of mxchat_acf_excluded_fields from field NAMES to field
1819 + * KEYS (plan 30e81f). Names are not unique across ACF groups, so two fields
1820 + * named the same in different groups shared one toggle, one saved state, and
1821 + * one exclusion — and the save-on-exit beacon could revert a save through the
1822 + * twin. Keys are unique; everything now runs on them.
1823 + *
1824 + * MIGRATION DIRECTION IS DELIBERATE: one stored name can match several keys —
1825 + * exclude EVERY one of them. The UI describes these as "sensitive or
1826 + * irrelevant fields"; an under-migration would silently start feeding a
1827 + * previously-excluded sensitive field into embeddings sent to a third-party
1828 + * provider. Over-excluding is visible in the UI and costs some retrieval
1829 + * quality; under-excluding is a silent privacy regression.
1830 + *
1831 + * Self-arming on acf/init (NOT the version-gated upgrade block): resolving
1832 + * names needs ACF fully booted with local JSON/PHP groups registered, and if
1833 + * ACF is deactivated at upgrade time the migration simply waits for the next
1834 + * load with ACF active. Until it runs, stored names keep working — every
1835 + * exclusion read site honors legacy name entries alongside keys. A name that
1836 + * matches no current key is KEPT (its group may be temporarily inactive), and
1837 + * the per-field include path lazily converts it if it ever resolves again.
1838 + */
1839 +function mxchat_migrate_acf_exclusions_to_keys() {
1840 + if (get_option('mxchat_acf_exclusions_migrated', '') === '1') {
1841 + return;
1842 + }
1843 + $stored = get_option('mxchat_acf_excluded_fields', array());
1844 + if (!is_array($stored) || empty($stored)) {
1845 + update_option('mxchat_acf_exclusions_migrated', '1', false);
1846 + return;
1847 + }
1848 + if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
1849 + return; // acf/init fired without the API? Bail; retry next load.
1850 + }
1851 +
1852 + // Map every current top-level field: name => [keys], and the set of keys.
1853 + $name_to_keys = array();
1854 + $current_keys = array();
1855 + foreach (acf_get_field_groups() as $group) {
1856 + $group_fields = acf_get_fields($group['key']);
1857 + if (empty($group_fields)) {
1858 + continue;
1859 + }
1860 + foreach ($group_fields as $field) {
1861 + if (empty($field['key']) || !isset($field['name'])) {
1862 + continue;
1863 + }
1864 + $current_keys[$field['key']] = true;
1865 + $name_to_keys[$field['name']][] = $field['key'];
1866 + }
1867 + }
1868 +
1869 + $migrated = array();
1870 + $converted = 0;
1871 + foreach ($stored as $entry) {
1872 + if (!is_string($entry) || $entry === '') {
1873 + continue;
1874 + }
1875 + if (isset($current_keys[$entry])) {
1876 + $migrated[] = $entry; // already a live key
1877 + } elseif (isset($name_to_keys[$entry])) {
1878 + foreach ($name_to_keys[$entry] as $key) {
1879 + $migrated[] = $key; // every key wearing this name — fail toward more exclusion
1880 + }
1881 + $converted++;
1882 + } else {
1883 + $migrated[] = $entry; // unresolved — keep; still honored by name everywhere
1884 + }
1885 + }
1886 + $migrated = array_values(array_unique($migrated));
1887 +
1888 + update_option('mxchat_acf_excluded_fields', $migrated);
1889 + update_option('mxchat_acf_exclusions_migrated', '1', false);
1890 + if ($converted > 0) {
1891 + update_option('mxchat_acf_exclusions_migrated_notice', '1', false);
1892 + }
1893 +}
1894 +add_action('acf/init', 'mxchat_migrate_acf_exclusions_to_keys', 20);
1895 +
1896 +/**
1897 + * One-time notice after the ACF exclusion migration actually converted
1898 + * name entries — the settings are worth a review, especially where one name
1899 + * fanned out to several fields (every match is now excluded, on purpose).
1900 + */
1901 +function mxchat_show_acf_exclusions_migrated_notice() {
1902 + if (!current_user_can('manage_options')) {
1903 + return;
1904 + }
1905 + if (get_option('mxchat_acf_exclusions_migrated_notice', '') !== '1') {
1906 + return;
1907 + }
1908 + ?>
1909 + <div class="notice notice-info is-dismissible">
1910 + <p>
1911 + <strong><?php esc_html_e('MxChat: ACF field exclusions updated', 'mxchat'); ?></strong><br>
1912 + <?php esc_html_e('Your ACF field exclusion settings were migrated to identify fields precisely, so same-named fields in different groups no longer share one toggle. Where a saved exclusion matched several fields, all of them are now excluded — please review the toggles under MxChat → Knowledge → ACF Field Settings.', 'mxchat'); ?>
1913 + </p>
1914 + </div>
1915 + <?php
1916 + delete_option('mxchat_acf_exclusions_migrated_notice');
1917 +}
1918 +add_action('admin_notices', 'mxchat_show_acf_exclusions_migrated_notice');
1628 1919
1629 1920 /**
1630 1921 * Ensure tables exist on every admin load for fresh installations
1631 1922 * This is a safety net for cases where activation hook doesn't fire