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