PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / trunk
MxChat – AI Chatbot & Content Generation for WordPress vtrunk
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 +363 -91 3.2.19trunk 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.21
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
@@ -48,8 +48,55 @@
48 48 define('MXCHAT_VIDEO_EMBED_THRESHOLD_DEFAULT', 55);
49 49 }
50 50
51 51 /**
52 + * One-time install stamp: records the plain version this site first ran, so
53 + * behavior defaults can differ between fresh installs and upgrades without
54 + * touching anyone's stored settings. Mirrors mxchat-mcp's 1.0.7 stamp shape
55 + * (plan 7b578e); first consumer is the "Strip unapproved links" default
56 + * (plan 58f8b4). Runs at init priority 1 — BEFORE initialize_default_options
57 + * (init 20) writes mxchat_options on a fresh site's first request, because
58 + * that option's pre-existing presence is how an upgrade is recognized.
59 + */
60 +function mxchat_stamp_install_version() {
61 + if (get_option('mxchat_installed_at_version', '') !== '') {
62 + return;
63 + }
64 + $existing = get_option('mxchat_options', false) !== false;
65 + $version = defined('MXCHAT_BASE_VERSION') ? MXCHAT_BASE_VERSION : '0.0.0';
66 + update_option('mxchat_installed_at_version', $existing ? 'legacy' : $version, false);
67 +}
68 +add_action('init', 'mxchat_stamp_install_version', 1);
69 +
70 +/**
71 + * Stamp-derived default for the "Strip unapproved links" toggle (plan 58f8b4,
72 + * option-c split of the old Citation Links conflation): 'on' only for installs
73 + * born at 3.2.20+. A missing or 'legacy' stamp means the site predates the
74 + * setting — keep 'off' so no existing site's links start vanishing on update.
75 + */
76 +function mxchat_strip_unapproved_links_default() {
77 + $stamp = get_option('mxchat_installed_at_version', '');
78 + if ($stamp === '' || $stamp === 'legacy') {
79 + return 'off';
80 + }
81 + return version_compare($stamp, '3.2.20', '>=') ? 'on' : 'off';
82 +}
83 +
84 +/**
85 + * Effective state of "Strip unapproved links": an explicitly saved option
86 + * always wins; until one exists the install-stamp default governs. Read via
87 + * a fresh get_option on purpose — the response URL guard runs late in the
88 + * request and must see a value saved moments earlier.
89 + */
90 +function mxchat_strip_unapproved_links_enabled() {
91 + $opts = get_option('mxchat_options', array());
92 + if (is_array($opts) && isset($opts['strip_unapproved_links_toggle'])) {
93 + return $opts['strip_unapproved_links_toggle'] === 'on';
94 + }
95 + return mxchat_strip_unapproved_links_default() === 'on';
96 +}
97 +
98 +/**
52 99 * Honest, versioned User-Agent for MXChat remote-content ingestion fetches
53 100 * (Knowledge Base PDF import, URL import, sitemap / website crawl).
54 101 *
55 102 * WAF rulesets (SiteGround/ModSecurity, Wordfence, Cloudflare managed rules)
@@ -427,8 +474,10 @@
427 474 'includes/class-mxchat-tool-registry.php',
428 475 'includes/class-mxchat-integrator.php',
429 476 'includes/class-mxchat-admin.php',
430 477 'includes/class-mxchat-public.php',
478 + 'includes/class-mxchat-block.php',
479 + 'includes/class-mxchat-elementor.php',
431 480 'includes/class-mxchat-utils.php',
432 481 'includes/class-mxchat-user.php',
433 482 'includes/class-mxchat-privacy.php',
434 483 'includes/class-mxchat-meta-box.php',
@@ -439,9 +488,10 @@
439 488 'includes/class-mxchat-editor-assistant.php',
440 489 'includes/class-rest-api.php',
441 490 'admin/class-ajax-handler.php',
442 491 'admin/class-pinecone-manager.php',
443 - 'admin/class-knowledge-manager.php'
492 + 'admin/class-knowledge-manager.php',
493 + 'admin/class-vectorstore-manager.php'
444 494 );
445 495
446 496 foreach ($class_files as $file) {
447 497 $file_path = plugin_dir_path(__FILE__) . $file;
@@ -474,8 +524,21 @@
474 524 if (class_exists('MxChat_Model_Liveness')) {
475 525 MxChat_Model_Liveness::init();
476 526 }
477 527
528 + // Gutenberg chatbot block (plan-95dd1e): a click-to-place wrapper over the
529 + // [mxchat_chatbot] shortcode. Registers on init; no-op below WP 5.0.
530 + if (class_exists('MxChat_Block')) {
531 + MxChat_Block::init();
532 + }
533 +
534 + // Elementor chatbot widget (plan-95dd1e part 2): registered ONLY inside
535 + // elementor/widgets/register (Elementor >= 3.5) — with Elementor absent or
536 + // older, the hook never fires and nothing further loads.
537 + if (class_exists('MxChat_Elementor')) {
538 + MxChat_Elementor::init();
539 + }
540 +
478 541 // Editor Assistant — free, OFF-by-default block-editor AI actions (plan-8cb0cb).
479 542 // init() wires REST + streaming AJAX + sidebar enqueue ONLY when the
480 543 // mxchat_editor_assistant_enabled option is 'on'; otherwise zero footprint.
481 544 if (class_exists('MxChat_Editor_Assistant')) {
@@ -481,8 +544,16 @@
481 544 if (class_exists('MxChat_Editor_Assistant')) {
482 545 MxChat_Editor_Assistant::init();
483 546 }
484 547
548 + // OpenAI Vector Store write path (plan-15b5c6): import/sync AJAX, the
549 + // import cron tick, the pending-delete sweeper, and the WP-CLI command
550 + // all register in the constructor. The sync itself only runs when the
551 + // sync toggle + store ID + OpenAI key are all present.
552 + if (class_exists('MxChat_Vectorstore_Manager')) {
553 + MxChat_Vectorstore_Manager::get_instance();
554 + }
555 +
485 556 // Admin pages that aren't classes (procedural include).
486 557 if (is_admin()) {
487 558 $admin_api_page = plugin_dir_path(__FILE__) . 'includes/admin-api-page.php';
488 559 if (file_exists($admin_api_page)) {
@@ -874,8 +945,66 @@
874 945 }
875 946 }
876 947
877 948 /**
949 + * 3.2.20: One-time backfill of the caee10 catalog usage-hint defaults
950 + * (plan 64c1ad). persist() writes usage_hint for EVERY shown tool on every
951 + * autosave — as '' when the box is untouched — and resolve_tool_setting()
952 + * seeds a catalog default_hint ONLY onto an ABSENT key. So any install that
953 + * saved the AI Tools screen before 3.2.20 holds '' everywhere and the shipped
954 + * defaults can never reach it. On an upgrade from < 3.2.20 an empty stored
955 + * hint cannot be a deliberate clear of a rendered default (those builds never
956 + * rendered one), so seeding is safe. From 3.2.20 on, empty means the owner
957 + * cleared a visible default and is respected — this never runs again (version
958 + * gate + its own marker, deliberately not caee10's flag).
959 + *
960 + * Never touched: entries with owner text, legacy bare-bool entries and
961 + * absent-key entries (both already resolve to the default at read time), and
962 + * tools whose catalog entry ships no default_hint.
963 + */
964 +function mxchat_backfill_tool_hint_defaults() {
965 + if (get_option('mxchat_tool_hint_backfill_64c1ad') === '1') {
966 + return;
967 + }
968 +
969 + if (class_exists('MxChat_Tool_Registry')) {
970 + $map = get_option('mxchat_function_calling_tools', array());
971 + if (is_array($map) && !empty($map)) {
972 + $defaults = array();
973 + foreach (MxChat_Tool_Registry::core_tool_catalog() as $fn => $meta) {
974 + if (!empty($meta['default_hint'])) {
975 + $defaults[$fn] = (string) $meta['default_hint'];
976 + }
977 + }
978 +
979 + $changed = false;
980 + foreach ($map as $fn => $entry) {
981 + if (!isset($defaults[$fn])) {
982 + continue; // no catalog default — nothing to seed
983 + }
984 + if (!is_array($entry)) {
985 + continue; // legacy bare bool: key absent, resolves to the default already
986 + }
987 + if (!array_key_exists('usage_hint', $entry)) {
988 + continue; // absent key gets the default at read time — must stay absent
989 + }
990 + if (trim((string) $entry['usage_hint']) !== '') {
991 + continue; // owner text — never touch
992 + }
993 + $map[$fn]['usage_hint'] = $defaults[$fn];
994 + $changed = true;
995 + }
996 +
997 + if ($changed) {
998 + update_option('mxchat_function_calling_tools', $map);
999 + }
1000 + }
1001 + }
1002 +
1003 + update_option('mxchat_tool_hint_backfill_64c1ad', '1', false);
1004 +}
1005 +
1006 +/**
878 1007 * 2.5.2: Create queue processing tables for reliable background processing
879 1008 */
880 1009 function mxchat_create_queue_tables() {
881 1010 global $wpdb;
@@ -973,8 +1102,41 @@
973 1102 dbDelta($sql);
974 1103 }
975 1104
976 1105 /**
1106 + * Create the OpenAI Vector Store file-mapping table (v3.2.20, plan 15b5c6).
1107 + * One row per mirrored KB entry: which store, which bot, which OpenAI file id,
1108 + * and a hash of the uploaded body for change detection. Vector store files are
1109 + * not patchable in place — without this mapping an update cannot find its
1110 + * predecessor, and the store silently accumulates stale duplicates.
1111 + * status: 'live' (serving) or 'pending_delete' (condemned, swept later).
1112 + */
1113 +function mxchat_create_vectorstore_files_table() {
1114 + global $wpdb;
1115 + $charset_collate = $wpdb->get_charset_collate();
1116 +
1117 + $table_name = $wpdb->prefix . 'mxchat_vectorstore_files';
1118 + $sql = "CREATE TABLE $table_name (
1119 + id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
1120 + store_id varchar(64) NOT NULL,
1121 + bot_id varchar(64) NOT NULL DEFAULT 'default',
1122 + entry_key char(32) NOT NULL,
1123 + source_url text,
1124 + file_id varchar(64) NOT NULL,
1125 + content_hash char(32) NOT NULL DEFAULT '',
1126 + status varchar(20) NOT NULL DEFAULT 'live',
1127 + last_error text,
1128 + updated_at datetime DEFAULT NULL,
1129 + PRIMARY KEY (id),
1130 + KEY store_entry (store_id, bot_id, entry_key, status),
1131 + KEY status (status)
1132 + ) $charset_collate;";
1133 +
1134 + require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1135 + dbDelta($sql);
1136 +}
1137 +
1138 +/**
977 1139 * 2.5.2: Fix URL column size to support long URLs (especially with UTF-8 encoding)
978 1140 * This fixes "url, source_url. The supplied values may be too long" errors
979 1141 */
980 1142 function mxchat_fix_url_column_size() {
@@ -1018,110 +1180,47 @@
1018 1180 if (!isset($options['model'])) {
1019 1181 return;
1020 1182 }
1021 1183
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 - );
1184 + // The retired-id → replacement mapping lives in ONE place:
1185 + // MxChat_Model_Catalog::retired_model_map() (plan 202df5). This function
1186 + // and every add-on that stores model ids consume that map — do not add a
1187 + // deprecation list here again.
1188 + if (!class_exists('MxChat_Model_Catalog') || !method_exists('MxChat_Model_Catalog', 'retired_model_map')) {
1189 + return; // catalog not loaded (defensive) — the next admin load retries
1064 1190 }
1191 + $map = MxChat_Model_Catalog::retired_model_map();
1065 1192
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';
1193 + $current_model = $options['model'];
1194 + if (isset($map[$current_model])) {
1195 + $entry = $map[$current_model];
1196 + $options['model'] = $entry['to'];
1071 1197 $migrated = true;
1072 1198 $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
1199 + 'Your chatbot model has been automatically updated from %s to %s %s.',
1200 + $current_model,
1201 + $entry['label'],
1202 + $entry['reason']
1075 1203 );
1076 1204 }
1077 1205
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)) {
1206 + // The content generator has its own model option — the same map applies.
1207 + // (Pre-202df5 this branch covered only the two gpt-5.x-chat-latest aliases;
1208 + // it now covers every retired id, so e.g. a content model stranded on a
1209 + // retired Claude id is rescued the same way the chat model is.)
1210 + if (isset($options['content_model']) && isset($map[$options['content_model']])) {
1092 1211 $old_content_model = $options['content_model'];
1093 - $options['content_model'] = 'gpt-5.6-sol';
1212 + $entry = $map[$old_content_model];
1213 + $options['content_model'] = $entry['to'];
1094 1214 $migrated = true;
1095 1215 $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
1216 + 'Your content generation model has also been automatically updated from %s to %s %s.',
1217 + $old_content_model,
1218 + $entry['label'],
1219 + $entry['reason']
1098 1220 ));
1099 1221 }
1100 1222
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 1223 if ($migrated) {
1125 1224 update_option('mxchat_options', $options);
1126 1225 update_option('mxchat_model_migrated_notice', true);
1127 1226 update_option('mxchat_model_migration_message', $migration_message);
@@ -1147,8 +1246,62 @@
1147 1246 }
1148 1247 }
1149 1248
1150 1249 /**
1250 + * One-time recommendation on EXISTING installs (stamp 'legacy') that the new
1251 + * "Strip unapproved links" guard exists and is worth turning on (plan 58f8b4).
1252 + * Fresh 3.2.20+ installs default it on and never see this. Shown only on
1253 + * MxChat admin pages, gone for good once dismissed or once the site saves an
1254 + * explicit value for the toggle either way.
1255 + */
1256 +function mxchat_show_strip_links_notice() {
1257 + if (!current_user_can('manage_options')) {
1258 + return;
1259 + }
1260 + $page = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
1261 + if (strpos($page, 'mxchat') !== 0) {
1262 + return;
1263 + }
1264 + if (get_option('mxchat_strip_links_notice_dismissed', '') === '1') {
1265 + return;
1266 + }
1267 + if (get_option('mxchat_installed_at_version', '') !== 'legacy') {
1268 + return;
1269 + }
1270 + $opts = get_option('mxchat_options', array());
1271 + if (is_array($opts) && isset($opts['strip_unapproved_links_toggle'])) {
1272 + return; // The site already made its choice — stop recommending.
1273 + }
1274 + $dismiss_url = wp_nonce_url(
1275 + admin_url('admin-post.php?action=mxchat_dismiss_strip_links_notice'),
1276 + 'mxchat_dismiss_strip_links_notice'
1277 + );
1278 + ?>
1279 + <div class="notice notice-info">
1280 + <p>
1281 + <strong><?php esc_html_e('MxChat: new link protection available', 'mxchat'); ?></strong><br>
1282 + <?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'); ?>
1283 + <a href="<?php echo esc_url($dismiss_url); ?>"><?php esc_html_e('Dismiss', 'mxchat'); ?></a>
1284 + </p>
1285 + </div>
1286 + <?php
1287 +}
1288 +add_action('admin_notices', 'mxchat_show_strip_links_notice');
1289 +
1290 +/** Dismiss handler for the strip-links recommendation notice (plan 58f8b4). */
1291 +function mxchat_dismiss_strip_links_notice() {
1292 + if (!current_user_can('manage_options')) {
1293 + wp_die(esc_html__('Insufficient permissions.', 'mxchat'), '', array('response' => 403));
1294 + }
1295 + check_admin_referer('mxchat_dismiss_strip_links_notice');
1296 + update_option('mxchat_strip_links_notice_dismissed', '1', false);
1297 + $referer = wp_get_referer();
1298 + wp_safe_redirect($referer ? $referer : admin_url('admin.php?page=mxchat-max'));
1299 + exit;
1300 +}
1301 +add_action('admin_post_mxchat_dismiss_strip_links_notice', 'mxchat_dismiss_strip_links_notice');
1302 +
1303 +/**
1151 1304 * Persistent admin notice when the provider rejected the configured model
1152 1305 * (model_not_found / no access). Set by mxchat_friendly_chat_error() in the
1153 1306 * integrator whenever a chat request fails on a model-access error — including
1154 1307 * requests from anonymous visitors, which is the case that otherwise stays
@@ -1260,8 +1413,11 @@
1260 1413
1261 1414 // Create per-session satisfaction ratings table (v3.2.6)
1262 1415 mxchat_create_session_ratings_table();
1263 1416
1417 + // Create Vector Store file-mapping table (v3.2.20, plan 15b5c6)
1418 + mxchat_create_vectorstore_files_table();
1419 +
1264 1420 // Ensure additional columns in system prompt table
1265 1421 $existing_system_columns = $wpdb->get_results("SHOW COLUMNS FROM $system_prompt_table");
1266 1422 if (!empty($existing_system_columns)) {
1267 1423 $existing_system_column_names = array_column($existing_system_columns, 'Field');
@@ -1544,8 +1700,21 @@
1544 1700 if (version_compare($current_version, '3.2.19', '<')) {
1545 1701 mxchat_migrate_deprecated_models();
1546 1702 }
1547 1703
1704 + // 3.2.20: Seed the caee10 usage-hint defaults onto tool entries a
1705 + // pre-3.2.20 autosave stamped with '' (plan 64c1ad). The gate value
1706 + // equals the version this block ships in (a5a598 rule); the
1707 + // function carries its own one-time marker on top.
1708 + // Also re-run the deprecated-models migration: 202df5 widened it to
1709 + // rescue a content_model stranded on ANY retired id (previously
1710 + // only the two gpt-5.x-chat-latest aliases). Idempotent — only
1711 + // rewrites models on the catalog's retired_model_map().
1712 + if (version_compare($current_version, '3.2.20', '<')) {
1713 + mxchat_backfill_tool_hint_defaults();
1714 + mxchat_migrate_deprecated_models();
1715 + }
1716 +
1548 1717 // Run full activation to ensure everything is up to date
1549 1718 mxchat_activate();
1550 1719
1551 1720 // Run migration functions
@@ -1624,8 +1793,111 @@
1624 1793 unset($mxchat_options['acf_pdf_extract_default']);
1625 1794 update_option('mxchat_options', $mxchat_options);
1626 1795 }
1627 1796 }
1797 +
1798 +/**
1799 + * One-time migration of mxchat_acf_excluded_fields from field NAMES to field
1800 + * KEYS (plan 30e81f). Names are not unique across ACF groups, so two fields
1801 + * named the same in different groups shared one toggle, one saved state, and
1802 + * one exclusion — and the save-on-exit beacon could revert a save through the
1803 + * twin. Keys are unique; everything now runs on them.
1804 + *
1805 + * MIGRATION DIRECTION IS DELIBERATE: one stored name can match several keys —
1806 + * exclude EVERY one of them. The UI describes these as "sensitive or
1807 + * irrelevant fields"; an under-migration would silently start feeding a
1808 + * previously-excluded sensitive field into embeddings sent to a third-party
1809 + * provider. Over-excluding is visible in the UI and costs some retrieval
1810 + * quality; under-excluding is a silent privacy regression.
1811 + *
1812 + * Self-arming on acf/init (NOT the version-gated upgrade block): resolving
1813 + * names needs ACF fully booted with local JSON/PHP groups registered, and if
1814 + * ACF is deactivated at upgrade time the migration simply waits for the next
1815 + * load with ACF active. Until it runs, stored names keep working — every
1816 + * exclusion read site honors legacy name entries alongside keys. A name that
1817 + * matches no current key is KEPT (its group may be temporarily inactive), and
1818 + * the per-field include path lazily converts it if it ever resolves again.
1819 + */
1820 +function mxchat_migrate_acf_exclusions_to_keys() {
1821 + if (get_option('mxchat_acf_exclusions_migrated', '') === '1') {
1822 + return;
1823 + }
1824 + $stored = get_option('mxchat_acf_excluded_fields', array());
1825 + if (!is_array($stored) || empty($stored)) {
1826 + update_option('mxchat_acf_exclusions_migrated', '1', false);
1827 + return;
1828 + }
1829 + if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
1830 + return; // acf/init fired without the API? Bail; retry next load.
1831 + }
1832 +
1833 + // Map every current top-level field: name => [keys], and the set of keys.
1834 + $name_to_keys = array();
1835 + $current_keys = array();
1836 + foreach (acf_get_field_groups() as $group) {
1837 + $group_fields = acf_get_fields($group['key']);
1838 + if (empty($group_fields)) {
1839 + continue;
1840 + }
1841 + foreach ($group_fields as $field) {
1842 + if (empty($field['key']) || !isset($field['name'])) {
1843 + continue;
1844 + }
1845 + $current_keys[$field['key']] = true;
1846 + $name_to_keys[$field['name']][] = $field['key'];
1847 + }
1848 + }
1849 +
1850 + $migrated = array();
1851 + $converted = 0;
1852 + foreach ($stored as $entry) {
1853 + if (!is_string($entry) || $entry === '') {
1854 + continue;
1855 + }
1856 + if (isset($current_keys[$entry])) {
1857 + $migrated[] = $entry; // already a live key
1858 + } elseif (isset($name_to_keys[$entry])) {
1859 + foreach ($name_to_keys[$entry] as $key) {
1860 + $migrated[] = $key; // every key wearing this name — fail toward more exclusion
1861 + }
1862 + $converted++;
1863 + } else {
1864 + $migrated[] = $entry; // unresolved — keep; still honored by name everywhere
1865 + }
1866 + }
1867 + $migrated = array_values(array_unique($migrated));
1868 +
1869 + update_option('mxchat_acf_excluded_fields', $migrated);
1870 + update_option('mxchat_acf_exclusions_migrated', '1', false);
1871 + if ($converted > 0) {
1872 + update_option('mxchat_acf_exclusions_migrated_notice', '1', false);
1873 + }
1874 +}
1875 +add_action('acf/init', 'mxchat_migrate_acf_exclusions_to_keys', 20);
1876 +
1877 +/**
1878 + * One-time notice after the ACF exclusion migration actually converted
1879 + * name entries — the settings are worth a review, especially where one name
1880 + * fanned out to several fields (every match is now excluded, on purpose).
1881 + */
1882 +function mxchat_show_acf_exclusions_migrated_notice() {
1883 + if (!current_user_can('manage_options')) {
1884 + return;
1885 + }
1886 + if (get_option('mxchat_acf_exclusions_migrated_notice', '') !== '1') {
1887 + return;
1888 + }
1889 + ?>
1890 + <div class="notice notice-info is-dismissible">
1891 + <p>
1892 + <strong><?php esc_html_e('MxChat: ACF field exclusions updated', 'mxchat'); ?></strong><br>
1893 + <?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'); ?>
1894 + </p>
1895 + </div>
1896 + <?php
1897 + delete_option('mxchat_acf_exclusions_migrated_notice');
1898 +}
1899 +add_action('admin_notices', 'mxchat_show_acf_exclusions_migrated_notice');
1628 1900
1629 1901 /**
1630 1902 * Ensure tables exist on every admin load for fresh installations
1631 1903 * This is a safety net for cases where activation hook doesn't fire