| @@ -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 |