PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.17
MxChat – AI Chatbot & Content Generation for WordPress v3.2.17
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 +347 -29 3.2.63.2.17 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.6
6 + * Version: 3.2.17
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
@@ -23,8 +23,16 @@
23 23
24 24 if (!defined('MXCHAT_VERSION')) {
25 25 $plugin_data = get_file_data(__FILE__, array('Version' => 'Version'), 'plugin');
26 26 $version = $plugin_data['Version'];
27 + // MXCHAT_BASE_VERSION: the plain header version, stable across requests even in
28 + // dev mode. Use it for anything PERSISTED or COMPARED (the stored
29 + // mxchat_plugin_version option and the migration gate in
30 + // mxchat_check_for_update). MXCHAT_VERSION keeps the time() suffix in dev for
31 + // ASSET cache-busting only — persisting the suffixed value made the version
32 + // comparison churn every request, re-running the full activation/migration
33 + // suite per page load on dev installs.
34 + define('MXCHAT_BASE_VERSION', $version);
27 35 if (MXCHAT_DEV_MODE) {
28 36 $version .= '.' . time();
29 37 }
30 38 define('MXCHAT_VERSION', $version);
@@ -29,8 +37,27 @@
29 37 }
30 38 define('MXCHAT_VERSION', $version);
31 39 }
32 40
41 +/**
42 + * Honest, versioned User-Agent for MXChat remote-content ingestion fetches
43 + * (Knowledge Base PDF import, URL import, sitemap / website crawl).
44 + *
45 + * WAF rulesets (SiteGround/ModSecurity, Wordfence, Cloudflare managed rules)
46 + * flag stale spoofed-browser UAs as scrapers and return 403 — which silently
47 + * broke the single most common KB source: self-hosted media on the site's own
48 + * domain. A truthful crawler identifier is the industry norm for well-behaved
49 + * bots and lets a site owner allowlist "MXChatBot" in their WAF. Filterable so
50 + * a locked-down host can supply a different string without a code change.
51 + */
52 +if (!function_exists('mxchat_ingest_user_agent')) {
53 + function mxchat_ingest_user_agent() {
54 + $version = defined('MXCHAT_VERSION') ? MXCHAT_VERSION : '1.0';
55 + $ua = 'MXChatBot/' . $version . ' (+https://mxchat.ai/bot)';
56 + return apply_filters('mxchat_ingest_user_agent', $ua);
57 + }
58 +}
59 +
33 60 function mxchat_load_textdomain() {
34 61 $domain = 'mxchat';
35 62 $locale = determine_locale();
36 63
@@ -46,8 +73,84 @@
46 73 }
47 74 add_action('init', 'mxchat_load_textdomain');
48 75
49 76 /**
77 + * One-time migration: gemini-3-pro-preview was shut down by Google on March 9, 2026.
78 + * Existing installs with the dead ID get auto-remapped to gemini-3.1-pro-preview
79 + * (Google's official migration target) the first time admin_init fires after update.
80 + */
81 +add_action('admin_init', function () {
82 + if (get_option('mxchat_gemini_3_remap_done')) {
83 + return;
84 + }
85 + $opts = get_option('mxchat_options');
86 + if (is_array($opts) && isset($opts['model']) && $opts['model'] === 'gemini-3-pro-preview') {
87 + $opts['model'] = 'gemini-3.1-pro-preview';
88 + update_option('mxchat_options', $opts);
89 + }
90 + if (is_array($opts) && isset($opts['content_model']) && $opts['content_model'] === 'gemini-3-pro-preview') {
91 + $opts['content_model'] = 'gemini-3.1-pro-preview';
92 + update_option('mxchat_options', $opts);
93 + }
94 + update_option('mxchat_gemini_3_remap_done', 1);
95 +});
96 +
97 +/**
98 + * One-time migration: the Grok 2 family was retired by xAI (grok-2, grok-2-1212,
99 + * grok-2-latest, grok-2-vision-1212 all return 400 "Model not found"). Existing
100 + * installs with the dead ID get auto-remapped to grok-4-1-fast-non-reasoning
101 + * (modern, fast, broadly available) the first time admin_init fires after update.
102 + */
103 +add_action('admin_init', function () {
104 + if (get_option('mxchat_grok_2_remap_done')) {
105 + return;
106 + }
107 + $opts = get_option('mxchat_options');
108 + if (is_array($opts) && isset($opts['model']) && $opts['model'] === 'grok-2') {
109 + $opts['model'] = 'grok-4-1-fast-non-reasoning';
110 + update_option('mxchat_options', $opts);
111 + }
112 + if (is_array($opts) && isset($opts['content_model']) && $opts['content_model'] === 'grok-2') {
113 + $opts['content_model'] = 'grok-4-1-fast-non-reasoning';
114 + update_option('mxchat_options', $opts);
115 + }
116 + update_option('mxchat_grok_2_remap_done', 1);
117 +});
118 +
119 +/**
120 + * One-time migration: Anthropic retired the Claude 4 (2025-05-14) snapshots on
121 + * June 15, 2026 — claude-opus-4-20250514 and claude-sonnet-4-20250514 now return
122 + * an API error. Existing installs with a dead ID get auto-remapped to the current
123 + * equivalents Anthropic recommends (Opus 4.8 / Sonnet 4.6) the first time admin_init
124 + * fires after update. Mirrors the gemini-3-pro-preview / grok-2 rescues above.
125 + */
126 +add_action('admin_init', function () {
127 + if (get_option('mxchat_claude_4_retire_remap_done')) {
128 + return;
129 + }
130 + $map = array(
131 + 'claude-opus-4-20250514' => 'claude-opus-4-8',
132 + 'claude-sonnet-4-20250514' => 'claude-sonnet-4-6',
133 + );
134 + $opts = get_option('mxchat_options');
135 + if (is_array($opts)) {
136 + $changed = false;
137 + if (isset($opts['model']) && isset($map[$opts['model']])) {
138 + $opts['model'] = $map[$opts['model']];
139 + $changed = true;
140 + }
141 + if (isset($opts['content_model']) && isset($map[$opts['content_model']])) {
142 + $opts['content_model'] = $map[$opts['content_model']];
143 + $changed = true;
144 + }
145 + if ($changed) {
146 + update_option('mxchat_options', $opts);
147 + }
148 + }
149 + update_option('mxchat_claude_4_retire_remap_done', 1);
150 +});
151 +
152 +/**
50 153 * Exclude MxChat assets from caching plugin optimizations
51 154 *
52 155 * This prevents issues with WP Rocket, LiteSpeed Cache, Autoptimize, WP Super Cache,
53 156 * W3 Total Cache, SG Optimizer, and similar plugins that may break the chatbot by
@@ -306,17 +409,23 @@
306 409
307 410 // Include classes with error handling
308 411 function mxchat_include_classes() {
309 412 $class_files = array(
413 + 'includes/class-mxchat-model-catalog.php',
414 + 'includes/class-mxchat-live-agent-schedule.php',
415 + 'includes/class-mxchat-tool-registry.php',
310 416 'includes/class-mxchat-integrator.php',
311 417 'includes/class-mxchat-admin.php',
312 418 'includes/class-mxchat-public.php',
313 419 'includes/class-mxchat-utils.php',
314 420 'includes/class-mxchat-user.php',
421 + 'includes/class-mxchat-privacy.php',
315 422 'includes/class-mxchat-meta-box.php',
316 423 'includes/class-mxchat-chunker.php',
317 424 'includes/class-mxchat-word-handler.php',
318 425 'includes/class-mxchat-content-generator.php',
426 + 'includes/class-mxchat-cache-purge.php',
427 + 'includes/class-mxchat-editor-assistant.php',
319 428 'includes/class-rest-api.php',
320 429 'admin/class-ajax-handler.php',
321 430 'admin/class-pinecone-manager.php',
322 431 'admin/class-knowledge-manager.php'
@@ -330,8 +439,25 @@
330 439 //error_log('MxChat: Missing class file - ' . $file);
331 440 }
332 441 }
333 442
443 + // Register the native function-calling admin-post save handler (a41dee).
444 + if (class_exists('MxChat_Tool_Registry')) {
445 + MxChat_Tool_Registry::init();
446 + }
447 +
448 + // GDPR: register with WP's personal-data export/erase tools (b81e42).
449 + if (class_exists('MxChat_Privacy')) {
450 + MxChat_Privacy::init();
451 + }
452 +
453 + // Editor Assistant — free, OFF-by-default block-editor AI actions (plan-8cb0cb).
454 + // init() wires REST + streaming AJAX + sidebar enqueue ONLY when the
455 + // mxchat_editor_assistant_enabled option is 'on'; otherwise zero footprint.
456 + if (class_exists('MxChat_Editor_Assistant')) {
457 + MxChat_Editor_Assistant::init();
458 + }
459 +
334 460 // Admin pages that aren't classes (procedural include).
335 461 if (is_admin()) {
336 462 $admin_api_page = plugin_dir_path(__FILE__) . 'includes/admin-api-page.php';
337 463 if (file_exists($admin_api_page)) {
@@ -336,8 +462,18 @@
336 462 $admin_api_page = plugin_dir_path(__FILE__) . 'includes/admin-api-page.php';
337 463 if (file_exists($admin_api_page)) {
338 464 require_once $admin_api_page;
339 465 }
466 + // f7c7d4 renamed this file admin-dashboard-page.php → admin-onboarding-page.php.
467 + // The require MUST live here (admin bootstrap) and not just inside
468 + // mxchat_add_plugin_page() on the admin_menu hook — admin_menu does NOT
469 + // fire on admin-ajax.php requests, so the wizard's AJAX handlers
470 + // (plan-905439: mxchat_onboarding_kb_status / save_step / mark_step /
471 + // auto_graduate + the f7c7d4 dismiss handler) would never register.
472 + $admin_onboarding_page = plugin_dir_path(__FILE__) . 'includes/admin-onboarding-page.php';
473 + if (file_exists($admin_onboarding_page)) {
474 + require_once $admin_onboarding_page;
475 + }
340 476 }
341 477 }
342 478
343 479 /**
@@ -866,18 +1002,43 @@
866 1002 $migrated = true;
867 1003 $migration_message = 'Your chatbot model has been automatically updated from Claude Haiku 3.5 to Claude Haiku 4.5 due to Anthropic deprecating the older model.';
868 1004 }
869 1005
870 - // Migrate deprecated GPT-4 series and GPT-3.5 Turbo to GPT-5.1 Chat Latest
1006 + // Migrate deprecated GPT-4 series and GPT-3.5 Turbo to GPT-5.6 Sol.
1007 + // (Previous target gpt-5.1-chat-latest itself retires 2026-08-10 — never
1008 + // migrate onto a model that is already on a deprecation list.)
871 1009 if (in_array($current_model, array('gpt-4o', 'gpt-4.1-2025-04-14', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'), true)) {
872 - $options['model'] = 'gpt-5.1-chat-latest';
1010 + $options['model'] = 'gpt-5.6-sol';
873 1011 $migrated = true;
874 1012 $migration_message = sprintf(
875 - 'Your chatbot model has been automatically updated from %s to GPT-5.1 Chat Latest due to OpenAI deprecating older models.',
1013 + 'Your chatbot model has been automatically updated from %s to GPT-5.6 Sol due to OpenAI deprecating older models.',
876 1014 $current_model
877 1015 );
878 1016 }
879 1017
1018 + // Migrate the gpt-5.x-chat-latest aliases OpenAI retires on August 10, 2026.
1019 + // Replacement per OpenAI's deprecations page: gpt-5.6-sol (plan e46b8f).
1020 + if (in_array($current_model, array('gpt-5.1-chat-latest', 'gpt-5.3-chat-latest'), true)) {
1021 + $options['model'] = 'gpt-5.6-sol';
1022 + $migrated = true;
1023 + $migration_message = sprintf(
1024 + '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.',
1025 + $current_model
1026 + );
1027 + }
1028 +
1029 + // The content generator has its own model option — same retirement applies.
1030 + if (isset($options['content_model'])
1031 + && in_array($options['content_model'], array('gpt-5.1-chat-latest', 'gpt-5.3-chat-latest'), true)) {
1032 + $old_content_model = $options['content_model'];
1033 + $options['content_model'] = 'gpt-5.6-sol';
1034 + $migrated = true;
1035 + $migration_message = trim($migration_message . ' ' . sprintf(
1036 + 'Your content generation model has also been updated from %s to GPT-5.6 Sol for the same OpenAI retirement.',
1037 + $old_content_model
1038 + ));
1039 + }
1040 +
880 1041 // Migrate deprecated GPT-4o Mini and GPT-4.1 Mini to GPT-5 Mini
881 1042 if (in_array($current_model, array('gpt-4o-mini', 'gpt-4.1-mini'), true)) {
882 1043 $options['model'] = 'gpt-5-mini';
883 1044 $migrated = true;
@@ -886,8 +1047,21 @@
886 1047 $current_model
887 1048 );
888 1049 }
889 1050
1051 + // Migrate retired DeepSeek ids to DeepSeek V4 Flash — the vendor removed
1052 + // deepseek-chat and deepseek-reasoner on 2026-07-24 (hard cutoff, every
1053 + // request 400s). V4 Flash is DeepSeek's designated successor for the
1054 + // legacy deepseek-chat alias.
1055 + if (in_array($current_model, array('deepseek-chat', 'deepseek-reasoner'), true)) {
1056 + $options['model'] = 'deepseek-v4-flash';
1057 + $migrated = true;
1058 + $migration_message = sprintf(
1059 + 'Your chatbot model has been automatically updated from %s to DeepSeek V4 Flash because DeepSeek retired its older API models on July 24, 2026.',
1060 + $current_model
1061 + );
1062 + }
1063 +
890 1064 if ($migrated) {
891 1065 update_option('mxchat_options', $options);
892 1066 update_option('mxchat_model_migrated_notice', true);
893 1067 update_option('mxchat_model_migration_message', $migration_message);
@@ -912,8 +1086,46 @@
912 1086 delete_option('mxchat_model_migration_message');
913 1087 }
914 1088 }
915 1089
1090 +/**
1091 + * Persistent admin notice when the provider rejected the configured model
1092 + * (model_not_found / no access). Set by mxchat_friendly_chat_error() in the
1093 + * integrator whenever a chat request fails on a model-access error — including
1094 + * requests from anonymous visitors, which is the case that otherwise stays
1095 + * invisible to the site owner for weeks (plan e46b8f).
1096 + *
1097 + * Deleted after render so it re-arms on the next failed chat: the notice keeps
1098 + * reappearing until the model is fixed, then stops on its own.
1099 + */
1100 +function mxchat_show_model_access_notice() {
1101 + if (!current_user_can('manage_options')) {
1102 + return;
1103 + }
1104 + $notice = get_option('mxchat_model_access_notice');
1105 + if (!is_array($notice) || empty($notice['model'])) {
1106 + return;
1107 + }
1108 + $settings_url = admin_url('admin.php?page=mxchat-max');
1109 + ?>
1110 + <div class="notice notice-error is-dismissible">
1111 + <p>
1112 + <strong><?php esc_html_e('MxChat: your AI model is being rejected by the provider', 'mxchat'); ?></strong><br>
1113 + <?php
1114 + printf(
1115 + /* translators: 1: model id, 2: provider name */
1116 + esc_html__('Chat requests using the model "%1$s" are failing because %2$s reports it as unavailable on your API key (it may have been retired). Visitors may be seeing errors instead of replies.', 'mxchat'),
1117 + esc_html($notice['model']),
1118 + esc_html(!empty($notice['provider']) ? $notice['provider'] : __('the AI provider', 'mxchat'))
1119 + );
1120 + ?>
1121 + <a href="<?php echo esc_url($settings_url); ?>"><?php esc_html_e('Choose a different model in MxChat Settings', 'mxchat'); ?></a>
1122 + </p>
1123 + </div>
1124 + <?php
1125 + delete_option('mxchat_model_access_notice');
1126 +}
1127 +
916 1128 function mxchat_activate() {
917 1129 global $wpdb;
918 1130 $charset_collate = $wpdb->get_charset_collate();
919 1131
@@ -1034,10 +1246,11 @@
1034 1246
1035 1247 // Setup cron jobs
1036 1248 mxchat_setup_cron_jobs();
1037 1249
1038 - // Update version
1039 - update_option('mxchat_plugin_version', MXCHAT_VERSION);
1250 + // Update version (stable base version — never the dev time()-suffixed one,
1251 + // or the check_for_update comparison would churn every request)
1252 + update_option('mxchat_plugin_version', MXCHAT_BASE_VERSION);
1040 1253
1041 1254 //error_log("MxChat: Activation function completed");
1042 1255 }
1043 1256
@@ -1052,23 +1265,29 @@
1052 1265 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
1053 1266 // Set flag to use fallback system
1054 1267 update_option('mxchat_use_fallback_rate_limits', true);
1055 1268 update_option('mxchat_next_rate_limit_check', time() + 3600);
1056 - return;
1057 - }
1058 -
1059 - // Schedule the rate limit reset cron job
1060 - $result = wp_schedule_event(time() + 300, 'hourly', 'mxchat_reset_rate_limits');
1061 -
1062 - if ($result === false) {
1063 - // Fallback if scheduling fails
1064 - update_option('mxchat_use_fallback_rate_limits', true);
1065 - update_option('mxchat_next_rate_limit_check', time() + 3600);
1269 + // Deliberately NO early return (plan-bc08a6): transcript cleanup below
1270 + // must still be scheduled. DISABLE_WP_CRON only changes HOW cron events
1271 + // execute (a server-side runner hitting wp-cron.php instead of loopback
1272 + // spawns) — scheduling still just writes the cron option. The old early
1273 + // return here meant a deactivate/reactivate cycle on a DISABLE_WP_CRON
1274 + // site permanently lost the transcript cleanup event while the retention
1275 + // setting still claimed to be active.
1066 1276 } else {
1067 - // Clear fallback flags if cron scheduling succeeded
1068 - delete_option('mxchat_use_fallback_rate_limits');
1277 + // Schedule the rate limit reset cron job
1278 + $result = wp_schedule_event(time() + 300, 'hourly', 'mxchat_reset_rate_limits');
1279 +
1280 + if ($result === false) {
1281 + // Fallback if scheduling fails
1282 + update_option('mxchat_use_fallback_rate_limits', true);
1283 + update_option('mxchat_next_rate_limit_check', time() + 3600);
1284 + } else {
1285 + // Clear fallback flags if cron scheduling succeeded
1286 + delete_option('mxchat_use_fallback_rate_limits');
1287 + }
1069 1288 }
1070 -
1289 +
1071 1290 // Schedule transcript cleanup if configured (bucket dropdown OR custom retention-days > 0)
1072 1291 $transcript_options = get_option('mxchat_transcripts_options', array());
1073 1292 $cleanup_interval = isset($transcript_options['mxchat_auto_delete_transcripts']) ? $transcript_options['mxchat_auto_delete_transcripts'] : 'never';
1074 1293 $custom_retention = isset($transcript_options['mxchat_retention_days']) ? (int) $transcript_options['mxchat_retention_days'] : 0;
@@ -1111,17 +1330,25 @@
1111 1330 return;
1112 1331 }
1113 1332
1114 1333 $next_check = get_option('mxchat_next_rate_limit_check', 0);
1115 -
1334 +
1116 1335 if (time() >= $next_check) {
1117 - // Only run reset if the MxChat_Integrator class exists
1118 - if (class_exists('MxChat_Integrator')) {
1119 - $integrator = new MxChat_Integrator();
1120 - if (method_exists($integrator, 'mxchat_reset_rate_limits')) {
1121 - $integrator->mxchat_reset_rate_limits();
1122 - update_option('mxchat_next_rate_limit_check', time() + 3600);
1123 - }
1336 + // Reuse the bootstrap's integrator — mxchat_init() creates the global on
1337 + // plugins_loaded (before this init-priority-5 callback), so it's always set
1338 + // here. Constructing a second MxChat_Integrator just to call one method
1339 + // re-registers every hook the plugin has (ajax pairs, wp_footer loader,
1340 + // rest_api_init, admin_init guard) on a duplicate instance for the rest of
1341 + // the request. Defensive construction only if the global is somehow unset.
1342 + // NOTE: MxChat_Integrator::check_fallback_rate_limits() is a second
1343 + // implementation of this same check — if either changes, change both.
1344 + global $mxchat_integrator;
1345 + $integrator = ($mxchat_integrator instanceof MxChat_Integrator)
1346 + ? $mxchat_integrator
1347 + : (class_exists('MxChat_Integrator') ? new MxChat_Integrator() : null);
1348 + if ($integrator && method_exists($integrator, 'mxchat_reset_rate_limits')) {
1349 + $integrator->mxchat_reset_rate_limits();
1350 + update_option('mxchat_next_rate_limit_check', time() + 3600);
1124 1351 }
1125 1352 }
1126 1353 }
1127 1354
@@ -1133,9 +1360,9 @@
1133 1360 global $wpdb;
1134 1361
1135 1362 try {
1136 1363 $current_version = get_option('mxchat_plugin_version', '0.0.0');
1137 - $plugin_version = MXCHAT_VERSION;
1364 + $plugin_version = MXCHAT_BASE_VERSION;
1138 1365
1139 1366 // Always ensure critical tables exist (even if version matches)
1140 1367 // This handles manual table deletion or fresh installs
1141 1368 $chat_table = $wpdb->prefix . 'mxchat_chat_transcripts';
@@ -1214,8 +1441,32 @@
1214 1441 if (version_compare($current_version, '3.2.4', '<')) {
1215 1442 mxchat_backfill_active_embedding_model();
1216 1443 }
1217 1444
1445 + // 3.2.15: Migrate retired DeepSeek ids (deepseek-chat / deepseek-reasoner
1446 + // were shut off at the vendor on 2026-07-24). The function is idempotent —
1447 + // it only rewrites models on its deprecation lists.
1448 + if (version_compare($current_version, '3.2.15', '<')) {
1449 + mxchat_migrate_deprecated_models();
1450 + }
1451 +
1452 + // 3.2.16: Migrate OpenAI ids retiring 2026-08-10 (gpt-5.1-chat-latest /
1453 + // gpt-5.3-chat-latest → gpt-5.6-sol per OpenAI's deprecations page).
1454 + // Idempotent — only rewrites models on the deprecation lists (e46b8f).
1455 + if (version_compare($current_version, '3.2.16', '<')) {
1456 + mxchat_migrate_deprecated_models();
1457 + }
1458 +
1459 + // 3.2.17: Credential options must not autoload (af2400) — the two
1460 + // Pinecone-secret-holding rows were in alloptions, i.e. read into
1461 + // memory on every request including anonymous page views. Idempotent.
1462 + // Also carry the import modal's remembered ACF→PDF checkbox state
1463 + // into the new install-level option (11720c).
1464 + if (version_compare($current_version, '3.2.17', '<')) {
1465 + mxchat_fix_credential_option_autoload();
1466 + mxchat_migrate_acf_pdf_extraction_option();
1467 + }
1468 +
1218 1469 // Run full activation to ensure everything is up to date
1219 1470 mxchat_activate();
1220 1471
1221 1472 // Run migration functions
@@ -1238,8 +1489,67 @@
1238 1489 }
1239 1490 }
1240 1491
1241 1492 /**
1493 + * Credential options must never enter the autoloaded alloptions set.
1494 + * mxchat_prompts_options and mxchat_pinecone_addon_options can hold the
1495 + * Pinecone API secret; mxchat_options already stores its keys with autoload
1496 + * off and these two must match it. The filter covers every future
1497 + * add_option()/update_option() that creates the row — Settings API saves
1498 + * through options.php and WP-CLI included — on WP 6.6+; older cores are
1499 + * covered by the explicit autoload arguments at the plugin's own write
1500 + * sites plus the one-time migration below.
1501 + */
1502 +add_filter('wp_default_autoload_value', 'mxchat_credential_option_autoload_value', 10, 2);
1503 +function mxchat_credential_option_autoload_value($autoload, $option) {
1504 + if (in_array($option, array('mxchat_prompts_options', 'mxchat_pinecone_addon_options'), true)) {
1505 + return false;
1506 + }
1507 + return $autoload;
1508 +}
1509 +
1510 +/**
1511 + * One-time upgrade migration: flip the autoload flag on credential option
1512 + * rows that existing installs are already carrying autoloaded. Includes
1513 + * mxchat_adv_api_token (Advanced Content bearer token) — harmless no-op
1514 + * when that add-on is not installed, since missing rows simply don't match.
1515 + */
1516 +function mxchat_fix_credential_option_autoload() {
1517 + $keys = array('mxchat_prompts_options', 'mxchat_pinecone_addon_options', 'mxchat_adv_api_token');
1518 + if (function_exists('wp_set_option_autoload_values')) {
1519 + wp_set_option_autoload_values(array_fill_keys($keys, false));
1520 + return;
1521 + }
1522 + // Pre-WP-6.4 fallback: direct flip + cache invalidation.
1523 + global $wpdb;
1524 + $placeholders = implode(',', array_fill(0, count($keys), '%s'));
1525 + $wpdb->query($wpdb->prepare("UPDATE {$wpdb->options} SET autoload = 'no' WHERE option_name IN ($placeholders)", $keys));
1526 + wp_cache_delete('alloptions', 'options');
1527 + foreach ($keys as $key) {
1528 + wp_cache_delete($key, 'options');
1529 + }
1530 +}
1531 +
1532 +/**
1533 + * One-time carry of the import modal's remembered ACF→PDF checkbox state
1534 + * (mxchat_options['acf_pdf_extract_default'], written per-import until 3.2.16)
1535 + * into the new install-level option mxchat_acf_pdf_extraction (plan 11720c).
1536 + * Fresh installs and installs that never touched the checkbox default OFF,
1537 + * matching the setting's own "recommended only if…" guidance.
1538 + */
1539 +function mxchat_migrate_acf_pdf_extraction_option() {
1540 + if (get_option('mxchat_acf_pdf_extraction', null) !== null) {
1541 + return; // already set — never overwrite an owner's choice
1542 + }
1543 + $mxchat_options = get_option('mxchat_options', array());
1544 + if (is_array($mxchat_options) && array_key_exists('acf_pdf_extract_default', $mxchat_options)) {
1545 + update_option('mxchat_acf_pdf_extraction', !empty($mxchat_options['acf_pdf_extract_default']) ? '1' : '0', false);
1546 + unset($mxchat_options['acf_pdf_extract_default']);
1547 + update_option('mxchat_options', $mxchat_options);
1548 + }
1549 +}
1550 +
1551 +/**
1242 1552 * Ensure tables exist on every admin load for fresh installations
1243 1553 * This is a safety net for cases where activation hook doesn't fire
1244 1554 */
1245 1555 function mxchat_ensure_tables_exist() {
@@ -1414,8 +1724,9 @@
1414 1724 add_action('init', 'mxchat_check_fallback_rate_limits', 5);
1415 1725
1416 1726 // Add migration notice hook
1417 1727 add_action('admin_notices', 'mxchat_show_migration_notice');
1728 + add_action('admin_notices', 'mxchat_show_model_access_notice');
1418 1729
1419 1730 // Initialize classes with error handling
1420 1731 try {
1421 1732 // Initialize admin classes
@@ -1440,8 +1751,15 @@
1440 1751 if (class_exists('MxChat_Content_Generator')) {
1441 1752 new MxChat_Content_Generator();
1442 1753 }
1443 1754
1755 + // Initialize cache purge globally — settings writes can happen on any
1756 + // request type (admin screens, admin-ajax autosave, wp-cli), and the
1757 + // deferred-purge cron event fires on front-end requests.
1758 + if (class_exists('MxChat_Cache_Purge')) {
1759 + MxChat_Cache_Purge::init();
1760 + }
1761 +
1444 1762 // Initialize REST API globally — endpoints must be registered on
1445 1763 // every request (admin and frontend) so they're reachable via /wp-json/.
1446 1764 // Endpoints are auth-gated and locked until the site owner generates
1447 1765 // a token in MxChat → API Access.
@@ -1566,9 +1884,9 @@
1566 1884 */
1567 1885 function mxchat_save_session_rating() {
1568 1886 global $wpdb;
1569 1887
1570 - $session_id = isset($_POST['session_id']) ? sanitize_text_field(wp_unslash($_POST['session_id'])) : '';
1888 + $session_id = isset($_POST['session_id']) ? MxChat_Utils::sanitize_session_id(wp_unslash($_POST['session_id'])) : '';
1571 1889 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field(wp_unslash($_POST['bot_id'])) : 'default';
1572 1890 $rating_raw = isset($_POST['rating']) ? (int) $_POST['rating'] : 0;
1573 1891 $feedback = isset($_POST['feedback']) ? sanitize_textarea_field(wp_unslash($_POST['feedback'])) : '';
1574 1892