PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.25
MxChat – AI Chatbot & Content Generation for WordPress v3.2.25
3.2.25 3.2.24 3.2.23 3.2.22 3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 All 156 releases
mxchat-basic / mxchat-basic.php

mxchat-basic.php in MxChat – AI Chatbot & Content Generation for WordPress 3.2.25, at mxchat-basic.php

2,388 lines 93.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: MxChat
4 * Plugin URI: https://mxchat.ai/
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.25
7 * Author: MxChat
8 * Author URI: https://mxchat.ai
9 * License: GPLv2 or later
10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
11 * Text Domain: mxchat
12 * Domain Path: /languages
13 */
14
15 if (!defined('ABSPATH')) {
16 exit; // Exit if accessed directly.
17 }
18
19
20 if (!defined('MXCHAT_DEV_MODE')) {
21 define('MXCHAT_DEV_MODE', false);
22 }
23
24 if (!defined('MXCHAT_VERSION')) {
25 $plugin_data = get_file_data(__FILE__, array('Version' => 'Version'), 'plugin');
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);
35 if (MXCHAT_DEV_MODE) {
36 $version .= '.' . time();
37 }
38 define('MXCHAT_VERSION', $version);
39 }
40
41 /**
42 * Toolbar items API level (plan-mxchat-20260916-65c9b6). Defined only by cores
43 * that apply the `mxchat_chat_toolbar_items` filter while rendering the chat
44 * toolbar and fire the `mxchat:toolbar-ready` / `mxchat:opened` document events.
45 * Add-ons test `defined('MXCHAT_TOOLBAR_ITEMS_API')` and fall back to their
46 * older DOM-injection path on cores without it, so a button is never rendered
47 * twice. Bump the value only if the item shape (id / html / order) changes.
48 */
49 if (!defined('MXCHAT_TOOLBAR_ITEMS_API')) {
50 define('MXCHAT_TOOLBAR_ITEMS_API', 1);
51 }
52
53 /**
54 * Default confidence floor for the in-chat YouTube card, as a percentage
55 * (plan-mxchat-20260813-f52492). Higher than the site-wide Similarity
56 * Threshold default of 35 by design — see MxChat_Utils::video_embed_threshold().
57 * Declared here so the gate, the admin field and the tests all read ONE number.
58 */
59 if (!defined('MXCHAT_VIDEO_EMBED_THRESHOLD_DEFAULT')) {
60 define('MXCHAT_VIDEO_EMBED_THRESHOLD_DEFAULT', 55);
61 }
62
63 /**
64 * One-time install stamp: records the plain version this site first ran, so
65 * behavior defaults can differ between fresh installs and upgrades without
66 * touching anyone's stored settings. Mirrors mxchat-mcp's 1.0.7 stamp shape
67 * (plan 7b578e); first consumer is the "Strip unapproved links" default
68 * (plan 58f8b4). Runs at init priority 1 — BEFORE initialize_default_options
69 * (init 20) writes mxchat_options on a fresh site's first request, because
70 * that option's pre-existing presence is how an upgrade is recognized.
71 */
72 function mxchat_stamp_install_version() {
73 if (get_option('mxchat_installed_at_version', '') !== '') {
74 return;
75 }
76 $existing = get_option('mxchat_options', false) !== false;
77 $version = defined('MXCHAT_BASE_VERSION') ? MXCHAT_BASE_VERSION : '0.0.0';
78 update_option('mxchat_installed_at_version', $existing ? 'legacy' : $version, false);
79 }
80 add_action('init', 'mxchat_stamp_install_version', 1);
81
82 /**
83 * Stamp-derived default for the "Strip unapproved links" toggle (plan 58f8b4,
84 * option-c split of the old Citation Links conflation): 'on' only for installs
85 * born at 3.2.20+. A missing or 'legacy' stamp means the site predates the
86 * setting — keep 'off' so no existing site's links start vanishing on update.
87 */
88 function mxchat_strip_unapproved_links_default() {
89 $stamp = get_option('mxchat_installed_at_version', '');
90 if ($stamp === '' || $stamp === 'legacy') {
91 return 'off';
92 }
93 return version_compare($stamp, '3.2.20', '>=') ? 'on' : 'off';
94 }
95
96 /**
97 * Effective state of "Strip unapproved links": an explicitly saved option
98 * always wins; until one exists the install-stamp default governs. Read via
99 * a fresh get_option on purpose — the response URL guard runs late in the
100 * request and must see a value saved moments earlier.
101 */
102 function mxchat_strip_unapproved_links_enabled() {
103 $opts = get_option('mxchat_options', array());
104 if (is_array($opts) && isset($opts['strip_unapproved_links_toggle'])) {
105 return $opts['strip_unapproved_links_toggle'] === 'on';
106 }
107 return mxchat_strip_unapproved_links_default() === 'on';
108 }
109
110 /**
111 * Honest, versioned User-Agent for MXChat remote-content ingestion fetches
112 * (Knowledge Base PDF import, URL import, sitemap / website crawl).
113 *
114 * WAF rulesets (SiteGround/ModSecurity, Wordfence, Cloudflare managed rules)
115 * flag stale spoofed-browser UAs as scrapers and return 403 — which silently
116 * broke the single most common KB source: self-hosted media on the site's own
117 * domain. A truthful crawler identifier is the industry norm for well-behaved
118 * bots and lets a site owner allowlist "MXChatBot" in their WAF. Filterable so
119 * a locked-down host can supply a different string without a code change.
120 */
121 if (!function_exists('mxchat_ingest_user_agent')) {
122 function mxchat_ingest_user_agent() {
123 $version = defined('MXCHAT_VERSION') ? MXCHAT_VERSION : '1.0';
124 $ua = 'MXChatBot/' . $version . ' (+https://mxchat.ai/bot)';
125 return apply_filters('mxchat_ingest_user_agent', $ua);
126 }
127 }
128
129 function mxchat_load_textdomain() {
130 $domain = 'mxchat';
131 $locale = determine_locale();
132
133 // First, try to load from /wp-content/languages/plugins/ (preserved during updates)
134 $mo_file = WP_LANG_DIR . '/plugins/' . $domain . '-' . $locale . '.mo';
135 if (file_exists($mo_file)) {
136 load_textdomain($domain, $mo_file);
137 return;
138 }
139
140 // Fallback to plugin's /languages directory
141 load_plugin_textdomain($domain, false, dirname(plugin_basename(__FILE__)) . '/languages');
142 }
143 add_action('init', 'mxchat_load_textdomain');
144
145 /**
146 * One-time migration: gemini-3-pro-preview was shut down by Google on March 9, 2026.
147 * Existing installs with the dead ID get auto-remapped to gemini-3.1-pro-preview
148 * (Google's official migration target) the first time admin_init fires after update.
149 */
150 add_action('admin_init', function () {
151 if (get_option('mxchat_gemini_3_remap_done')) {
152 return;
153 }
154 $opts = get_option('mxchat_options');
155 if (is_array($opts) && isset($opts['model']) && $opts['model'] === 'gemini-3-pro-preview') {
156 $opts['model'] = 'gemini-3.1-pro-preview';
157 update_option('mxchat_options', $opts);
158 }
159 if (is_array($opts) && isset($opts['content_model']) && $opts['content_model'] === 'gemini-3-pro-preview') {
160 $opts['content_model'] = 'gemini-3.1-pro-preview';
161 update_option('mxchat_options', $opts);
162 }
163 update_option('mxchat_gemini_3_remap_done', 1);
164 });
165
166 /**
167 * One-time migration: the Grok 2 family was retired by xAI (grok-2, grok-2-1212,
168 * grok-2-latest, grok-2-vision-1212 all return 400 "Model not found"). Existing
169 * installs with the dead ID get auto-remapped to grok-4-1-fast-non-reasoning
170 * (modern, fast, broadly available) the first time admin_init fires after update.
171 */
172 add_action('admin_init', function () {
173 if (get_option('mxchat_grok_2_remap_done')) {
174 return;
175 }
176 $opts = get_option('mxchat_options');
177 if (is_array($opts) && isset($opts['model']) && $opts['model'] === 'grok-2') {
178 $opts['model'] = 'grok-4-1-fast-non-reasoning';
179 update_option('mxchat_options', $opts);
180 }
181 if (is_array($opts) && isset($opts['content_model']) && $opts['content_model'] === 'grok-2') {
182 $opts['content_model'] = 'grok-4-1-fast-non-reasoning';
183 update_option('mxchat_options', $opts);
184 }
185 update_option('mxchat_grok_2_remap_done', 1);
186 });
187
188 /**
189 * One-time migration: Anthropic retired the Claude 4 (2025-05-14) snapshots on
190 * June 15, 2026 — claude-opus-4-20250514 and claude-sonnet-4-20250514 now return
191 * an API error. Existing installs with a dead ID get auto-remapped to the current
192 * equivalents Anthropic recommends (Opus 4.8 / Sonnet 4.6) the first time admin_init
193 * fires after update. Mirrors the gemini-3-pro-preview / grok-2 rescues above.
194 */
195 add_action('admin_init', function () {
196 if (get_option('mxchat_claude_4_retire_remap_done')) {
197 return;
198 }
199 $map = array(
200 'claude-opus-4-20250514' => 'claude-opus-4-8',
201 'claude-sonnet-4-20250514' => 'claude-sonnet-4-6',
202 );
203 $opts = get_option('mxchat_options');
204 if (is_array($opts)) {
205 $changed = false;
206 if (isset($opts['model']) && isset($map[$opts['model']])) {
207 $opts['model'] = $map[$opts['model']];
208 $changed = true;
209 }
210 if (isset($opts['content_model']) && isset($map[$opts['content_model']])) {
211 $opts['content_model'] = $map[$opts['content_model']];
212 $changed = true;
213 }
214 if ($changed) {
215 update_option('mxchat_options', $opts);
216 }
217 }
218 update_option('mxchat_claude_4_retire_remap_done', 1);
219 });
220
221 /**
222 * Exclude MxChat assets from caching plugin optimizations
223 *
224 * This prevents issues with WP Rocket, LiteSpeed Cache, Autoptimize, WP Super Cache,
225 * W3 Total Cache, SG Optimizer, and similar plugins that may break the chatbot by
226 * removing "unused" CSS, minifying/combining JS, or deferring/delaying jQuery.
227 *
228 * Both chat-script.js and floating-script.js depend on jQuery, so jQuery must also
229 * be excluded from any optimization that changes load order or timing.
230 */
231
232 // ── WP Rocket ────────────────────────────────────────────────────────────────
233
234 // Exclude from Remove Unused CSS (RUCSS)
235 add_filter('rocket_rucss_inline_atts_exclusions', function($exclusions) {
236 if (!is_array($exclusions)) $exclusions = array();
237 $exclusions[] = 'mxchat';
238 return $exclusions;
239 });
240
241 // Exclude CSS from minification/combination
242 add_filter('rocket_exclude_css', function($excluded) {
243 if (!is_array($excluded)) $excluded = array();
244 $excluded[] = '/plugins/mxchat-basic/css/chat-style.css';
245 return $excluded;
246 });
247
248 // Exclude JS from minification/combination
249 add_filter('rocket_exclude_js', function($excluded) {
250 if (!is_array($excluded)) $excluded = array();
251 $excluded[] = '/plugins/mxchat-basic/js/chat-script.js';
252 $excluded[] = '/plugins/mxchat-basic/js/floating-script.js';
253 $excluded[] = '/jquery-core';
254 $excluded[] = '/jquery.min.js';
255 $excluded[] = '/jquery.js';
256 $excluded[] = '/jquery-migrate';
257 return $excluded;
258 });
259
260 // Exclude JS from defer
261 add_filter('rocket_exclude_defer_js', function($excluded) {
262 if (!is_array($excluded)) $excluded = array();
263 $excluded[] = '/plugins/mxchat-basic/js/chat-script.js';
264 $excluded[] = '/plugins/mxchat-basic/js/floating-script.js';
265 $excluded[] = '/jquery-core';
266 $excluded[] = '/jquery.min.js';
267 $excluded[] = '/jquery.js';
268 $excluded[] = '/jquery-migrate';
269 return $excluded;
270 });
271
272 // Exclude from delay JS execution
273 add_filter('rocket_delay_js_exclusions', function($excluded) {
274 if (!is_array($excluded)) $excluded = array();
275 $excluded[] = 'mxchat';
276 $excluded[] = 'chat-script';
277 $excluded[] = 'floating-script';
278 $excluded[] = '/jquery-core';
279 $excluded[] = '/jquery.min.js';
280 $excluded[] = '/jquery.js';
281 $excluded[] = '/jquery-migrate';
282 return $excluded;
283 });
284
285 // ── LiteSpeed Cache ──────────────────────────────────────────────────────────
286
287 // Exclude CSS from optimization
288 add_filter('litespeed_optimize_css_excludes', function($excluded) {
289 if (!is_array($excluded)) $excluded = array();
290 $excluded[] = 'chat-style.css';
291 $excluded[] = 'mxchat';
292 return $excluded;
293 });
294
295 // Exclude from UCSS (Unique CSS) - prevents LiteSpeed from stripping "unused" MxChat CSS
296 add_filter('litespeed_ucss_whitelist', function($whitelist) {
297 if (!is_array($whitelist)) $whitelist = array();
298 $whitelist[] = '.mxchat-chatbot-wrapper';
299 $whitelist[] = '.floating-chatbot';
300 $whitelist[] = '.floating-chatbot-button';
301 $whitelist[] = '.chatbot-top-bar';
302 $whitelist[] = '.mxchat-chatbot';
303 $whitelist[] = '.chat-container';
304 $whitelist[] = '.chat-box';
305 $whitelist[] = '.bot-message';
306 $whitelist[] = '.input-container';
307 $whitelist[] = '.chat-input';
308 $whitelist[] = '.send-button';
309 $whitelist[] = '.pre-chat-message';
310 $whitelist[] = '.mxchat-popular-questions';
311 $whitelist[] = '.chat-toolbar';
312 $whitelist[] = '.exit-chat';
313 $whitelist[] = '.email-blocker';
314 return $whitelist;
315 });
316
317 // Exclude CSS from CCSS (Critical CSS) generation
318 add_filter('litespeed_optm_ccss_exc', function($excluded) {
319 if (!is_array($excluded)) $excluded = array();
320 $excluded[] = 'chat-style.css';
321 $excluded[] = 'mxchat';
322 return $excluded;
323 });
324
325 // Exclude JS from defer
326 add_filter('litespeed_optm_js_defer_exc', function($excluded) {
327 if (!is_array($excluded)) $excluded = array();
328 $excluded[] = 'chat-script.js';
329 $excluded[] = 'floating-script.js';
330 $excluded[] = 'mxchat';
331 $excluded[] = 'jquery.min.js';
332 $excluded[] = 'jquery.js';
333 return $excluded;
334 });
335
336 // Exclude JS from combining
337 add_filter('litespeed_optm_js_exc', function($excluded) {
338 if (!is_array($excluded)) $excluded = array();
339 $excluded[] = 'chat-script.js';
340 $excluded[] = 'floating-script.js';
341 $excluded[] = 'mxchat';
342 $excluded[] = 'jquery.min.js';
343 $excluded[] = 'jquery.js';
344 return $excluded;
345 });
346
347 // Exclude JS from delayed execution
348 add_filter('litespeed_optm_js_delay_exc', function($excluded) {
349 if (!is_array($excluded)) $excluded = array();
350 $excluded[] = 'chat-script.js';
351 $excluded[] = 'floating-script.js';
352 $excluded[] = 'mxchat';
353 return $excluded;
354 });
355
356 // Exclude from Guest Mode optimization
357 add_filter('litespeed_guest_optm_exc', function($excluded) {
358 if (!is_array($excluded)) $excluded = array();
359 $excluded[] = 'mxchat';
360 $excluded[] = 'chat-style';
361 $excluded[] = 'chat-script';
362 $excluded[] = 'floating-script';
363 return $excluded;
364 });
365
366 // ── Autoptimize ──────────────────────────────────────────────────────────────
367
368 // Exclude CSS from optimization (comma-separated strings)
369 add_filter('autoptimize_filter_css_exclude', function($excluded) {
370 if (!is_string($excluded)) $excluded = '';
371 return $excluded . ', mxchat, chat-style.css';
372 });
373
374 // Exclude JS from optimization (comma-separated strings)
375 add_filter('autoptimize_filter_js_exclude', function($excluded) {
376 if (!is_string($excluded)) $excluded = '';
377 return $excluded . ', mxchat, chat-script.js, floating-script.js, jquery.min.js, jquery.js';
378 });
379
380 // ── SG Optimizer (SiteGround) ────────────────────────────────────────────────
381
382 add_filter('sgo_js_minify_exclude', function($excluded) {
383 if (!is_array($excluded)) $excluded = array();
384 $excluded[] = 'chat-script.js';
385 $excluded[] = 'floating-script.js';
386 $excluded[] = 'jquery.min.js';
387 return $excluded;
388 });
389
390 add_filter('sgo_javascript_combine_exclude', function($excluded) {
391 if (!is_array($excluded)) $excluded = array();
392 $excluded[] = 'chat-script.js';
393 $excluded[] = 'floating-script.js';
394 $excluded[] = 'jquery.min.js';
395 return $excluded;
396 });
397
398 add_filter('sgo_js_async_exclude', function($excluded) {
399 if (!is_array($excluded)) $excluded = array();
400 $excluded[] = 'chat-script.js';
401 $excluded[] = 'floating-script.js';
402 $excluded[] = 'jquery.min.js';
403 return $excluded;
404 });
405
406 // ── W3 Total Cache ───────────────────────────────────────────────────────────
407
408 add_filter('w3tc_minify_js_do_tag_minification', function($do_minify, $script_tag, $file) {
409 if (strpos($file, 'chat-script.js') !== false ||
410 strpos($file, 'floating-script.js') !== false ||
411 strpos($file, 'jquery.min.js') !== false ||
412 strpos($file, 'jquery.js') !== false) {
413 return false;
414 }
415 return $do_minify;
416 }, 10, 3);
417
418 // ── WP Super Cache ──────────────────────────────────────────────────────────
419
420 add_filter('wpsc_rejected_uri', function($rejected) {
421 if (!is_array($rejected)) $rejected = array();
422 $rejected[] = 'wp-admin/admin-ajax.php';
423 return $rejected;
424 });
425
426 // ── Page-cache bypass for chat AJAX (companion to the 3.2.6 nonce-race hotfix)
427 // Each cache plugin gets its own filter export so that visitors hitting an
428 // edge-cached page never receive cached chat-AJAX responses. The chat send /
429 // stream send / file upload all POST to /wp-admin/admin-ajax.php with
430 // `action=mxchat_*`. Without these exports, a cache plugin can stale a response
431 // and break the per-session nonce flow on the first message.
432
433 // WP Rocket — `rocket_cache_reject_uri` takes a flat array of regex strings.
434 add_filter('rocket_cache_reject_uri', function($uris) {
435 if (!is_array($uris)) $uris = array();
436 $uris[] = '/wp-admin/admin-ajax\.php\?action=mxchat_.*';
437 return $uris;
438 });
439
440 // LiteSpeed Cache — `litespeed_cache_no_cache_for_request` short-circuits
441 // caching when the request matches our chat-AJAX pattern.
442 add_filter('litespeed_cache_no_cache_for_request', function($no_cache) {
443 if ($no_cache) return $no_cache;
444 if (!empty($_SERVER['REQUEST_URI']) &&
445 strpos($_SERVER['REQUEST_URI'], '/wp-admin/admin-ajax.php') !== false &&
446 !empty($_REQUEST['action']) &&
447 strpos((string) $_REQUEST['action'], 'mxchat_') === 0) {
448 return true;
449 }
450 return $no_cache;
451 });
452
453 // W3 Total Cache — `w3tc_pgcache_request_skip_uri` flips page-cache off when
454 // the URI matches.
455 add_filter('w3tc_pgcache_request_skip_uri', function($skip) {
456 if ($skip) return $skip;
457 if (!empty($_SERVER['REQUEST_URI']) &&
458 strpos($_SERVER['REQUEST_URI'], '/wp-admin/admin-ajax.php') !== false &&
459 !empty($_REQUEST['action']) &&
460 strpos((string) $_REQUEST['action'], 'mxchat_') === 0) {
461 return true;
462 }
463 return $skip;
464 });
465
466 // FlyingPress — `flying_press_cacheable` takes a boolean and is run per
467 // request. Same pattern as LiteSpeed / W3TC.
468 add_filter('flying_press_cacheable', function($cacheable) {
469 if (!$cacheable) return $cacheable;
470 if (!empty($_SERVER['REQUEST_URI']) &&
471 strpos($_SERVER['REQUEST_URI'], '/wp-admin/admin-ajax.php') !== false &&
472 !empty($_REQUEST['action']) &&
473 strpos((string) $_REQUEST['action'], 'mxchat_') === 0) {
474 return false;
475 }
476 return $cacheable;
477 });
478
479 // Include classes with error handling
480 function mxchat_include_classes() {
481 $class_files = array(
482 'includes/class-mxchat-model-catalog.php',
483 'includes/class-mxchat-model-liveness.php',
484 'includes/class-mxchat-license-check.php',
485 'includes/class-mxchat-session-store.php',
486 'includes/class-mxchat-live-agent-schedule.php',
487 'includes/class-mxchat-tool-registry.php',
488 'includes/class-mxchat-integrator.php',
489 'includes/class-mxchat-admin.php',
490 'includes/class-mxchat-public.php',
491 'includes/class-mxchat-block.php',
492 'includes/class-mxchat-elementor.php',
493 'includes/class-mxchat-utils.php',
494 'includes/class-mxchat-user.php',
495 'includes/class-mxchat-privacy.php',
496 'includes/class-mxchat-meta-box.php',
497 'includes/class-mxchat-chunker.php',
498 'includes/class-mxchat-word-handler.php',
499 'includes/class-mxchat-content-generator.php',
500 'includes/class-mxchat-cache-purge.php',
501 'includes/class-mxchat-editor-assistant.php',
502 'includes/class-rest-api.php',
503 'admin/class-ajax-handler.php',
504 'admin/class-pinecone-manager.php',
505 'admin/class-pinecone-documents.php',
506 'admin/class-knowledge-manager.php',
507 'admin/class-vectorstore-manager.php'
508 );
509
510 foreach ($class_files as $file) {
511 $file_path = plugin_dir_path(__FILE__) . $file;
512 if (file_exists($file_path)) {
513 require_once $file_path;
514 } else {
515 //error_log('MxChat: Missing class file - ' . $file);
516 }
517 }
518
519 // Register the native function-calling admin-post save handler (a41dee).
520 if (class_exists('MxChat_Tool_Registry')) {
521 MxChat_Tool_Registry::init();
522 }
523
524 // GDPR: register with WP's personal-data export/erase tools (b81e42).
525 if (class_exists('MxChat_Privacy')) {
526 MxChat_Privacy::init();
527 }
528
529 // Per-session state store: retention cron + the cron-independent
530 // migration drain off admin_init (b64b77).
531 if (class_exists('MxChat_Session_Store')) {
532 MxChat_Session_Store::init();
533 }
534
535 // Daily model-liveness check + its warning notice (b65e8d). Read-only and
536 // fail-open: one listing request per in-use provider per day, none at all
537 // when no key is stored.
538 if (class_exists('MxChat_Model_Liveness')) {
539 MxChat_Model_Liveness::init();
540 }
541
542 // Licence domain check (c93ede): once a day asks mxchat.ai whether this
543 // site's domain is still switched on for its key, honours an explicit
544 // "off" after a three-day grace period, and fails open on everything else.
545 if (class_exists('MxChat_License_Check')) {
546 MxChat_License_Check::init();
547 }
548
549 // Gutenberg chatbot block (plan-95dd1e): a click-to-place wrapper over the
550 // [mxchat_chatbot] shortcode. Registers on init; no-op below WP 5.0.
551 if (class_exists('MxChat_Block')) {
552 MxChat_Block::init();
553 }
554
555 // Elementor chatbot widget (plan-95dd1e part 2): registered ONLY inside
556 // elementor/widgets/register (Elementor >= 3.5) — with Elementor absent or
557 // older, the hook never fires and nothing further loads.
558 if (class_exists('MxChat_Elementor')) {
559 MxChat_Elementor::init();
560 }
561
562 // Editor Assistant — free, OFF-by-default block-editor AI actions (plan-8cb0cb).
563 // init() wires REST + streaming AJAX + sidebar enqueue ONLY when the
564 // mxchat_editor_assistant_enabled option is 'on'; otherwise zero footprint.
565 if (class_exists('MxChat_Editor_Assistant')) {
566 MxChat_Editor_Assistant::init();
567 }
568
569 // OpenAI Vector Store write path (plan-15b5c6): import/sync AJAX, the
570 // import cron tick, the pending-delete sweeper, and the WP-CLI command
571 // all register in the constructor. The sync itself only runs when the
572 // sync toggle + store ID + OpenAI key are all present.
573 if (class_exists('MxChat_Vectorstore_Manager')) {
574 MxChat_Vectorstore_Manager::get_instance();
575 }
576
577 // Admin pages that aren't classes (procedural include).
578 if (is_admin()) {
579 $admin_api_page = plugin_dir_path(__FILE__) . 'includes/admin-api-page.php';
580 if (file_exists($admin_api_page)) {
581 require_once $admin_api_page;
582 }
583 // f7c7d4 renamed this file admin-dashboard-page.php → admin-onboarding-page.php.
584 // The require MUST live here (admin bootstrap) and not just inside
585 // mxchat_add_plugin_page() on the admin_menu hook — admin_menu does NOT
586 // fire on admin-ajax.php requests, so the wizard's AJAX handlers
587 // (plan-905439: mxchat_onboarding_kb_status / save_step / mark_step /
588 // auto_graduate + the f7c7d4 dismiss handler) would never register.
589 $admin_onboarding_page = plugin_dir_path(__FILE__) . 'includes/admin-onboarding-page.php';
590 if (file_exists($admin_onboarding_page)) {
591 require_once $admin_onboarding_page;
592 }
593 }
594 }
595
596 /**
597 * Lazy-load the PDF parser library only when needed.
598 * Avoids loading 44 files on every page request.
599 */
600 function mxchat_load_pdf_parser() {
601 if (class_exists('\Smalot\PdfParser\Parser')) {
602 return true;
603 }
604 $autoload_path = plugin_dir_path(__FILE__) . 'includes/pdf-parser/alt_autoload.php';
605 if (file_exists($autoload_path)) {
606 require_once $autoload_path;
607 return true;
608 }
609 return false;
610 }
611
612 /**
613 * Create URL click tracking table
614 */
615 function mxchat_create_url_clicks_table() {
616 global $wpdb;
617
618 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
619
620 $charset_collate = $wpdb->get_charset_collate();
621
622 $sql = "CREATE TABLE $table_name (
623 id mediumint(9) NOT NULL AUTO_INCREMENT,
624 session_id varchar(100) NOT NULL,
625 clicked_url text NOT NULL,
626 message_context text,
627 click_timestamp datetime DEFAULT CURRENT_TIMESTAMP,
628 user_ip varchar(45),
629 user_agent text,
630 PRIMARY KEY (id),
631 KEY session_id (session_id),
632 KEY click_timestamp (click_timestamp)
633 ) $charset_collate;";
634
635 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
636 dbDelta($sql);
637 }
638
639 /**
640 * Create the per-session state table (b64b77).
641 *
642 * Callable from the activation hook, which can run before plugins_loaded has
643 * included the class files — so it loads the class itself when needed.
644 */
645 function mxchat_create_sessions_table() {
646 if (!class_exists('MxChat_Session_Store')) {
647 $path = plugin_dir_path(__FILE__) . 'includes/class-mxchat-session-store.php';
648 if (!file_exists($path)) {
649 return false;
650 }
651 require_once $path;
652 }
653
654 return MxChat_Session_Store::create_table();
655 }
656
657 /**
658 * FIXED: Robust table creation and column management
659 */
660 function mxchat_create_chat_transcripts_table() {
661 global $wpdb;
662
663 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
664 $charset_collate = $wpdb->get_charset_collate();
665
666 // Create table with ALL columns including user_name from the start
667 $sql = "CREATE TABLE $table_name (
668 id MEDIUMINT(9) NOT NULL AUTO_INCREMENT,
669 user_id MEDIUMINT(9) DEFAULT 0,
670 session_id VARCHAR(255) NOT NULL,
671 role VARCHAR(255) NOT NULL,
672 message TEXT NOT NULL,
673 user_email VARCHAR(255) DEFAULT NULL,
674 user_name VARCHAR(100) DEFAULT NULL,
675 user_identifier VARCHAR(255) DEFAULT NULL,
676 originating_page_url TEXT DEFAULT NULL,
677 originating_page_title VARCHAR(500) DEFAULT NULL,
678 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
679 PRIMARY KEY (id),
680 KEY session_id (session_id),
681 KEY user_email (user_email),
682 KEY timestamp (timestamp)
683 ) $charset_collate;";
684
685 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
686 $result = dbDelta($sql);
687
688 // Log the result for debugging
689 if (empty($result)) {
690 //error_log("MxChat: dbDelta returned empty result for chat transcripts table");
691 } else {
692 //error_log("MxChat: dbDelta result: " . print_r($result, true));
693 }
694
695 // IMPORTANT: Ensure all columns exist for existing installations
696 mxchat_ensure_all_columns($table_name);
697 }
698
699 /**
700 * Ensure all required columns exist (for upgrades)
701 */
702 function mxchat_ensure_all_columns($table_name) {
703 global $wpdb;
704
705 // First check if table exists
706 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
707 if (!$table_exists) {
708 //error_log("MxChat: Table $table_name does not exist, cannot add columns");
709 return;
710 }
711
712 // Define all required columns and their types
713 $required_columns = [
714 'user_identifier' => 'VARCHAR(255) DEFAULT NULL',
715 'user_email' => 'VARCHAR(255) DEFAULT NULL',
716 'user_name' => 'VARCHAR(100) DEFAULT NULL',
717 'originating_page_url' => 'TEXT DEFAULT NULL',
718 'originating_page_title' => 'VARCHAR(500) DEFAULT NULL',
719 'rag_context' => 'LONGTEXT DEFAULT NULL'
720 ];
721
722 // Get existing columns
723 $existing_columns = $wpdb->get_results("SHOW COLUMNS FROM $table_name");
724 if (empty($existing_columns)) {
725 //error_log("MxChat: Could not get columns for table $table_name");
726 return;
727 }
728
729 $existing_column_names = array_column($existing_columns, 'Field');
730
731 // Add missing columns
732 foreach ($required_columns as $column_name => $column_definition) {
733 if (!in_array($column_name, $existing_column_names)) {
734 $alter_sql = "ALTER TABLE $table_name ADD COLUMN $column_name $column_definition";
735 $result = $wpdb->query($alter_sql);
736
737 if ($result === false) {
738 //error_log("MxChat: Failed to add column $column_name to $table_name. Error: " . $wpdb->last_error);
739 } else {
740 //error_log("MxChat: Successfully added column $column_name to $table_name");
741 }
742 }
743 }
744 }
745
746 /**
747 * Add role restriction column to knowledge base table
748 */
749 function mxchat_add_role_restriction_column() {
750 global $wpdb;
751 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
752
753 // Check if table exists first
754 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
755 if (!$table_exists) {
756 //error_log("MxChat: System prompt content table does not exist, cannot add role_restriction column");
757 return;
758 }
759
760 // Check if column already exists
761 $column_exists = $wpdb->get_results(
762 $wpdb->prepare(
763 "SHOW COLUMNS FROM {$table_name} LIKE %s",
764 'role_restriction'
765 )
766 );
767
768 if (empty($column_exists)) {
769 $alter_sql = "ALTER TABLE {$table_name} ADD COLUMN role_restriction VARCHAR(50) DEFAULT 'public' AFTER source_url";
770 $result = $wpdb->query($alter_sql);
771
772 if ($result === false) {
773 //error_log("MxChat: Failed to add role_restriction column. Error: " . $wpdb->last_error);
774 } else {
775 //error_log("MxChat: Successfully added role_restriction column");
776
777 // Set all existing records to 'public' (everyone can access)
778 $update_result = $wpdb->query(
779 "UPDATE {$table_name}
780 SET role_restriction = 'public'
781 WHERE role_restriction IS NULL OR role_restriction = ''"
782 );
783
784 if ($update_result !== false) {
785 //error_log("MxChat: Updated {$update_result} existing records to public access");
786 }
787 }
788 }
789 }
790
791 /**
792 * Add enabled_bots column to intents table for multi-bot action filtering
793 */
794 function mxchat_add_enabled_bots_column() {
795 global $wpdb;
796 $table_name = $wpdb->prefix . 'mxchat_intents';
797
798 // Check if table exists first
799 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
800 if (!$table_exists) {
801 //error_log("MxChat: Intents table does not exist, cannot add enabled_bots column");
802 return;
803 }
804
805 // Check if column already exists
806 $column_exists = $wpdb->get_results(
807 $wpdb->prepare(
808 "SHOW COLUMNS FROM {$table_name} LIKE %s",
809 'enabled_bots'
810 )
811 );
812
813 if (empty($column_exists)) {
814 $alter_sql = "ALTER TABLE {$table_name} ADD COLUMN enabled_bots LONGTEXT DEFAULT NULL AFTER enabled";
815 $result = $wpdb->query($alter_sql);
816
817 if ($result === false) {
818 //error_log("MxChat: Failed to add enabled_bots column. Error: " . $wpdb->last_error);
819 } else {
820 //error_log("MxChat: Successfully added enabled_bots column");
821
822 // Set all existing actions to work with 'default' bot for backward compatibility
823 $default_bots = json_encode(['default']);
824 $update_result = $wpdb->query(
825 $wpdb->prepare(
826 "UPDATE {$table_name}
827 SET enabled_bots = %s
828 WHERE enabled_bots IS NULL OR enabled_bots = ''",
829 $default_bots
830 )
831 );
832
833 if ($update_result !== false) {
834 //error_log("MxChat: Updated {$update_result} existing actions to work with default bot");
835 }
836 }
837 }
838 }
839
840 /**
841 * Create Pinecone role restrictions table with multi-bot support
842 */
843 function mxchat_create_pinecone_roles_table() {
844 global $wpdb;
845
846 $table_name = $wpdb->prefix . 'mxchat_pinecone_roles';
847 $charset_collate = $wpdb->get_charset_collate();
848
849 $sql = "CREATE TABLE $table_name (
850 id mediumint(9) NOT NULL AUTO_INCREMENT,
851 vector_id varchar(255) NOT NULL,
852 bot_id varchar(50) NOT NULL DEFAULT 'default',
853 source_url text,
854 role_restriction varchar(50) DEFAULT 'public',
855 updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
856 PRIMARY KEY (id),
857 UNIQUE KEY vector_bot (vector_id, bot_id),
858 KEY role_restriction (role_restriction),
859 KEY bot_id (bot_id)
860 ) $charset_collate;";
861
862 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
863 dbDelta($sql);
864 }
865
866 /**
867 * Add bot_id column to mxchat_pinecone_roles table for multi-bot support
868 * This migration runs once to update existing installations
869 */
870 function mxchat_migrate_pinecone_roles_add_bot_id() {
871 global $wpdb;
872
873 // Check if migration already ran
874 $migration_version = get_option('mxchat_pinecone_roles_migration_version', '0');
875 if (version_compare($migration_version, '2.5.2', '>=')) {
876 return; // Already migrated
877 }
878
879 $table_name = $wpdb->prefix . 'mxchat_pinecone_roles';
880
881 // Check if table exists
882 if ($wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") != $table_name) {
883 return; // Table doesn't exist yet
884 }
885
886 // Check if bot_id column already exists
887 $column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$table_name} LIKE 'bot_id'");
888
889 if (empty($column_exists)) {
890 // Add bot_id column
891 $wpdb->query("ALTER TABLE {$table_name} ADD COLUMN bot_id VARCHAR(50) NOT NULL DEFAULT 'default' AFTER vector_id");
892
893 // Update the unique key to include bot_id
894 $wpdb->query("ALTER TABLE {$table_name} DROP INDEX vector_id");
895 $wpdb->query("ALTER TABLE {$table_name} ADD UNIQUE KEY vector_bot (vector_id, bot_id)");
896
897 // Add index for bot_id
898 $wpdb->query("ALTER TABLE {$table_name} ADD KEY bot_id (bot_id)");
899
900 //error_log('MxChat: Successfully added bot_id column to mxchat_pinecone_roles table');
901 }
902
903 // Mark migration as complete
904 update_option('mxchat_pinecone_roles_migration_version', '2.5.2');
905 }
906
907 /**
908 * 2.5.6: Add content_type column to mxchat_system_prompt_content table
909 * Enables filtering knowledge base by content type (posts, pages, PDFs, etc.)
910 */
911 function mxchat_migrate_add_content_type_column() {
912 global $wpdb;
913
914 // Check if migration already ran
915 $migration_version = get_option('mxchat_content_type_migration_version', '0');
916 if (version_compare($migration_version, '2.5.6', '>=')) {
917 return;
918 }
919
920 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
921
922 // Check if table exists
923 if ($wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") != $table_name) {
924 return;
925 }
926
927 // Check if content_type column already exists
928 $column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$table_name} LIKE 'content_type'");
929
930 if (empty($column_exists)) {
931 // Add content_type column with default value 'content' for backwards compatibility
932 $wpdb->query("ALTER TABLE {$table_name} ADD COLUMN content_type VARCHAR(50) DEFAULT 'content' AFTER role_restriction");
933
934 // Add index for better query performance
935 $wpdb->query("ALTER TABLE {$table_name} ADD KEY content_type (content_type)");
936
937 //error_log('MxChat: Successfully added content_type column to mxchat_system_prompt_content table');
938 }
939
940 // Mark migration as complete
941 update_option('mxchat_content_type_migration_version', '2.5.6');
942 }
943
944 /**
945 * 3.2.4: Backfill the active embedding model option for installs that already
946 * have KB content but no stamped model. The mismatch warning compares this
947 * against the user's currently selected model — no per-row column needed.
948 */
949 function mxchat_backfill_active_embedding_model() {
950 global $wpdb;
951
952 if (get_option('mxchat_active_embedding_model', '') !== '') {
953 return;
954 }
955
956 $kb_table = $wpdb->prefix . 'mxchat_system_prompt_content';
957 if ($wpdb->get_var("SHOW TABLES LIKE '{$kb_table}'") !== $kb_table) {
958 return;
959 }
960
961 $kb_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$kb_table}");
962 if ($kb_count > 0) {
963 $options = get_option('mxchat_options', array());
964 $current_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
965 update_option('mxchat_active_embedding_model', $current_model, false);
966 }
967 }
968
969 /**
970 * 3.2.20: One-time backfill of the caee10 catalog usage-hint defaults
971 * (plan 64c1ad). persist() writes usage_hint for EVERY shown tool on every
972 * autosave — as '' when the box is untouched — and resolve_tool_setting()
973 * seeds a catalog default_hint ONLY onto an ABSENT key. So any install that
974 * saved the AI Tools screen before 3.2.20 holds '' everywhere and the shipped
975 * defaults can never reach it. On an upgrade from < 3.2.20 an empty stored
976 * hint cannot be a deliberate clear of a rendered default (those builds never
977 * rendered one), so seeding is safe. From 3.2.20 on, empty means the owner
978 * cleared a visible default and is respected — this never runs again (version
979 * gate + its own marker, deliberately not caee10's flag).
980 *
981 * Never touched: entries with owner text, legacy bare-bool entries and
982 * absent-key entries (both already resolve to the default at read time), and
983 * tools whose catalog entry ships no default_hint.
984 */
985 function mxchat_backfill_tool_hint_defaults() {
986 if (get_option('mxchat_tool_hint_backfill_64c1ad') === '1') {
987 return;
988 }
989
990 if (class_exists('MxChat_Tool_Registry')) {
991 $map = get_option('mxchat_function_calling_tools', array());
992 if (is_array($map) && !empty($map)) {
993 $defaults = array();
994 foreach (MxChat_Tool_Registry::core_tool_catalog() as $fn => $meta) {
995 if (!empty($meta['default_hint'])) {
996 $defaults[$fn] = (string) $meta['default_hint'];
997 }
998 }
999
1000 $changed = false;
1001 foreach ($map as $fn => $entry) {
1002 if (!isset($defaults[$fn])) {
1003 continue; // no catalog default — nothing to seed
1004 }
1005 if (!is_array($entry)) {
1006 continue; // legacy bare bool: key absent, resolves to the default already
1007 }
1008 if (!array_key_exists('usage_hint', $entry)) {
1009 continue; // absent key gets the default at read time — must stay absent
1010 }
1011 if (trim((string) $entry['usage_hint']) !== '') {
1012 continue; // owner text — never touch
1013 }
1014 $map[$fn]['usage_hint'] = $defaults[$fn];
1015 $changed = true;
1016 }
1017
1018 if ($changed) {
1019 update_option('mxchat_function_calling_tools', $map);
1020 }
1021 }
1022 }
1023
1024 update_option('mxchat_tool_hint_backfill_64c1ad', '1', false);
1025 }
1026
1027 /**
1028 * 2.5.2: Create queue processing tables for reliable background processing
1029 */
1030 function mxchat_create_queue_tables() {
1031 global $wpdb;
1032 $charset_collate = $wpdb->get_charset_collate();
1033
1034 // Main queue table
1035 $queue_table = $wpdb->prefix . 'mxchat_processing_queue';
1036 $sql_queue = "CREATE TABLE $queue_table (
1037 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
1038 queue_id varchar(64) NOT NULL,
1039 item_type varchar(20) NOT NULL,
1040 item_data longtext NOT NULL,
1041 status varchar(20) NOT NULL DEFAULT 'pending',
1042 bot_id varchar(50) NOT NULL DEFAULT 'default',
1043 priority int(11) NOT NULL DEFAULT 0,
1044 attempts int(11) NOT NULL DEFAULT 0,
1045 max_attempts int(11) NOT NULL DEFAULT 3,
1046 error_message text DEFAULT NULL,
1047 created_at datetime NOT NULL,
1048 started_at datetime DEFAULT NULL,
1049 completed_at datetime DEFAULT NULL,
1050 PRIMARY KEY (id),
1051 KEY queue_id (queue_id),
1052 KEY status (status),
1053 KEY item_type (item_type),
1054 KEY priority (priority)
1055 ) $charset_collate;";
1056
1057 // Queue metadata table
1058 $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
1059 $sql_meta = "CREATE TABLE $meta_table (
1060 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
1061 queue_id varchar(64) NOT NULL,
1062 meta_key varchar(255) NOT NULL,
1063 meta_value longtext,
1064 PRIMARY KEY (id),
1065 KEY queue_id (queue_id),
1066 KEY meta_key (meta_key)
1067 ) $charset_collate;";
1068
1069 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1070 dbDelta($sql_queue);
1071 dbDelta($sql_meta);
1072
1073 //error_log("MxChat: Queue tables created/updated successfully");
1074 }
1075
1076 /**
1077 * Create transcript translations table for persisting translations
1078 */
1079 function mxchat_create_translations_table() {
1080 global $wpdb;
1081 $charset_collate = $wpdb->get_charset_collate();
1082
1083 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
1084 $sql = "CREATE TABLE $table_name (
1085 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
1086 session_id varchar(255) NOT NULL,
1087 language_code varchar(10) NOT NULL,
1088 translations longtext NOT NULL,
1089 created_at datetime NOT NULL,
1090 updated_at datetime NOT NULL,
1091 PRIMARY KEY (id),
1092 UNIQUE KEY session_lang (session_id, language_code),
1093 KEY session_id (session_id)
1094 ) $charset_collate;";
1095
1096 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1097 dbDelta($sql);
1098 }
1099
1100 /**
1101 * Create per-session satisfaction ratings table (v3.2.6)
1102 * Stores one 👍/👎 rating + optional feedback per chat session.
1103 */
1104 function mxchat_create_session_ratings_table() {
1105 global $wpdb;
1106 $charset_collate = $wpdb->get_charset_collate();
1107
1108 $table_name = $wpdb->prefix . 'mxchat_session_ratings';
1109 $sql = "CREATE TABLE $table_name (
1110 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
1111 session_id varchar(255) NOT NULL,
1112 bot_id varchar(50) NOT NULL DEFAULT 'default',
1113 rating_value tinyint(1) NOT NULL,
1114 rating_feedback text DEFAULT NULL,
1115 created_at datetime NOT NULL,
1116 PRIMARY KEY (id),
1117 UNIQUE KEY session_id (session_id),
1118 KEY bot_id (bot_id),
1119 KEY created_at (created_at)
1120 ) $charset_collate;";
1121
1122 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1123 dbDelta($sql);
1124 }
1125
1126 /**
1127 * Create the OpenAI Vector Store file-mapping table (v3.2.20, plan 15b5c6).
1128 * One row per mirrored KB entry: which store, which bot, which OpenAI file id,
1129 * and a hash of the uploaded body for change detection. Vector store files are
1130 * not patchable in place — without this mapping an update cannot find its
1131 * predecessor, and the store silently accumulates stale duplicates.
1132 * status: 'live' (serving) or 'pending_delete' (condemned, swept later).
1133 */
1134 function mxchat_create_vectorstore_files_table() {
1135 global $wpdb;
1136 $charset_collate = $wpdb->get_charset_collate();
1137
1138 $table_name = $wpdb->prefix . 'mxchat_vectorstore_files';
1139 $sql = "CREATE TABLE $table_name (
1140 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
1141 store_id varchar(64) NOT NULL,
1142 bot_id varchar(64) NOT NULL DEFAULT 'default',
1143 entry_key char(32) NOT NULL,
1144 source_url text,
1145 file_id varchar(64) NOT NULL,
1146 content_hash char(32) NOT NULL DEFAULT '',
1147 status varchar(20) NOT NULL DEFAULT 'live',
1148 last_error text,
1149 updated_at datetime DEFAULT NULL,
1150 PRIMARY KEY (id),
1151 KEY store_entry (store_id, bot_id, entry_key, status),
1152 KEY status (status)
1153 ) $charset_collate;";
1154
1155 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
1156 dbDelta($sql);
1157 }
1158
1159 /**
1160 * 2.5.2: Fix URL column size to support long URLs (especially with UTF-8 encoding)
1161 * This fixes "url, source_url. The supplied values may be too long" errors
1162 */
1163 function mxchat_fix_url_column_size() {
1164 global $wpdb;
1165 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
1166
1167 // Check if table exists
1168 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
1169 if (!$table_exists) {
1170 return;
1171 }
1172
1173 // Change url and source_url from VARCHAR to TEXT to handle long URLs
1174 // This is especially important for URLs with UTF-8 encoded characters (Hebrew, Arabic, etc.)
1175 $wpdb->query("ALTER TABLE {$table_name} MODIFY COLUMN url TEXT");
1176 $wpdb->query("ALTER TABLE {$table_name} MODIFY COLUMN source_url TEXT");
1177
1178 //error_log("MxChat: Successfully updated url and source_url columns to TEXT type for long URL support");
1179 }
1180
1181 /**
1182 * Migrate deprecated AI models to their replacements
1183 * Version 2.5.1: Migrate Claude 3.5 Sonnet (deprecated) to Claude 3.7 Sonnet
1184 * Version 3.1.2: Convert chat transcripts table to utf8mb4 for emoji support
1185 * Without utf8mb4, any bot response containing emojis silently fails to insert.
1186 */
1187 function mxchat_migrate_transcripts_charset() {
1188 global $wpdb;
1189 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1190 $wpdb->query("ALTER TABLE $table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
1191 }
1192
1193 /**
1194 * Version 3.0.55: Migrate GPT-4 series models (deprecated 2026-02-17) to GPT-5 series
1195 */
1196 function mxchat_migrate_deprecated_models() {
1197 $options = get_option('mxchat_options', array());
1198 $migrated = false;
1199 $migration_message = '';
1200
1201 if (!isset($options['model'])) {
1202 return;
1203 }
1204
1205 // The retired-id → replacement mapping lives in ONE place:
1206 // MxChat_Model_Catalog::retired_model_map() (plan 202df5). This function
1207 // and every add-on that stores model ids consume that map — do not add a
1208 // deprecation list here again.
1209 if (!class_exists('MxChat_Model_Catalog') || !method_exists('MxChat_Model_Catalog', 'retired_model_map')) {
1210 return; // catalog not loaded (defensive) — the next admin load retries
1211 }
1212 $map = MxChat_Model_Catalog::retired_model_map();
1213
1214 $current_model = $options['model'];
1215 if (isset($map[$current_model])) {
1216 $entry = $map[$current_model];
1217 $options['model'] = $entry['to'];
1218 $migrated = true;
1219 $migration_message = sprintf(
1220 'Your chatbot model has been automatically updated from %s to %s %s.',
1221 $current_model,
1222 $entry['label'],
1223 $entry['reason']
1224 );
1225 }
1226
1227 // The content generator has its own model option — the same map applies.
1228 // (Pre-202df5 this branch covered only the two gpt-5.x-chat-latest aliases;
1229 // it now covers every retired id, so e.g. a content model stranded on a
1230 // retired Claude id is rescued the same way the chat model is.)
1231 if (isset($options['content_model']) && isset($map[$options['content_model']])) {
1232 $old_content_model = $options['content_model'];
1233 $entry = $map[$old_content_model];
1234 $options['content_model'] = $entry['to'];
1235 $migrated = true;
1236 $migration_message = trim($migration_message . ' ' . sprintf(
1237 'Your content generation model has also been automatically updated from %s to %s %s.',
1238 $old_content_model,
1239 $entry['label'],
1240 $entry['reason']
1241 ));
1242 }
1243
1244 if ($migrated) {
1245 update_option('mxchat_options', $options);
1246 update_option('mxchat_model_migrated_notice', true);
1247 update_option('mxchat_model_migration_message', $migration_message);
1248 }
1249 }
1250
1251 /**
1252 * Show admin notice after model migration
1253 */
1254 function mxchat_show_migration_notice() {
1255 if (get_option('mxchat_model_migrated_notice')) {
1256 $migration_message = get_option('mxchat_model_migration_message', __('Your chatbot model has been automatically updated due to a model deprecation.', 'mxchat'));
1257 ?>
1258 <div class="notice notice-info is-dismissible">
1259 <p>
1260 <strong><?php esc_html_e('MxChat Model Updated', 'mxchat'); ?></strong><br>
1261 <?php echo esc_html($migration_message); ?>
1262 </p>
1263 </div>
1264 <?php
1265 delete_option('mxchat_model_migrated_notice');
1266 delete_option('mxchat_model_migration_message');
1267 }
1268 }
1269
1270 /**
1271 * One-time recommendation on EXISTING installs (stamp 'legacy') that the new
1272 * "Strip unapproved links" guard exists and is worth turning on (plan 58f8b4).
1273 * Fresh 3.2.20+ installs default it on and never see this. Shown only on
1274 * MxChat admin pages, gone for good once dismissed or once the site saves an
1275 * explicit value for the toggle either way.
1276 */
1277 function mxchat_show_strip_links_notice() {
1278 if (!current_user_can('manage_options')) {
1279 return;
1280 }
1281 $page = isset($_GET['page']) ? sanitize_key($_GET['page']) : '';
1282 if (strpos($page, 'mxchat') !== 0) {
1283 return;
1284 }
1285 if (get_option('mxchat_strip_links_notice_dismissed', '') === '1') {
1286 return;
1287 }
1288 if (get_option('mxchat_installed_at_version', '') !== 'legacy') {
1289 return;
1290 }
1291 $opts = get_option('mxchat_options', array());
1292 if (is_array($opts) && isset($opts['strip_unapproved_links_toggle'])) {
1293 return; // The site already made its choice — stop recommending.
1294 }
1295 $dismiss_url = wp_nonce_url(
1296 admin_url('admin-post.php?action=mxchat_dismiss_strip_links_notice'),
1297 'mxchat_dismiss_strip_links_notice'
1298 );
1299 ?>
1300 <div class="notice notice-info">
1301 <p>
1302 <strong><?php esc_html_e('MxChat: new link protection available', 'mxchat'); ?></strong><br>
1303 <?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'); ?>
1304 <a href="<?php echo esc_url($dismiss_url); ?>"><?php esc_html_e('Dismiss', 'mxchat'); ?></a>
1305 </p>
1306 </div>
1307 <?php
1308 }
1309 add_action('admin_notices', 'mxchat_show_strip_links_notice');
1310
1311 /** Dismiss handler for the strip-links recommendation notice (plan 58f8b4). */
1312 function mxchat_dismiss_strip_links_notice() {
1313 if (!current_user_can('manage_options')) {
1314 wp_die(esc_html__('Insufficient permissions.', 'mxchat'), '', array('response' => 403));
1315 }
1316 check_admin_referer('mxchat_dismiss_strip_links_notice');
1317 update_option('mxchat_strip_links_notice_dismissed', '1', false);
1318 $referer = wp_get_referer();
1319 wp_safe_redirect($referer ? $referer : admin_url('admin.php?page=mxchat-max'));
1320 exit;
1321 }
1322 add_action('admin_post_mxchat_dismiss_strip_links_notice', 'mxchat_dismiss_strip_links_notice');
1323
1324 /**
1325 * Persistent admin notice when the provider rejected the configured model
1326 * (model_not_found / no access). Set by mxchat_friendly_chat_error() in the
1327 * integrator whenever a chat request fails on a model-access error — including
1328 * requests from anonymous visitors, which is the case that otherwise stays
1329 * invisible to the site owner for weeks (plan e46b8f).
1330 *
1331 * Deleted after render so it re-arms on the next failed chat: the notice keeps
1332 * reappearing until the model is fixed, then stops on its own.
1333 */
1334 function mxchat_show_model_access_notice() {
1335 if (!current_user_can('manage_options')) {
1336 return;
1337 }
1338 $notice = get_option('mxchat_model_access_notice');
1339 if (!is_array($notice) || empty($notice['model'])) {
1340 return;
1341 }
1342 $settings_url = admin_url('admin.php?page=mxchat-max');
1343 ?>
1344 <div class="notice notice-error is-dismissible">
1345 <p>
1346 <strong><?php esc_html_e('MxChat: your AI model is being rejected by the provider', 'mxchat'); ?></strong><br>
1347 <?php
1348 printf(
1349 /* translators: 1: model id, 2: provider name */
1350 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'),
1351 esc_html($notice['model']),
1352 esc_html(!empty($notice['provider']) ? $notice['provider'] : __('the AI provider', 'mxchat'))
1353 );
1354 ?>
1355 <a href="<?php echo esc_url($settings_url); ?>"><?php esc_html_e('Choose a different model in MxChat Settings', 'mxchat'); ?></a>
1356 </p>
1357 </div>
1358 <?php
1359 delete_option('mxchat_model_access_notice');
1360 }
1361
1362 function mxchat_activate() {
1363 global $wpdb;
1364 $charset_collate = $wpdb->get_charset_collate();
1365
1366 //error_log("MxChat: Running activation function");
1367
1368 // Create chat transcripts table with improved function
1369 mxchat_create_chat_transcripts_table();
1370
1371 // Per-session state table (b64b77). Activation can run before
1372 // plugins_loaded has included the class files, so require it directly.
1373 mxchat_create_sessions_table();
1374
1375 // System Prompt Content Table - UPDATED: Use TEXT for url and source_url columns
1376 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
1377 $sql_system_prompt = "CREATE TABLE $system_prompt_table (
1378 id MEDIUMINT(9) NOT NULL AUTO_INCREMENT,
1379 url TEXT NOT NULL,
1380 article_content LONGTEXT NOT NULL,
1381 embedding_vector LONGTEXT,
1382 source_url TEXT DEFAULT NULL,
1383 title VARCHAR(255) DEFAULT NULL,
1384 role_restriction VARCHAR(50) DEFAULT 'public',
1385 content_type VARCHAR(50) DEFAULT 'content',
1386 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1387 PRIMARY KEY (id),
1388 KEY content_type (content_type)
1389 ) $charset_collate;";
1390
1391 // Intents Table - NOW INCLUDES enabled_bots column from the start
1392 $intents_table = $wpdb->prefix . 'mxchat_intents';
1393 $sql_intents_table = "CREATE TABLE $intents_table (
1394 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
1395 intent_label VARCHAR(255) NOT NULL,
1396 phrases TEXT NOT NULL,
1397 embedding_vector LONGTEXT NOT NULL,
1398 callback_function VARCHAR(255) NOT NULL,
1399 similarity_threshold FLOAT DEFAULT 0.85,
1400 enabled TINYINT(1) NOT NULL DEFAULT 1,
1401 enabled_bots LONGTEXT DEFAULT NULL,
1402 PRIMARY KEY (id)
1403 ) $charset_collate;";
1404
1405 // Individual Intent Phrases Table - each phrase gets its own embedding vector
1406 $intent_phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
1407 $sql_intent_phrases_table = "CREATE TABLE $intent_phrases_table (
1408 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
1409 intent_id BIGINT(20) UNSIGNED NOT NULL,
1410 phrase TEXT NOT NULL,
1411 embedding_vector LONGTEXT NOT NULL,
1412 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1413 PRIMARY KEY (id),
1414 KEY intent_id (intent_id)
1415 ) $charset_collate;";
1416
1417 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
1418
1419 // Create other tables
1420 dbDelta($sql_system_prompt);
1421 dbDelta($sql_intents_table);
1422 dbDelta($sql_intent_phrases_table);
1423
1424 // Create URL click tracking table
1425 mxchat_create_url_clicks_table();
1426
1427 // Create Pinecone roles table
1428 mxchat_create_pinecone_roles_table();
1429
1430 // NEW 2.5.2: Create queue processing tables
1431 mxchat_create_queue_tables();
1432
1433 // Create transcript translations table
1434 mxchat_create_translations_table();
1435
1436 // Create per-session satisfaction ratings table (v3.2.6)
1437 mxchat_create_session_ratings_table();
1438
1439 // Create Vector Store file-mapping table (v3.2.20, plan 15b5c6)
1440 mxchat_create_vectorstore_files_table();
1441
1442 // Ensure additional columns in system prompt table
1443 $existing_system_columns = $wpdb->get_results("SHOW COLUMNS FROM $system_prompt_table");
1444 if (!empty($existing_system_columns)) {
1445 $existing_system_column_names = array_column($existing_system_columns, 'Field');
1446
1447 if (!in_array('embedding_vector', $existing_system_column_names)) {
1448 $wpdb->query("ALTER TABLE $system_prompt_table ADD COLUMN embedding_vector LONGTEXT");
1449 }
1450 if (!in_array('source_url', $existing_system_column_names)) {
1451 $wpdb->query("ALTER TABLE $system_prompt_table ADD COLUMN source_url TEXT DEFAULT NULL");
1452 }
1453 if (!in_array('role_restriction', $existing_system_column_names)) {
1454 $wpdb->query("ALTER TABLE $system_prompt_table ADD COLUMN role_restriction VARCHAR(50) DEFAULT 'public' AFTER source_url");
1455 }
1456 // Page title captured at import time (3.2.23, plan d9ee66) - read by
1457 // add-ons that name a source, e.g. AI Search's source cards.
1458 if (!in_array('title', $existing_system_column_names)) {
1459 $wpdb->query("ALTER TABLE $system_prompt_table ADD COLUMN title VARCHAR(255) DEFAULT NULL AFTER source_url");
1460 }
1461 }
1462
1463 // Set default thresholds for existing intents
1464 $wpdb->query("UPDATE {$intents_table} SET similarity_threshold = 0.85 WHERE similarity_threshold IS NULL");
1465
1466 // Ensure enabled column exists in intents table
1467 $existing_intent_columns = $wpdb->get_results("SHOW COLUMNS FROM $intents_table");
1468 if (!empty($existing_intent_columns)) {
1469 $existing_intent_column_names = array_column($existing_intent_columns, 'Field');
1470
1471 if (!in_array('enabled', $existing_intent_column_names)) {
1472 $wpdb->query("ALTER TABLE $intents_table ADD COLUMN enabled TINYINT(1) NOT NULL DEFAULT 1");
1473 }
1474
1475 // Ensure enabled_bots column exists for existing installations
1476 if (!in_array('enabled_bots', $existing_intent_column_names)) {
1477 $wpdb->query("ALTER TABLE $intents_table ADD COLUMN enabled_bots LONGTEXT DEFAULT NULL AFTER enabled");
1478
1479 // Set existing actions to work with default bot
1480 $default_bots = json_encode(['default']);
1481 $wpdb->query($wpdb->prepare(
1482 "UPDATE {$intents_table} SET enabled_bots = %s WHERE enabled_bots IS NULL",
1483 $default_bots
1484 ));
1485 }
1486 }
1487
1488 // Run migration for existing installations
1489 mxchat_migrate_pinecone_roles_add_bot_id();
1490
1491 // 3.2.4: Backfill active embedding model option (replaces 3.2.3 column-based tracking)
1492 mxchat_backfill_active_embedding_model();
1493
1494 // Setup cron jobs
1495 mxchat_setup_cron_jobs();
1496
1497 // Update version (stable base version — never the dev time()-suffixed one,
1498 // or the check_for_update comparison would churn every request)
1499 update_option('mxchat_plugin_version', MXCHAT_BASE_VERSION);
1500
1501 //error_log("MxChat: Activation function completed");
1502 }
1503
1504 /**
1505 * Setup cron jobs on plugin activation
1506 */
1507 function mxchat_setup_cron_jobs() {
1508 // Clear any existing cron jobs first
1509 wp_clear_scheduled_hook('mxchat_reset_rate_limits');
1510
1511 // Check if WordPress cron is disabled
1512 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
1513 // Set flag to use fallback system
1514 update_option('mxchat_use_fallback_rate_limits', true);
1515 update_option('mxchat_next_rate_limit_check', time() + 3600);
1516 // Deliberately NO early return (plan-bc08a6): transcript cleanup below
1517 // must still be scheduled. DISABLE_WP_CRON only changes HOW cron events
1518 // execute (a server-side runner hitting wp-cron.php instead of loopback
1519 // spawns) — scheduling still just writes the cron option. The old early
1520 // return here meant a deactivate/reactivate cycle on a DISABLE_WP_CRON
1521 // site permanently lost the transcript cleanup event while the retention
1522 // setting still claimed to be active.
1523 } else {
1524 // Schedule the rate limit reset cron job
1525 $result = wp_schedule_event(time() + 300, 'hourly', 'mxchat_reset_rate_limits');
1526
1527 if ($result === false) {
1528 // Fallback if scheduling fails
1529 update_option('mxchat_use_fallback_rate_limits', true);
1530 update_option('mxchat_next_rate_limit_check', time() + 3600);
1531 } else {
1532 // Clear fallback flags if cron scheduling succeeded
1533 delete_option('mxchat_use_fallback_rate_limits');
1534 }
1535 }
1536
1537 // Schedule transcript cleanup if configured (bucket dropdown OR custom retention-days > 0)
1538 $transcript_options = get_option('mxchat_transcripts_options', array());
1539 $cleanup_interval = isset($transcript_options['mxchat_auto_delete_transcripts']) ? $transcript_options['mxchat_auto_delete_transcripts'] : 'never';
1540 $custom_retention = isset($transcript_options['mxchat_retention_days']) ? (int) $transcript_options['mxchat_retention_days'] : 0;
1541
1542 if ($cleanup_interval !== 'never' || $custom_retention > 0) {
1543 // Check if not already scheduled
1544 if (!wp_next_scheduled('mxchat_cleanup_old_transcripts')) {
1545 // Schedule to run daily at 3 AM
1546 $next_run = strtotime('tomorrow 3:00 AM');
1547 wp_schedule_event($next_run, 'daily', 'mxchat_cleanup_old_transcripts');
1548 }
1549 }
1550 }
1551
1552 /**
1553 * Clean up on plugin deactivation
1554 */
1555 function mxchat_deactivate() {
1556 // Clear scheduled cron jobs
1557 wp_clear_scheduled_hook('mxchat_reset_rate_limits');
1558 wp_clear_scheduled_hook('mxchat_cleanup_old_transcripts');
1559 wp_clear_scheduled_hook('mxchat_send_delayed_transcript');
1560 wp_clear_scheduled_hook('mxchat_model_liveness_check');
1561 wp_clear_scheduled_hook('mxchat_license_check');
1562
1563 // Clear fallback options
1564 delete_option('mxchat_use_fallback_rate_limits');
1565 delete_option('mxchat_next_rate_limit_check');
1566 delete_option('mxchat_fallback_check_interval');
1567
1568 // NOTE: We do NOT delete queue tables on deactivation
1569 // This preserves data if user accidentally deactivates the plugin
1570 }
1571
1572 /**
1573 * Check if fallback rate limit cleanup is needed
1574 */
1575 function mxchat_check_fallback_rate_limits() {
1576 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
1577
1578 if (!$use_fallback) {
1579 return;
1580 }
1581
1582 $next_check = get_option('mxchat_next_rate_limit_check', 0);
1583
1584 if (time() >= $next_check) {
1585 // Reuse the bootstrap's integrator — mxchat_init() creates the global on
1586 // plugins_loaded (before this init-priority-5 callback), so it's always set
1587 // here. Constructing a second MxChat_Integrator just to call one method
1588 // re-registers every hook the plugin has (ajax pairs, wp_footer loader,
1589 // rest_api_init, admin_init guard) on a duplicate instance for the rest of
1590 // the request. Defensive construction only if the global is somehow unset.
1591 // NOTE: MxChat_Integrator::check_fallback_rate_limits() is a second
1592 // implementation of this same check — if either changes, change both.
1593 global $mxchat_integrator;
1594 $integrator = ($mxchat_integrator instanceof MxChat_Integrator)
1595 ? $mxchat_integrator
1596 : (class_exists('MxChat_Integrator') ? new MxChat_Integrator() : null);
1597 if ($integrator && method_exists($integrator, 'mxchat_reset_rate_limits')) {
1598 $integrator->mxchat_reset_rate_limits();
1599 update_option('mxchat_next_rate_limit_check', time() + 3600);
1600 }
1601 }
1602 }
1603
1604 /**
1605 * Robust update checking with role restriction migration, model deprecation, and queue tables
1606 * CRITICAL: This runs on EVERY page load to ensure tables exist
1607 */
1608 function mxchat_check_for_update() {
1609 global $wpdb;
1610
1611 try {
1612 $current_version = get_option('mxchat_plugin_version', '0.0.0');
1613 $plugin_version = MXCHAT_BASE_VERSION;
1614
1615 // Always ensure critical tables exist (even if version matches)
1616 // This handles manual table deletion or fresh installs
1617 $chat_table = $wpdb->prefix . 'mxchat_chat_transcripts';
1618 $queue_table = $wpdb->prefix . 'mxchat_processing_queue';
1619
1620 $sessions_table = $wpdb->prefix . 'mxchat_sessions';
1621
1622 $chat_exists = $wpdb->get_var("SHOW TABLES LIKE '$chat_table'") === $chat_table;
1623 $queue_exists = $wpdb->get_var("SHOW TABLES LIKE '$queue_table'") === $queue_table;
1624 $sessions_exists = get_option('mxchat_session_store_ready') === '1'
1625 || $wpdb->get_var("SHOW TABLES LIKE '$sessions_table'") === $sessions_table;
1626
1627 if (!$chat_exists || !$queue_exists || !$sessions_exists) {
1628 //error_log("MxChat: Critical tables missing, running activation");
1629 mxchat_activate();
1630 }
1631
1632 // Version-specific migrations
1633 if ($current_version !== $plugin_version) {
1634 //error_log("MxChat: Version change detected: $current_version -> $plugin_version");
1635
1636 // Run live agent update BEFORE updating the stored version
1637 mxchat_handle_live_agent_update();
1638
1639 // Run theme migration notice for 3.0.1 (AI theme CSS structure changes)
1640 mxchat_handle_theme_migration_notice();
1641
1642 // Run role restriction migration for 2.4.1
1643 if (version_compare($current_version, '2.4.1', '<')) {
1644 mxchat_add_role_restriction_column();
1645 }
1646
1647 // Run enabled_bots column migration for 2.4.4
1648 if (version_compare($current_version, '2.4.4', '<')) {
1649 mxchat_add_enabled_bots_column();
1650 }
1651
1652 // Run model migration for 2.5.1 (Claude deprecation)
1653 if (version_compare($current_version, '2.5.1', '<')) {
1654 mxchat_migrate_deprecated_models();
1655 }
1656
1657 // 2.5.2: Ensure queue tables exist and fix URL column sizes for all users upgrading to 2.5.2
1658 if (version_compare($current_version, '2.5.2', '<')) {
1659 mxchat_create_queue_tables();
1660 mxchat_fix_url_column_size(); // NEW: Fix URL column size for long URLs
1661 //error_log("MxChat: Queue tables created and URL columns updated for upgrade to 2.5.2");
1662 }
1663
1664 // 2.6.0: Ensure rag_context column exists for retrieved documents feature
1665 if (version_compare($current_version, '2.6.0', '<')) {
1666 $chat_table = $wpdb->prefix . 'mxchat_chat_transcripts';
1667 mxchat_ensure_all_columns($chat_table);
1668 //error_log("MxChat: rag_context column migration for 2.6.0");
1669 }
1670
1671 // 3.0.5: Migrate deprecated Gemini embedding model
1672 if (version_compare($current_version, '3.0.5', '<')) {
1673 mxchat_migrate_gemini_embedding_model();
1674 }
1675
1676 // 3.0.6: Migrate deprecated OpenAI and Claude models
1677 if (version_compare($current_version, '3.0.6', '<')) {
1678 mxchat_migrate_deprecated_models();
1679 }
1680
1681 // 3.1.2: Convert chat transcripts table to utf8mb4 for emoji support
1682 if (version_compare($current_version, '3.1.2', '<')) {
1683 mxchat_migrate_transcripts_charset();
1684 }
1685
1686 // 3.1.7: Clean up stale shared session email/name entries
1687 if (version_compare($current_version, '3.1.7', '<')) {
1688 delete_option('mxchat_email_null');
1689 delete_option('mxchat_name_null');
1690 }
1691
1692 // 3.2.4: Backfill active embedding model option for the warning UI
1693 // (replaces the per-row column tracking from 3.2.3, which was reverted)
1694 if (version_compare($current_version, '3.2.4', '<')) {
1695 mxchat_backfill_active_embedding_model();
1696 }
1697
1698 // 3.2.15: Migrate retired DeepSeek ids (deepseek-chat / deepseek-reasoner
1699 // were shut off at the vendor on 2026-07-24). The function is idempotent —
1700 // it only rewrites models on its deprecation lists.
1701 if (version_compare($current_version, '3.2.15', '<')) {
1702 mxchat_migrate_deprecated_models();
1703 }
1704
1705 // 3.2.16: Migrate OpenAI ids retiring 2026-08-10 (gpt-5.1-chat-latest /
1706 // gpt-5.3-chat-latest → gpt-5.6-sol per OpenAI's deprecations page).
1707 // Idempotent — only rewrites models on the deprecation lists (e46b8f).
1708 if (version_compare($current_version, '3.2.16', '<')) {
1709 mxchat_migrate_deprecated_models();
1710 }
1711
1712 // 3.2.17: Credential options must not autoload (af2400) — the two
1713 // Pinecone-secret-holding rows were in alloptions, i.e. read into
1714 // memory on every request including anonymous page views. Idempotent.
1715 // Also carry the import modal's remembered ACF→PDF checkbox state
1716 // into the new install-level option (11720c).
1717 if (version_compare($current_version, '3.2.17', '<')) {
1718 mxchat_fix_credential_option_autoload();
1719 mxchat_migrate_acf_pdf_extraction_option();
1720 }
1721
1722 // 3.2.19: Claude Opus 4.1 retired Aug 5, 2026 — auto-move stranded
1723 // sites (and the older tiers on the migration's lists) per the
1724 // changelog's promise. Idempotent — only rewrites models on the
1725 // deprecation lists. Without a gate at this release's version,
1726 // nothing calls the migration for 3.2.18 upgraders (plan a5a598;
1727 // the gate value must equal the version this block ships in).
1728 if (version_compare($current_version, '3.2.19', '<')) {
1729 mxchat_migrate_deprecated_models();
1730 }
1731
1732 // 3.2.20: Seed the caee10 usage-hint defaults onto tool entries a
1733 // pre-3.2.20 autosave stamped with '' (plan 64c1ad). The gate value
1734 // equals the version this block ships in (a5a598 rule); the
1735 // function carries its own one-time marker on top.
1736 // Also re-run the deprecated-models migration: 202df5 widened it to
1737 // rescue a content_model stranded on ANY retired id (previously
1738 // only the two gpt-5.x-chat-latest aliases). Idempotent — only
1739 // rewrites models on the catalog's retired_model_map().
1740 if (version_compare($current_version, '3.2.20', '<')) {
1741 mxchat_backfill_tool_hint_defaults();
1742 mxchat_migrate_deprecated_models();
1743 }
1744
1745 // Run full activation to ensure everything is up to date
1746 mxchat_activate();
1747
1748 // Run migration functions
1749 mxchat_migrate_live_agent_status();
1750
1751 // (The 2.1.8 orphaned-history reconciliation sweep is gone —
1752 // 3.2.19's mxchat_history_backlog_* drain supersedes it, and it
1753 // self-arms without a version gate: plan 839c4c.)
1754
1755 // Update version LAST
1756 update_option('mxchat_plugin_version', $plugin_version);
1757
1758 //error_log("MxChat: Updated from version $current_version to $plugin_version");
1759 }
1760
1761 } catch (Exception $e) {
1762 //error_log('MxChat update error: ' . $e->getMessage());
1763 // Don't update version if there was an error
1764 }
1765 }
1766
1767 /**
1768 * Credential options must never enter the autoloaded alloptions set.
1769 * mxchat_prompts_options and mxchat_pinecone_addon_options can hold the
1770 * Pinecone API secret; mxchat_options already stores its keys with autoload
1771 * off and these two must match it. The filter covers every future
1772 * add_option()/update_option() that creates the row — Settings API saves
1773 * through options.php and WP-CLI included — on WP 6.6+; older cores are
1774 * covered by the explicit autoload arguments at the plugin's own write
1775 * sites plus the one-time migration below.
1776 */
1777 add_filter('wp_default_autoload_value', 'mxchat_credential_option_autoload_value', 10, 2);
1778 function mxchat_credential_option_autoload_value($autoload, $option) {
1779 if (in_array($option, array('mxchat_prompts_options', 'mxchat_pinecone_addon_options'), true)) {
1780 return false;
1781 }
1782 return $autoload;
1783 }
1784
1785 /**
1786 * One-time upgrade migration: flip the autoload flag on credential option
1787 * rows that existing installs are already carrying autoloaded. Includes
1788 * mxchat_adv_api_token (Advanced Content bearer token) — harmless no-op
1789 * when that add-on is not installed, since missing rows simply don't match.
1790 */
1791 function mxchat_fix_credential_option_autoload() {
1792 $keys = array('mxchat_prompts_options', 'mxchat_pinecone_addon_options', 'mxchat_adv_api_token');
1793 if (function_exists('wp_set_option_autoload_values')) {
1794 wp_set_option_autoload_values(array_fill_keys($keys, false));
1795 return;
1796 }
1797 // Pre-WP-6.4 fallback: direct flip + cache invalidation.
1798 global $wpdb;
1799 $placeholders = implode(',', array_fill(0, count($keys), '%s'));
1800 $wpdb->query($wpdb->prepare("UPDATE {$wpdb->options} SET autoload = 'no' WHERE option_name IN ($placeholders)", $keys));
1801 wp_cache_delete('alloptions', 'options');
1802 foreach ($keys as $key) {
1803 wp_cache_delete($key, 'options');
1804 }
1805 }
1806
1807 /**
1808 * One-time carry of the import modal's remembered ACF→PDF checkbox state
1809 * (mxchat_options['acf_pdf_extract_default'], written per-import until 3.2.16)
1810 * into the new install-level option mxchat_acf_pdf_extraction (plan 11720c).
1811 * Fresh installs and installs that never touched the checkbox default OFF,
1812 * matching the setting's own "recommended only if…" guidance.
1813 */
1814 function mxchat_migrate_acf_pdf_extraction_option() {
1815 if (get_option('mxchat_acf_pdf_extraction', null) !== null) {
1816 return; // already set — never overwrite an owner's choice
1817 }
1818 $mxchat_options = get_option('mxchat_options', array());
1819 if (is_array($mxchat_options) && array_key_exists('acf_pdf_extract_default', $mxchat_options)) {
1820 update_option('mxchat_acf_pdf_extraction', !empty($mxchat_options['acf_pdf_extract_default']) ? '1' : '0', false);
1821 unset($mxchat_options['acf_pdf_extract_default']);
1822 update_option('mxchat_options', $mxchat_options);
1823 }
1824 }
1825
1826 /**
1827 * One-time migration of mxchat_acf_excluded_fields from field NAMES to field
1828 * KEYS (plan 30e81f). Names are not unique across ACF groups, so two fields
1829 * named the same in different groups shared one toggle, one saved state, and
1830 * one exclusion — and the save-on-exit beacon could revert a save through the
1831 * twin. Keys are unique; everything now runs on them.
1832 *
1833 * MIGRATION DIRECTION IS DELIBERATE: one stored name can match several keys —
1834 * exclude EVERY one of them. The UI describes these as "sensitive or
1835 * irrelevant fields"; an under-migration would silently start feeding a
1836 * previously-excluded sensitive field into embeddings sent to a third-party
1837 * provider. Over-excluding is visible in the UI and costs some retrieval
1838 * quality; under-excluding is a silent privacy regression.
1839 *
1840 * Self-arming on acf/init (NOT the version-gated upgrade block): resolving
1841 * names needs ACF fully booted with local JSON/PHP groups registered, and if
1842 * ACF is deactivated at upgrade time the migration simply waits for the next
1843 * load with ACF active. Until it runs, stored names keep working — every
1844 * exclusion read site honors legacy name entries alongside keys. A name that
1845 * matches no current key is KEPT (its group may be temporarily inactive), and
1846 * the per-field include path lazily converts it if it ever resolves again.
1847 */
1848 function mxchat_migrate_acf_exclusions_to_keys() {
1849 if (get_option('mxchat_acf_exclusions_migrated', '') === '1') {
1850 return;
1851 }
1852 $stored = get_option('mxchat_acf_excluded_fields', array());
1853 if (!is_array($stored) || empty($stored)) {
1854 update_option('mxchat_acf_exclusions_migrated', '1', false);
1855 return;
1856 }
1857 if (!function_exists('acf_get_field_groups') || !function_exists('acf_get_fields')) {
1858 return; // acf/init fired without the API? Bail; retry next load.
1859 }
1860
1861 // Map every current top-level field: name => [keys], and the set of keys.
1862 $name_to_keys = array();
1863 $current_keys = array();
1864 foreach (acf_get_field_groups() as $group) {
1865 $group_fields = acf_get_fields($group['key']);
1866 if (empty($group_fields)) {
1867 continue;
1868 }
1869 foreach ($group_fields as $field) {
1870 if (empty($field['key']) || !isset($field['name'])) {
1871 continue;
1872 }
1873 $current_keys[$field['key']] = true;
1874 $name_to_keys[$field['name']][] = $field['key'];
1875 }
1876 }
1877
1878 $migrated = array();
1879 $converted = 0;
1880 foreach ($stored as $entry) {
1881 if (!is_string($entry) || $entry === '') {
1882 continue;
1883 }
1884 if (isset($current_keys[$entry])) {
1885 $migrated[] = $entry; // already a live key
1886 } elseif (isset($name_to_keys[$entry])) {
1887 foreach ($name_to_keys[$entry] as $key) {
1888 $migrated[] = $key; // every key wearing this name — fail toward more exclusion
1889 }
1890 $converted++;
1891 } else {
1892 $migrated[] = $entry; // unresolved — keep; still honored by name everywhere
1893 }
1894 }
1895 $migrated = array_values(array_unique($migrated));
1896
1897 update_option('mxchat_acf_excluded_fields', $migrated);
1898 update_option('mxchat_acf_exclusions_migrated', '1', false);
1899 if ($converted > 0) {
1900 update_option('mxchat_acf_exclusions_migrated_notice', '1', false);
1901 }
1902 }
1903 add_action('acf/init', 'mxchat_migrate_acf_exclusions_to_keys', 20);
1904
1905 /**
1906 * One-time notice after the ACF exclusion migration actually converted
1907 * name entries — the settings are worth a review, especially where one name
1908 * fanned out to several fields (every match is now excluded, on purpose).
1909 */
1910 function mxchat_show_acf_exclusions_migrated_notice() {
1911 if (!current_user_can('manage_options')) {
1912 return;
1913 }
1914 if (get_option('mxchat_acf_exclusions_migrated_notice', '') !== '1') {
1915 return;
1916 }
1917 ?>
1918 <div class="notice notice-info is-dismissible">
1919 <p>
1920 <strong><?php esc_html_e('MxChat: ACF field exclusions updated', 'mxchat'); ?></strong><br>
1921 <?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'); ?>
1922 </p>
1923 </div>
1924 <?php
1925 delete_option('mxchat_acf_exclusions_migrated_notice');
1926 }
1927 add_action('admin_notices', 'mxchat_show_acf_exclusions_migrated_notice');
1928
1929 /**
1930 * Ensure tables exist on every admin load for fresh installations
1931 * This is a safety net for cases where activation hook doesn't fire
1932 */
1933 function mxchat_ensure_tables_exist() {
1934 global $wpdb;
1935
1936 // Only run for admin users to avoid performance impact
1937 if (!current_user_can('administrator')) {
1938 return;
1939 }
1940
1941 // Check if we've already verified tables in this session
1942 static $tables_checked = false;
1943 if ($tables_checked) {
1944 return;
1945 }
1946 $tables_checked = true;
1947
1948 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1949 $queue_table = $wpdb->prefix . 'mxchat_processing_queue';
1950
1951 $sessions_table = $wpdb->prefix . 'mxchat_sessions';
1952
1953 $chat_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
1954 $queue_exists = $wpdb->get_var("SHOW TABLES LIKE '$queue_table'") === $queue_table;
1955 $sessions_exists = get_option('mxchat_session_store_ready') !== false
1956 || $wpdb->get_var("SHOW TABLES LIKE '$sessions_table'") === $sessions_table;
1957
1958 if (!$chat_exists || !$queue_exists || !$sessions_exists) {
1959 //error_log("MxChat: Tables missing on admin load, running activation");
1960 mxchat_activate();
1961 }
1962 }
1963
1964 /**
1965 * One-shot cleanup of legacy mxchat_history_<sid> option rows (plan 839c4c).
1966 *
1967 * 3.2.19 deduplicated per-session chat history into the transcripts table
1968 * (which already held a superset of every option copy measured), so nothing
1969 * writes these options any more — but an upgraded install still carries one
1970 * per session, at up to 64 KB a row (462 rows measured on one production
1971 * install). This drain replaces the old mxchat_cleanup_orphaned_chat_history()
1972 * reconciliation sweep, which existed only because there were two copies to
1973 * reconcile.
1974 *
1975 * b64b77 pattern throughout: an option_id bookmark that only moves forward
1976 * (a crash mid-batch re-processes at most one batch), delete_option() per row
1977 * so the object + notoptions caches stay coherent, batches drained off
1978 * non-AJAX admin_init plus the session-store maintenance cron. Once the
1979 * backlog is gone the state marks done and every later call is one cached
1980 * option read.
1981 */
1982 function mxchat_history_backlog_state() {
1983 // The state option name MUST NOT start with 'mxchat_history_' — the drain
1984 // deletes everything matching that prefix, and a state option inside the
1985 // pattern gets eaten by its own drain (caught by the 839c4c rig: batches
1986 // ran 2,2,2,1 instead of 2,2,1 because the bookmark row was being deleted
1987 // and re-created every pass).
1988 $state = get_option('mxchat_legacy_history_cleanup', array());
1989 if (!is_array($state)) {
1990 $state = array();
1991 }
1992
1993 return wp_parse_args($state, array(
1994 'done' => false,
1995 'last_option_id' => 0,
1996 'deleted' => 0,
1997 ));
1998 }
1999
2000 /**
2001 * Delete one batch of legacy history options.
2002 *
2003 * @param int $batch Rows per pass — capped small; these can be 64 KB rows.
2004 * @return int Option rows deleted in this pass.
2005 */
2006 function mxchat_history_backlog_batch($batch = 200) {
2007 global $wpdb;
2008
2009 $state = mxchat_history_backlog_state();
2010 if (!empty($state['done'])) {
2011 return 0;
2012 }
2013
2014 $batch = max(1, (int) $batch);
2015
2016 $rows = $wpdb->get_results(
2017 $wpdb->prepare(
2018 "SELECT option_id, option_name FROM {$wpdb->options}
2019 WHERE option_id > %d AND option_name LIKE %s
2020 ORDER BY option_id ASC LIMIT %d",
2021 (int) $state['last_option_id'],
2022 $wpdb->esc_like('mxchat_history_') . '%',
2023 $batch
2024 ),
2025 ARRAY_A
2026 );
2027
2028 if (empty($rows)) {
2029 $state['done'] = true;
2030 update_option('mxchat_legacy_history_cleanup', $state, 'no');
2031 return 0;
2032 }
2033
2034 foreach ($rows as $row) {
2035 $state['last_option_id'] = max((int) $state['last_option_id'], (int) $row['option_id']);
2036 delete_option($row['option_name']);
2037 $state['deleted'] = (int) $state['deleted'] + 1;
2038 }
2039
2040 if (count($rows) < $batch) {
2041 $state['done'] = true;
2042 }
2043
2044 update_option('mxchat_legacy_history_cleanup', $state, 'no');
2045
2046 return count($rows);
2047 }
2048
2049 /** One batch per admin page load until drained. */
2050 function mxchat_history_backlog_drain() {
2051 mxchat_history_backlog_batch();
2052 }
2053
2054 /** Cron leg: drain faster, same cap per batch. */
2055 function mxchat_history_backlog_drain_cron() {
2056 for ($i = 0; $i < 10; $i++) {
2057 if (mxchat_history_backlog_batch() === 0) {
2058 break;
2059 }
2060 }
2061 }
2062
2063 // wp_doing_ajax() guard is load-bearing, not defensive (b64b77's shipped-and-
2064 // caught defect): admin-ajax.php fires admin_init too, and the chat widget's
2065 // message endpoint is an admin-ajax action — without the guard an anonymous
2066 // visitor would pay for a delete batch inside their own chat request.
2067 if (!wp_doing_ajax()) {
2068 add_action('admin_init', 'mxchat_history_backlog_drain', 21);
2069 }
2070 // Belt for installs whose admin is rarely visited: ride the session store's
2071 // existing daily maintenance event rather than scheduling another.
2072 add_action('mxchat_session_store_maintenance', 'mxchat_history_backlog_drain_cron');
2073
2074 function mxchat_migrate_live_agent_status() {
2075 $options = get_option('mxchat_options', []);
2076
2077 // Check if live_agent_status exists
2078 if (isset($options['live_agent_status'])) {
2079 $current_status = $options['live_agent_status'];
2080 $needs_update = false;
2081
2082 // Convert to new format if needed
2083 if ($current_status === 'online') {
2084 $options['live_agent_status'] = 'on';
2085 $needs_update = true;
2086 } else if ($current_status === 'offline') {
2087 $options['live_agent_status'] = 'off';
2088 $needs_update = true;
2089 } else if (!in_array($current_status, ['on', 'off'])) {
2090 // Default to off for any unexpected values
2091 $options['live_agent_status'] = 'off';
2092 $needs_update = true;
2093 }
2094
2095 // Only update if needed
2096 if ($needs_update) {
2097 update_option('mxchat_options', $options);
2098 }
2099 } else {
2100 // If status doesn't exist, set default to off
2101 $options['live_agent_status'] = 'off';
2102 update_option('mxchat_options', $options);
2103 }
2104 }
2105
2106 function mxchat_handle_live_agent_update() {
2107 // Get the CURRENT stored version (before it gets updated)
2108 $current_version = get_option('mxchat_plugin_version', '0.0.0');
2109 $new_version = '2.2.2';
2110
2111 // Only run this once for the update to 2.2.2
2112 $update_handled = get_option('mxchat_live_agent_update_2_2_2_handled', false);
2113
2114 // Check if we're upgrading TO 2.2.2 and haven't handled this yet
2115 if (version_compare($current_version, $new_version, '<') && !$update_handled) {
2116 $options = get_option('mxchat_options', array());
2117
2118 // Check if live agent was previously enabled
2119 if (isset($options['live_agent_status']) && $options['live_agent_status'] === 'on') {
2120 // Disable live agent
2121 $options['live_agent_status'] = 'off';
2122 update_option('mxchat_options', $options);
2123
2124 // Set flag to show the notification banner
2125 update_option('mxchat_show_live_agent_disabled_notice', true);
2126 }
2127
2128 // Mark this update as handled
2129 update_option('mxchat_live_agent_update_2_2_2_handled', true);
2130 }
2131 }
2132
2133 /**
2134 * Handle theme migration notice for version 3.0.1
2135 * Shows a dismissible notice to Pro users about migrating AI-generated themes
2136 */
2137 function mxchat_handle_theme_migration_notice() {
2138 // Get the CURRENT stored version (before it gets updated)
2139 $current_version = get_option('mxchat_plugin_version', '0.0.0');
2140 $target_version = '3.0.1';
2141
2142 // Only run this once for the update to 3.0.1
2143 $update_handled = get_option('mxchat_theme_migration_update_3_0_1_handled', false);
2144
2145 // Check if we're upgrading TO 3.0.1 and haven't handled this yet
2146 if (version_compare($current_version, $target_version, '<') && !$update_handled) {
2147 // Check if Pro is activated - only show to Pro users
2148 $license_status = get_option('mxchat_license_status', 'inactive');
2149 $is_pro = ($license_status === 'active');
2150
2151 if ($is_pro) {
2152 // Set flag to show the theme migration notification banner
2153 update_option('mxchat_show_theme_migration_notice', true);
2154 }
2155
2156 // Mark this update as handled (whether Pro or not)
2157 update_option('mxchat_theme_migration_update_3_0_1_handled', true);
2158 }
2159 }
2160
2161 // Initialize plugin safely
2162 function mxchat_init() {
2163 // Include all class files first
2164 mxchat_include_classes();
2165
2166 // Run update check (this also ensures tables exist)
2167 mxchat_check_for_update();
2168
2169 // CRITICAL: Ensure tables exist on admin pages (safety net)
2170 add_action('admin_init', 'mxchat_ensure_tables_exist', 1);
2171
2172 // Add fallback rate limit check
2173 add_action('init', 'mxchat_check_fallback_rate_limits', 5);
2174
2175 // Add migration notice hook
2176 add_action('admin_notices', 'mxchat_show_migration_notice');
2177 add_action('admin_notices', 'mxchat_show_model_access_notice');
2178
2179 // Initialize classes with error handling
2180 try {
2181 // Initialize admin classes
2182 if (is_admin()) {
2183 if (class_exists('MxChat_Knowledge_Manager')) {
2184 $mxchat_knowledge_manager = new MxChat_Knowledge_Manager();
2185
2186 if (class_exists('MxChat_Admin')) {
2187 $mxchat_admin = new MxChat_Admin($mxchat_knowledge_manager);
2188 }
2189 }
2190
2191 // Initialize meta box class
2192 if (class_exists('MxChat_Meta_Box')) {
2193 new MxChat_Meta_Box();
2194 }
2195
2196 }
2197
2198 // Initialize content generator globally — it registers wp_head hook
2199 // for frontend CSS injection, plus wp_ajax_ hooks for admin.
2200 if (class_exists('MxChat_Content_Generator')) {
2201 new MxChat_Content_Generator();
2202 }
2203
2204 // Initialize cache purge globally — settings writes can happen on any
2205 // request type (admin screens, admin-ajax autosave, wp-cli), and the
2206 // deferred-purge cron event fires on front-end requests.
2207 if (class_exists('MxChat_Cache_Purge')) {
2208 MxChat_Cache_Purge::init();
2209 }
2210
2211 // Initialize REST API globally — endpoints must be registered on
2212 // every request (admin and frontend) so they're reachable via /wp-json/.
2213 // Endpoints are auth-gated and locked until the site owner generates
2214 // a token in MxChat → API Access.
2215 if (class_exists('MxChat_Rest_Api')) {
2216 new MxChat_Rest_Api();
2217 }
2218
2219 // Initialize public classes
2220 if (class_exists('MxChat_Public')) {
2221 $mxchat_public = new MxChat_Public();
2222 }
2223
2224 if (class_exists('MxChat_Integrator')) {
2225 global $mxchat_integrator;
2226 $mxchat_integrator = new MxChat_Integrator();
2227 }
2228
2229 } catch (Exception $e) {
2230 //error_log('MxChat initialization error: ' . $e->getMessage());
2231
2232 // Show admin notice if there's an error
2233 if (is_admin()) {
2234 add_action('admin_notices', function() use ($e) {
2235 echo '<div class="notice notice-error"><p>';
2236 echo '<strong>MxChat Error:</strong> Plugin initialization failed. ';
2237 echo 'Please check error logs or contact support. Error: ' . esc_html($e->getMessage());
2238 echo '</p></div>';
2239 });
2240 }
2241 }
2242 }
2243
2244 // Run initialization on plugins_loaded
2245 add_action('plugins_loaded', 'mxchat_init');
2246
2247 // Run migration check on admin init (for auto-updates without reactivation)
2248 add_action('admin_init', 'mxchat_check_and_run_migrations');
2249
2250 /**
2251 * Check and run migrations on admin init
2252 * This ensures migrations run even when plugin is auto-updated
2253 */
2254 function mxchat_check_and_run_migrations() {
2255 // Only run in admin and not on every request
2256 static $checked = false;
2257 if ($checked) {
2258 return;
2259 }
2260 $checked = true;
2261
2262 mxchat_migrate_pinecone_roles_add_bot_id();
2263 mxchat_migrate_add_content_type_column();
2264 mxchat_migrate_add_translations_table();
2265 mxchat_migrate_add_session_ratings_table();
2266 }
2267
2268 /**
2269 * Migration: Create per-session satisfaction ratings table (v3.2.6)
2270 * For users upgrading from versions before 3.2.6
2271 */
2272 function mxchat_migrate_add_session_ratings_table() {
2273 $migration_key = 'mxchat_session_ratings_table_created';
2274 if (get_option($migration_key)) {
2275 return;
2276 }
2277 mxchat_create_session_ratings_table();
2278 update_option($migration_key, '3.2.6');
2279 }
2280
2281 /**
2282 * Migration: Create transcript translations table (v3.0.4)
2283 * For users upgrading from versions before 3.0.4
2284 */
2285 function mxchat_migrate_add_translations_table() {
2286 $migration_key = 'mxchat_translations_table_created';
2287
2288 // Check if migration already ran
2289 if (get_option($migration_key)) {
2290 return;
2291 }
2292
2293 // Create the translations table
2294 mxchat_create_translations_table();
2295
2296 // Mark migration as complete
2297 update_option($migration_key, '3.0.4');
2298 }
2299
2300 /**
2301 * Migration: Update deprecated Gemini embedding model (v3.0.5)
2302 * Updates gemini-embedding-exp-03-07 to gemini-embedding-001 for users who had it selected
2303 */
2304 function mxchat_migrate_gemini_embedding_model() {
2305 $options = get_option('mxchat_options', array());
2306
2307 if (isset($options['embedding_model']) && $options['embedding_model'] === 'gemini-embedding-exp-03-07') {
2308 $options['embedding_model'] = 'gemini-embedding-001';
2309 update_option('mxchat_options', $options);
2310 }
2311 }
2312
2313 // Register activation hook
2314 register_activation_hook(__FILE__, 'mxchat_activate');
2315
2316 // Add cron schedule
2317 add_filter('cron_schedules', function($schedules) {
2318 $schedules['one_minute'] = array(
2319 'interval' => 60,
2320 'display' => 'Every Minute'
2321 );
2322 return $schedules;
2323 });
2324
2325 // Register deactivation hook
2326 register_deactivation_hook(__FILE__, 'mxchat_deactivate');
2327
2328 /**
2329 * Per-session satisfaction rating: AJAX save handler (v3.2.6).
2330 * Records one 👍/👎 + optional feedback per chat session. The UNIQUE KEY on
2331 * session_id makes this naturally idempotent — only the first rating per
2332 * session is stored; duplicate POSTs are silent no-ops.
2333 */
2334 function mxchat_save_session_rating() {
2335 global $wpdb;
2336
2337 $session_id = isset($_POST['session_id']) ? MxChat_Utils::sanitize_session_id(wp_unslash($_POST['session_id'])) : '';
2338 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field(wp_unslash($_POST['bot_id'])) : 'default';
2339 $rating_raw = isset($_POST['rating']) ? (int) $_POST['rating'] : 0;
2340 $feedback = isset($_POST['feedback']) ? sanitize_textarea_field(wp_unslash($_POST['feedback'])) : '';
2341
2342 if ($session_id === '' || ($rating_raw !== 1 && $rating_raw !== -1)) {
2343 wp_send_json_error(array('message' => 'invalid_input'), 400);
2344 }
2345
2346 if (strlen($feedback) > 1000) {
2347 $feedback = substr($feedback, 0, 1000);
2348 }
2349
2350 $table_name = $wpdb->prefix . 'mxchat_session_ratings';
2351 $existing = $wpdb->get_var($wpdb->prepare(
2352 "SELECT id FROM $table_name WHERE session_id = %s LIMIT 1",
2353 $session_id
2354 ));
2355
2356 if ($existing) {
2357 if ($feedback !== '') {
2358 $wpdb->update(
2359 $table_name,
2360 array('rating_feedback' => $feedback),
2361 array('id' => (int) $existing),
2362 array('%s'),
2363 array('%d')
2364 );
2365 }
2366 wp_send_json_success(array('updated' => true));
2367 }
2368
2369 $inserted = $wpdb->insert(
2370 $table_name,
2371 array(
2372 'session_id' => $session_id,
2373 'bot_id' => $bot_id !== '' ? $bot_id : 'default',
2374 'rating_value' => $rating_raw,
2375 'rating_feedback' => $feedback !== '' ? $feedback : null,
2376 'created_at' => current_time('mysql'),
2377 ),
2378 array('%s', '%s', '%d', '%s', '%s')
2379 );
2380
2381 if ($inserted === false) {
2382 wp_send_json_error(array('message' => 'db_insert_failed'), 500);
2383 }
2384
2385 wp_send_json_success(array('saved' => true));
2386 }
2387 add_action('wp_ajax_mxchat_save_rating', 'mxchat_save_session_rating');
2388 add_action('wp_ajax_nopriv_mxchat_save_rating', 'mxchat_save_session_rating');