PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.14
MxChat – AI Chatbot & Content Generation for WordPress v3.2.14
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / mxchat-basic.php

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

1,756 lines 64.9 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.14
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 if (MXCHAT_DEV_MODE) {
28 $version .= '.' . time();
29 }
30 define('MXCHAT_VERSION', $version);
31 }
32
33 /**
34 * Honest, versioned User-Agent for MXChat remote-content ingestion fetches
35 * (Knowledge Base PDF import, URL import, sitemap / website crawl).
36 *
37 * WAF rulesets (SiteGround/ModSecurity, Wordfence, Cloudflare managed rules)
38 * flag stale spoofed-browser UAs as scrapers and return 403 — which silently
39 * broke the single most common KB source: self-hosted media on the site's own
40 * domain. A truthful crawler identifier is the industry norm for well-behaved
41 * bots and lets a site owner allowlist "MXChatBot" in their WAF. Filterable so
42 * a locked-down host can supply a different string without a code change.
43 */
44 if (!function_exists('mxchat_ingest_user_agent')) {
45 function mxchat_ingest_user_agent() {
46 $version = defined('MXCHAT_VERSION') ? MXCHAT_VERSION : '1.0';
47 $ua = 'MXChatBot/' . $version . ' (+https://mxchat.ai/bot)';
48 return apply_filters('mxchat_ingest_user_agent', $ua);
49 }
50 }
51
52 function mxchat_load_textdomain() {
53 $domain = 'mxchat';
54 $locale = determine_locale();
55
56 // First, try to load from /wp-content/languages/plugins/ (preserved during updates)
57 $mo_file = WP_LANG_DIR . '/plugins/' . $domain . '-' . $locale . '.mo';
58 if (file_exists($mo_file)) {
59 load_textdomain($domain, $mo_file);
60 return;
61 }
62
63 // Fallback to plugin's /languages directory
64 load_plugin_textdomain($domain, false, dirname(plugin_basename(__FILE__)) . '/languages');
65 }
66 add_action('init', 'mxchat_load_textdomain');
67
68 /**
69 * One-time migration: gemini-3-pro-preview was shut down by Google on March 9, 2026.
70 * Existing installs with the dead ID get auto-remapped to gemini-3.1-pro-preview
71 * (Google's official migration target) the first time admin_init fires after update.
72 */
73 add_action('admin_init', function () {
74 if (get_option('mxchat_gemini_3_remap_done')) {
75 return;
76 }
77 $opts = get_option('mxchat_options');
78 if (is_array($opts) && isset($opts['model']) && $opts['model'] === 'gemini-3-pro-preview') {
79 $opts['model'] = 'gemini-3.1-pro-preview';
80 update_option('mxchat_options', $opts);
81 }
82 if (is_array($opts) && isset($opts['content_model']) && $opts['content_model'] === 'gemini-3-pro-preview') {
83 $opts['content_model'] = 'gemini-3.1-pro-preview';
84 update_option('mxchat_options', $opts);
85 }
86 update_option('mxchat_gemini_3_remap_done', 1);
87 });
88
89 /**
90 * One-time migration: the Grok 2 family was retired by xAI (grok-2, grok-2-1212,
91 * grok-2-latest, grok-2-vision-1212 all return 400 "Model not found"). Existing
92 * installs with the dead ID get auto-remapped to grok-4-1-fast-non-reasoning
93 * (modern, fast, broadly available) the first time admin_init fires after update.
94 */
95 add_action('admin_init', function () {
96 if (get_option('mxchat_grok_2_remap_done')) {
97 return;
98 }
99 $opts = get_option('mxchat_options');
100 if (is_array($opts) && isset($opts['model']) && $opts['model'] === 'grok-2') {
101 $opts['model'] = 'grok-4-1-fast-non-reasoning';
102 update_option('mxchat_options', $opts);
103 }
104 if (is_array($opts) && isset($opts['content_model']) && $opts['content_model'] === 'grok-2') {
105 $opts['content_model'] = 'grok-4-1-fast-non-reasoning';
106 update_option('mxchat_options', $opts);
107 }
108 update_option('mxchat_grok_2_remap_done', 1);
109 });
110
111 /**
112 * One-time migration: Anthropic retired the Claude 4 (2025-05-14) snapshots on
113 * June 15, 2026 — claude-opus-4-20250514 and claude-sonnet-4-20250514 now return
114 * an API error. Existing installs with a dead ID get auto-remapped to the current
115 * equivalents Anthropic recommends (Opus 4.8 / Sonnet 4.6) the first time admin_init
116 * fires after update. Mirrors the gemini-3-pro-preview / grok-2 rescues above.
117 */
118 add_action('admin_init', function () {
119 if (get_option('mxchat_claude_4_retire_remap_done')) {
120 return;
121 }
122 $map = array(
123 'claude-opus-4-20250514' => 'claude-opus-4-8',
124 'claude-sonnet-4-20250514' => 'claude-sonnet-4-6',
125 );
126 $opts = get_option('mxchat_options');
127 if (is_array($opts)) {
128 $changed = false;
129 if (isset($opts['model']) && isset($map[$opts['model']])) {
130 $opts['model'] = $map[$opts['model']];
131 $changed = true;
132 }
133 if (isset($opts['content_model']) && isset($map[$opts['content_model']])) {
134 $opts['content_model'] = $map[$opts['content_model']];
135 $changed = true;
136 }
137 if ($changed) {
138 update_option('mxchat_options', $opts);
139 }
140 }
141 update_option('mxchat_claude_4_retire_remap_done', 1);
142 });
143
144 /**
145 * Exclude MxChat assets from caching plugin optimizations
146 *
147 * This prevents issues with WP Rocket, LiteSpeed Cache, Autoptimize, WP Super Cache,
148 * W3 Total Cache, SG Optimizer, and similar plugins that may break the chatbot by
149 * removing "unused" CSS, minifying/combining JS, or deferring/delaying jQuery.
150 *
151 * Both chat-script.js and floating-script.js depend on jQuery, so jQuery must also
152 * be excluded from any optimization that changes load order or timing.
153 */
154
155 // ── WP Rocket ────────────────────────────────────────────────────────────────
156
157 // Exclude from Remove Unused CSS (RUCSS)
158 add_filter('rocket_rucss_inline_atts_exclusions', function($exclusions) {
159 if (!is_array($exclusions)) $exclusions = array();
160 $exclusions[] = 'mxchat';
161 return $exclusions;
162 });
163
164 // Exclude CSS from minification/combination
165 add_filter('rocket_exclude_css', function($excluded) {
166 if (!is_array($excluded)) $excluded = array();
167 $excluded[] = '/plugins/mxchat-basic/css/chat-style.css';
168 return $excluded;
169 });
170
171 // Exclude JS from minification/combination
172 add_filter('rocket_exclude_js', function($excluded) {
173 if (!is_array($excluded)) $excluded = array();
174 $excluded[] = '/plugins/mxchat-basic/js/chat-script.js';
175 $excluded[] = '/plugins/mxchat-basic/js/floating-script.js';
176 $excluded[] = '/jquery-core';
177 $excluded[] = '/jquery.min.js';
178 $excluded[] = '/jquery.js';
179 $excluded[] = '/jquery-migrate';
180 return $excluded;
181 });
182
183 // Exclude JS from defer
184 add_filter('rocket_exclude_defer_js', function($excluded) {
185 if (!is_array($excluded)) $excluded = array();
186 $excluded[] = '/plugins/mxchat-basic/js/chat-script.js';
187 $excluded[] = '/plugins/mxchat-basic/js/floating-script.js';
188 $excluded[] = '/jquery-core';
189 $excluded[] = '/jquery.min.js';
190 $excluded[] = '/jquery.js';
191 $excluded[] = '/jquery-migrate';
192 return $excluded;
193 });
194
195 // Exclude from delay JS execution
196 add_filter('rocket_delay_js_exclusions', function($excluded) {
197 if (!is_array($excluded)) $excluded = array();
198 $excluded[] = 'mxchat';
199 $excluded[] = 'chat-script';
200 $excluded[] = 'floating-script';
201 $excluded[] = '/jquery-core';
202 $excluded[] = '/jquery.min.js';
203 $excluded[] = '/jquery.js';
204 $excluded[] = '/jquery-migrate';
205 return $excluded;
206 });
207
208 // ── LiteSpeed Cache ──────────────────────────────────────────────────────────
209
210 // Exclude CSS from optimization
211 add_filter('litespeed_optimize_css_excludes', function($excluded) {
212 if (!is_array($excluded)) $excluded = array();
213 $excluded[] = 'chat-style.css';
214 $excluded[] = 'mxchat';
215 return $excluded;
216 });
217
218 // Exclude from UCSS (Unique CSS) - prevents LiteSpeed from stripping "unused" MxChat CSS
219 add_filter('litespeed_ucss_whitelist', function($whitelist) {
220 if (!is_array($whitelist)) $whitelist = array();
221 $whitelist[] = '.mxchat-chatbot-wrapper';
222 $whitelist[] = '.floating-chatbot';
223 $whitelist[] = '.floating-chatbot-button';
224 $whitelist[] = '.chatbot-top-bar';
225 $whitelist[] = '.mxchat-chatbot';
226 $whitelist[] = '.chat-container';
227 $whitelist[] = '.chat-box';
228 $whitelist[] = '.bot-message';
229 $whitelist[] = '.input-container';
230 $whitelist[] = '.chat-input';
231 $whitelist[] = '.send-button';
232 $whitelist[] = '.pre-chat-message';
233 $whitelist[] = '.mxchat-popular-questions';
234 $whitelist[] = '.chat-toolbar';
235 $whitelist[] = '.exit-chat';
236 $whitelist[] = '.email-blocker';
237 return $whitelist;
238 });
239
240 // Exclude CSS from CCSS (Critical CSS) generation
241 add_filter('litespeed_optm_ccss_exc', function($excluded) {
242 if (!is_array($excluded)) $excluded = array();
243 $excluded[] = 'chat-style.css';
244 $excluded[] = 'mxchat';
245 return $excluded;
246 });
247
248 // Exclude JS from defer
249 add_filter('litespeed_optm_js_defer_exc', function($excluded) {
250 if (!is_array($excluded)) $excluded = array();
251 $excluded[] = 'chat-script.js';
252 $excluded[] = 'floating-script.js';
253 $excluded[] = 'mxchat';
254 $excluded[] = 'jquery.min.js';
255 $excluded[] = 'jquery.js';
256 return $excluded;
257 });
258
259 // Exclude JS from combining
260 add_filter('litespeed_optm_js_exc', function($excluded) {
261 if (!is_array($excluded)) $excluded = array();
262 $excluded[] = 'chat-script.js';
263 $excluded[] = 'floating-script.js';
264 $excluded[] = 'mxchat';
265 $excluded[] = 'jquery.min.js';
266 $excluded[] = 'jquery.js';
267 return $excluded;
268 });
269
270 // Exclude JS from delayed execution
271 add_filter('litespeed_optm_js_delay_exc', function($excluded) {
272 if (!is_array($excluded)) $excluded = array();
273 $excluded[] = 'chat-script.js';
274 $excluded[] = 'floating-script.js';
275 $excluded[] = 'mxchat';
276 return $excluded;
277 });
278
279 // Exclude from Guest Mode optimization
280 add_filter('litespeed_guest_optm_exc', function($excluded) {
281 if (!is_array($excluded)) $excluded = array();
282 $excluded[] = 'mxchat';
283 $excluded[] = 'chat-style';
284 $excluded[] = 'chat-script';
285 $excluded[] = 'floating-script';
286 return $excluded;
287 });
288
289 // ── Autoptimize ──────────────────────────────────────────────────────────────
290
291 // Exclude CSS from optimization (comma-separated strings)
292 add_filter('autoptimize_filter_css_exclude', function($excluded) {
293 if (!is_string($excluded)) $excluded = '';
294 return $excluded . ', mxchat, chat-style.css';
295 });
296
297 // Exclude JS from optimization (comma-separated strings)
298 add_filter('autoptimize_filter_js_exclude', function($excluded) {
299 if (!is_string($excluded)) $excluded = '';
300 return $excluded . ', mxchat, chat-script.js, floating-script.js, jquery.min.js, jquery.js';
301 });
302
303 // ── SG Optimizer (SiteGround) ────────────────────────────────────────────────
304
305 add_filter('sgo_js_minify_exclude', function($excluded) {
306 if (!is_array($excluded)) $excluded = array();
307 $excluded[] = 'chat-script.js';
308 $excluded[] = 'floating-script.js';
309 $excluded[] = 'jquery.min.js';
310 return $excluded;
311 });
312
313 add_filter('sgo_javascript_combine_exclude', function($excluded) {
314 if (!is_array($excluded)) $excluded = array();
315 $excluded[] = 'chat-script.js';
316 $excluded[] = 'floating-script.js';
317 $excluded[] = 'jquery.min.js';
318 return $excluded;
319 });
320
321 add_filter('sgo_js_async_exclude', function($excluded) {
322 if (!is_array($excluded)) $excluded = array();
323 $excluded[] = 'chat-script.js';
324 $excluded[] = 'floating-script.js';
325 $excluded[] = 'jquery.min.js';
326 return $excluded;
327 });
328
329 // ── W3 Total Cache ───────────────────────────────────────────────────────────
330
331 add_filter('w3tc_minify_js_do_tag_minification', function($do_minify, $script_tag, $file) {
332 if (strpos($file, 'chat-script.js') !== false ||
333 strpos($file, 'floating-script.js') !== false ||
334 strpos($file, 'jquery.min.js') !== false ||
335 strpos($file, 'jquery.js') !== false) {
336 return false;
337 }
338 return $do_minify;
339 }, 10, 3);
340
341 // ── WP Super Cache ──────────────────────────────────────────────────────────
342
343 add_filter('wpsc_rejected_uri', function($rejected) {
344 if (!is_array($rejected)) $rejected = array();
345 $rejected[] = 'wp-admin/admin-ajax.php';
346 return $rejected;
347 });
348
349 // ── Page-cache bypass for chat AJAX (companion to the 3.2.6 nonce-race hotfix)
350 // Each cache plugin gets its own filter export so that visitors hitting an
351 // edge-cached page never receive cached chat-AJAX responses. The chat send /
352 // stream send / file upload all POST to /wp-admin/admin-ajax.php with
353 // `action=mxchat_*`. Without these exports, a cache plugin can stale a response
354 // and break the per-session nonce flow on the first message.
355
356 // WP Rocket — `rocket_cache_reject_uri` takes a flat array of regex strings.
357 add_filter('rocket_cache_reject_uri', function($uris) {
358 if (!is_array($uris)) $uris = array();
359 $uris[] = '/wp-admin/admin-ajax\.php\?action=mxchat_.*';
360 return $uris;
361 });
362
363 // LiteSpeed Cache — `litespeed_cache_no_cache_for_request` short-circuits
364 // caching when the request matches our chat-AJAX pattern.
365 add_filter('litespeed_cache_no_cache_for_request', function($no_cache) {
366 if ($no_cache) return $no_cache;
367 if (!empty($_SERVER['REQUEST_URI']) &&
368 strpos($_SERVER['REQUEST_URI'], '/wp-admin/admin-ajax.php') !== false &&
369 !empty($_REQUEST['action']) &&
370 strpos((string) $_REQUEST['action'], 'mxchat_') === 0) {
371 return true;
372 }
373 return $no_cache;
374 });
375
376 // W3 Total Cache — `w3tc_pgcache_request_skip_uri` flips page-cache off when
377 // the URI matches.
378 add_filter('w3tc_pgcache_request_skip_uri', function($skip) {
379 if ($skip) return $skip;
380 if (!empty($_SERVER['REQUEST_URI']) &&
381 strpos($_SERVER['REQUEST_URI'], '/wp-admin/admin-ajax.php') !== false &&
382 !empty($_REQUEST['action']) &&
383 strpos((string) $_REQUEST['action'], 'mxchat_') === 0) {
384 return true;
385 }
386 return $skip;
387 });
388
389 // FlyingPress — `flying_press_cacheable` takes a boolean and is run per
390 // request. Same pattern as LiteSpeed / W3TC.
391 add_filter('flying_press_cacheable', function($cacheable) {
392 if (!$cacheable) return $cacheable;
393 if (!empty($_SERVER['REQUEST_URI']) &&
394 strpos($_SERVER['REQUEST_URI'], '/wp-admin/admin-ajax.php') !== false &&
395 !empty($_REQUEST['action']) &&
396 strpos((string) $_REQUEST['action'], 'mxchat_') === 0) {
397 return false;
398 }
399 return $cacheable;
400 });
401
402 // Include classes with error handling
403 function mxchat_include_classes() {
404 $class_files = array(
405 'includes/class-mxchat-model-catalog.php',
406 'includes/class-mxchat-live-agent-schedule.php',
407 'includes/class-mxchat-tool-registry.php',
408 'includes/class-mxchat-integrator.php',
409 'includes/class-mxchat-admin.php',
410 'includes/class-mxchat-public.php',
411 'includes/class-mxchat-utils.php',
412 'includes/class-mxchat-user.php',
413 'includes/class-mxchat-meta-box.php',
414 'includes/class-mxchat-chunker.php',
415 'includes/class-mxchat-word-handler.php',
416 'includes/class-mxchat-content-generator.php',
417 'includes/class-mxchat-cache-purge.php',
418 'includes/class-mxchat-editor-assistant.php',
419 'includes/class-rest-api.php',
420 'admin/class-ajax-handler.php',
421 'admin/class-pinecone-manager.php',
422 'admin/class-knowledge-manager.php'
423 );
424
425 foreach ($class_files as $file) {
426 $file_path = plugin_dir_path(__FILE__) . $file;
427 if (file_exists($file_path)) {
428 require_once $file_path;
429 } else {
430 //error_log('MxChat: Missing class file - ' . $file);
431 }
432 }
433
434 // Register the native function-calling admin-post save handler (a41dee).
435 if (class_exists('MxChat_Tool_Registry')) {
436 MxChat_Tool_Registry::init();
437 }
438
439 // Editor Assistant — free, OFF-by-default block-editor AI actions (plan-8cb0cb).
440 // init() wires REST + streaming AJAX + sidebar enqueue ONLY when the
441 // mxchat_editor_assistant_enabled option is 'on'; otherwise zero footprint.
442 if (class_exists('MxChat_Editor_Assistant')) {
443 MxChat_Editor_Assistant::init();
444 }
445
446 // Admin pages that aren't classes (procedural include).
447 if (is_admin()) {
448 $admin_api_page = plugin_dir_path(__FILE__) . 'includes/admin-api-page.php';
449 if (file_exists($admin_api_page)) {
450 require_once $admin_api_page;
451 }
452 // f7c7d4 renamed this file admin-dashboard-page.php → admin-onboarding-page.php.
453 // The require MUST live here (admin bootstrap) and not just inside
454 // mxchat_add_plugin_page() on the admin_menu hook — admin_menu does NOT
455 // fire on admin-ajax.php requests, so the wizard's AJAX handlers
456 // (plan-905439: mxchat_onboarding_kb_status / save_step / mark_step /
457 // auto_graduate + the f7c7d4 dismiss handler) would never register.
458 $admin_onboarding_page = plugin_dir_path(__FILE__) . 'includes/admin-onboarding-page.php';
459 if (file_exists($admin_onboarding_page)) {
460 require_once $admin_onboarding_page;
461 }
462 }
463 }
464
465 /**
466 * Lazy-load the PDF parser library only when needed.
467 * Avoids loading 44 files on every page request.
468 */
469 function mxchat_load_pdf_parser() {
470 if (class_exists('\Smalot\PdfParser\Parser')) {
471 return true;
472 }
473 $autoload_path = plugin_dir_path(__FILE__) . 'includes/pdf-parser/alt_autoload.php';
474 if (file_exists($autoload_path)) {
475 require_once $autoload_path;
476 return true;
477 }
478 return false;
479 }
480
481 /**
482 * Create URL click tracking table
483 */
484 function mxchat_create_url_clicks_table() {
485 global $wpdb;
486
487 $table_name = $wpdb->prefix . 'mxchat_url_clicks';
488
489 $charset_collate = $wpdb->get_charset_collate();
490
491 $sql = "CREATE TABLE $table_name (
492 id mediumint(9) NOT NULL AUTO_INCREMENT,
493 session_id varchar(100) NOT NULL,
494 clicked_url text NOT NULL,
495 message_context text,
496 click_timestamp datetime DEFAULT CURRENT_TIMESTAMP,
497 user_ip varchar(45),
498 user_agent text,
499 PRIMARY KEY (id),
500 KEY session_id (session_id),
501 KEY click_timestamp (click_timestamp)
502 ) $charset_collate;";
503
504 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
505 dbDelta($sql);
506 }
507
508 /**
509 * FIXED: Robust table creation and column management
510 */
511 function mxchat_create_chat_transcripts_table() {
512 global $wpdb;
513
514 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
515 $charset_collate = $wpdb->get_charset_collate();
516
517 // Create table with ALL columns including user_name from the start
518 $sql = "CREATE TABLE $table_name (
519 id MEDIUMINT(9) NOT NULL AUTO_INCREMENT,
520 user_id MEDIUMINT(9) DEFAULT 0,
521 session_id VARCHAR(255) NOT NULL,
522 role VARCHAR(255) NOT NULL,
523 message TEXT NOT NULL,
524 user_email VARCHAR(255) DEFAULT NULL,
525 user_name VARCHAR(100) DEFAULT NULL,
526 user_identifier VARCHAR(255) DEFAULT NULL,
527 originating_page_url TEXT DEFAULT NULL,
528 originating_page_title VARCHAR(500) DEFAULT NULL,
529 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
530 PRIMARY KEY (id),
531 KEY session_id (session_id),
532 KEY user_email (user_email),
533 KEY timestamp (timestamp)
534 ) $charset_collate;";
535
536 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
537 $result = dbDelta($sql);
538
539 // Log the result for debugging
540 if (empty($result)) {
541 //error_log("MxChat: dbDelta returned empty result for chat transcripts table");
542 } else {
543 //error_log("MxChat: dbDelta result: " . print_r($result, true));
544 }
545
546 // IMPORTANT: Ensure all columns exist for existing installations
547 mxchat_ensure_all_columns($table_name);
548 }
549
550 /**
551 * Ensure all required columns exist (for upgrades)
552 */
553 function mxchat_ensure_all_columns($table_name) {
554 global $wpdb;
555
556 // First check if table exists
557 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
558 if (!$table_exists) {
559 //error_log("MxChat: Table $table_name does not exist, cannot add columns");
560 return;
561 }
562
563 // Define all required columns and their types
564 $required_columns = [
565 'user_identifier' => 'VARCHAR(255) DEFAULT NULL',
566 'user_email' => 'VARCHAR(255) DEFAULT NULL',
567 'user_name' => 'VARCHAR(100) DEFAULT NULL',
568 'originating_page_url' => 'TEXT DEFAULT NULL',
569 'originating_page_title' => 'VARCHAR(500) DEFAULT NULL',
570 'rag_context' => 'LONGTEXT DEFAULT NULL'
571 ];
572
573 // Get existing columns
574 $existing_columns = $wpdb->get_results("SHOW COLUMNS FROM $table_name");
575 if (empty($existing_columns)) {
576 //error_log("MxChat: Could not get columns for table $table_name");
577 return;
578 }
579
580 $existing_column_names = array_column($existing_columns, 'Field');
581
582 // Add missing columns
583 foreach ($required_columns as $column_name => $column_definition) {
584 if (!in_array($column_name, $existing_column_names)) {
585 $alter_sql = "ALTER TABLE $table_name ADD COLUMN $column_name $column_definition";
586 $result = $wpdb->query($alter_sql);
587
588 if ($result === false) {
589 //error_log("MxChat: Failed to add column $column_name to $table_name. Error: " . $wpdb->last_error);
590 } else {
591 //error_log("MxChat: Successfully added column $column_name to $table_name");
592 }
593 }
594 }
595 }
596
597 /**
598 * Add role restriction column to knowledge base table
599 */
600 function mxchat_add_role_restriction_column() {
601 global $wpdb;
602 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
603
604 // Check if table exists first
605 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
606 if (!$table_exists) {
607 //error_log("MxChat: System prompt content table does not exist, cannot add role_restriction column");
608 return;
609 }
610
611 // Check if column already exists
612 $column_exists = $wpdb->get_results(
613 $wpdb->prepare(
614 "SHOW COLUMNS FROM {$table_name} LIKE %s",
615 'role_restriction'
616 )
617 );
618
619 if (empty($column_exists)) {
620 $alter_sql = "ALTER TABLE {$table_name} ADD COLUMN role_restriction VARCHAR(50) DEFAULT 'public' AFTER source_url";
621 $result = $wpdb->query($alter_sql);
622
623 if ($result === false) {
624 //error_log("MxChat: Failed to add role_restriction column. Error: " . $wpdb->last_error);
625 } else {
626 //error_log("MxChat: Successfully added role_restriction column");
627
628 // Set all existing records to 'public' (everyone can access)
629 $update_result = $wpdb->query(
630 "UPDATE {$table_name}
631 SET role_restriction = 'public'
632 WHERE role_restriction IS NULL OR role_restriction = ''"
633 );
634
635 if ($update_result !== false) {
636 //error_log("MxChat: Updated {$update_result} existing records to public access");
637 }
638 }
639 }
640 }
641
642 /**
643 * Add enabled_bots column to intents table for multi-bot action filtering
644 */
645 function mxchat_add_enabled_bots_column() {
646 global $wpdb;
647 $table_name = $wpdb->prefix . 'mxchat_intents';
648
649 // Check if table exists first
650 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
651 if (!$table_exists) {
652 //error_log("MxChat: Intents table does not exist, cannot add enabled_bots column");
653 return;
654 }
655
656 // Check if column already exists
657 $column_exists = $wpdb->get_results(
658 $wpdb->prepare(
659 "SHOW COLUMNS FROM {$table_name} LIKE %s",
660 'enabled_bots'
661 )
662 );
663
664 if (empty($column_exists)) {
665 $alter_sql = "ALTER TABLE {$table_name} ADD COLUMN enabled_bots LONGTEXT DEFAULT NULL AFTER enabled";
666 $result = $wpdb->query($alter_sql);
667
668 if ($result === false) {
669 //error_log("MxChat: Failed to add enabled_bots column. Error: " . $wpdb->last_error);
670 } else {
671 //error_log("MxChat: Successfully added enabled_bots column");
672
673 // Set all existing actions to work with 'default' bot for backward compatibility
674 $default_bots = json_encode(['default']);
675 $update_result = $wpdb->query(
676 $wpdb->prepare(
677 "UPDATE {$table_name}
678 SET enabled_bots = %s
679 WHERE enabled_bots IS NULL OR enabled_bots = ''",
680 $default_bots
681 )
682 );
683
684 if ($update_result !== false) {
685 //error_log("MxChat: Updated {$update_result} existing actions to work with default bot");
686 }
687 }
688 }
689 }
690
691 /**
692 * Create Pinecone role restrictions table with multi-bot support
693 */
694 function mxchat_create_pinecone_roles_table() {
695 global $wpdb;
696
697 $table_name = $wpdb->prefix . 'mxchat_pinecone_roles';
698 $charset_collate = $wpdb->get_charset_collate();
699
700 $sql = "CREATE TABLE $table_name (
701 id mediumint(9) NOT NULL AUTO_INCREMENT,
702 vector_id varchar(255) NOT NULL,
703 bot_id varchar(50) NOT NULL DEFAULT 'default',
704 source_url text,
705 role_restriction varchar(50) DEFAULT 'public',
706 updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
707 PRIMARY KEY (id),
708 UNIQUE KEY vector_bot (vector_id, bot_id),
709 KEY role_restriction (role_restriction),
710 KEY bot_id (bot_id)
711 ) $charset_collate;";
712
713 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
714 dbDelta($sql);
715 }
716
717 /**
718 * Add bot_id column to mxchat_pinecone_roles table for multi-bot support
719 * This migration runs once to update existing installations
720 */
721 function mxchat_migrate_pinecone_roles_add_bot_id() {
722 global $wpdb;
723
724 // Check if migration already ran
725 $migration_version = get_option('mxchat_pinecone_roles_migration_version', '0');
726 if (version_compare($migration_version, '2.5.2', '>=')) {
727 return; // Already migrated
728 }
729
730 $table_name = $wpdb->prefix . 'mxchat_pinecone_roles';
731
732 // Check if table exists
733 if ($wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") != $table_name) {
734 return; // Table doesn't exist yet
735 }
736
737 // Check if bot_id column already exists
738 $column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$table_name} LIKE 'bot_id'");
739
740 if (empty($column_exists)) {
741 // Add bot_id column
742 $wpdb->query("ALTER TABLE {$table_name} ADD COLUMN bot_id VARCHAR(50) NOT NULL DEFAULT 'default' AFTER vector_id");
743
744 // Update the unique key to include bot_id
745 $wpdb->query("ALTER TABLE {$table_name} DROP INDEX vector_id");
746 $wpdb->query("ALTER TABLE {$table_name} ADD UNIQUE KEY vector_bot (vector_id, bot_id)");
747
748 // Add index for bot_id
749 $wpdb->query("ALTER TABLE {$table_name} ADD KEY bot_id (bot_id)");
750
751 //error_log('MxChat: Successfully added bot_id column to mxchat_pinecone_roles table');
752 }
753
754 // Mark migration as complete
755 update_option('mxchat_pinecone_roles_migration_version', '2.5.2');
756 }
757
758 /**
759 * 2.5.6: Add content_type column to mxchat_system_prompt_content table
760 * Enables filtering knowledge base by content type (posts, pages, PDFs, etc.)
761 */
762 function mxchat_migrate_add_content_type_column() {
763 global $wpdb;
764
765 // Check if migration already ran
766 $migration_version = get_option('mxchat_content_type_migration_version', '0');
767 if (version_compare($migration_version, '2.5.6', '>=')) {
768 return;
769 }
770
771 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
772
773 // Check if table exists
774 if ($wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") != $table_name) {
775 return;
776 }
777
778 // Check if content_type column already exists
779 $column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$table_name} LIKE 'content_type'");
780
781 if (empty($column_exists)) {
782 // Add content_type column with default value 'content' for backwards compatibility
783 $wpdb->query("ALTER TABLE {$table_name} ADD COLUMN content_type VARCHAR(50) DEFAULT 'content' AFTER role_restriction");
784
785 // Add index for better query performance
786 $wpdb->query("ALTER TABLE {$table_name} ADD KEY content_type (content_type)");
787
788 //error_log('MxChat: Successfully added content_type column to mxchat_system_prompt_content table');
789 }
790
791 // Mark migration as complete
792 update_option('mxchat_content_type_migration_version', '2.5.6');
793 }
794
795 /**
796 * 3.2.4: Backfill the active embedding model option for installs that already
797 * have KB content but no stamped model. The mismatch warning compares this
798 * against the user's currently selected model — no per-row column needed.
799 */
800 function mxchat_backfill_active_embedding_model() {
801 global $wpdb;
802
803 if (get_option('mxchat_active_embedding_model', '') !== '') {
804 return;
805 }
806
807 $kb_table = $wpdb->prefix . 'mxchat_system_prompt_content';
808 if ($wpdb->get_var("SHOW TABLES LIKE '{$kb_table}'") !== $kb_table) {
809 return;
810 }
811
812 $kb_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$kb_table}");
813 if ($kb_count > 0) {
814 $options = get_option('mxchat_options', array());
815 $current_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
816 update_option('mxchat_active_embedding_model', $current_model, false);
817 }
818 }
819
820 /**
821 * 2.5.2: Create queue processing tables for reliable background processing
822 */
823 function mxchat_create_queue_tables() {
824 global $wpdb;
825 $charset_collate = $wpdb->get_charset_collate();
826
827 // Main queue table
828 $queue_table = $wpdb->prefix . 'mxchat_processing_queue';
829 $sql_queue = "CREATE TABLE $queue_table (
830 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
831 queue_id varchar(64) NOT NULL,
832 item_type varchar(20) NOT NULL,
833 item_data longtext NOT NULL,
834 status varchar(20) NOT NULL DEFAULT 'pending',
835 bot_id varchar(50) NOT NULL DEFAULT 'default',
836 priority int(11) NOT NULL DEFAULT 0,
837 attempts int(11) NOT NULL DEFAULT 0,
838 max_attempts int(11) NOT NULL DEFAULT 3,
839 error_message text DEFAULT NULL,
840 created_at datetime NOT NULL,
841 started_at datetime DEFAULT NULL,
842 completed_at datetime DEFAULT NULL,
843 PRIMARY KEY (id),
844 KEY queue_id (queue_id),
845 KEY status (status),
846 KEY item_type (item_type),
847 KEY priority (priority)
848 ) $charset_collate;";
849
850 // Queue metadata table
851 $meta_table = $wpdb->prefix . 'mxchat_queue_meta';
852 $sql_meta = "CREATE TABLE $meta_table (
853 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
854 queue_id varchar(64) NOT NULL,
855 meta_key varchar(255) NOT NULL,
856 meta_value longtext,
857 PRIMARY KEY (id),
858 KEY queue_id (queue_id),
859 KEY meta_key (meta_key)
860 ) $charset_collate;";
861
862 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
863 dbDelta($sql_queue);
864 dbDelta($sql_meta);
865
866 //error_log("MxChat: Queue tables created/updated successfully");
867 }
868
869 /**
870 * Create transcript translations table for persisting translations
871 */
872 function mxchat_create_translations_table() {
873 global $wpdb;
874 $charset_collate = $wpdb->get_charset_collate();
875
876 $table_name = $wpdb->prefix . 'mxchat_transcript_translations';
877 $sql = "CREATE TABLE $table_name (
878 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
879 session_id varchar(255) NOT NULL,
880 language_code varchar(10) NOT NULL,
881 translations longtext NOT NULL,
882 created_at datetime NOT NULL,
883 updated_at datetime NOT NULL,
884 PRIMARY KEY (id),
885 UNIQUE KEY session_lang (session_id, language_code),
886 KEY session_id (session_id)
887 ) $charset_collate;";
888
889 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
890 dbDelta($sql);
891 }
892
893 /**
894 * Create per-session satisfaction ratings table (v3.2.6)
895 * Stores one 👍/👎 rating + optional feedback per chat session.
896 */
897 function mxchat_create_session_ratings_table() {
898 global $wpdb;
899 $charset_collate = $wpdb->get_charset_collate();
900
901 $table_name = $wpdb->prefix . 'mxchat_session_ratings';
902 $sql = "CREATE TABLE $table_name (
903 id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
904 session_id varchar(255) NOT NULL,
905 bot_id varchar(50) NOT NULL DEFAULT 'default',
906 rating_value tinyint(1) NOT NULL,
907 rating_feedback text DEFAULT NULL,
908 created_at datetime NOT NULL,
909 PRIMARY KEY (id),
910 UNIQUE KEY session_id (session_id),
911 KEY bot_id (bot_id),
912 KEY created_at (created_at)
913 ) $charset_collate;";
914
915 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
916 dbDelta($sql);
917 }
918
919 /**
920 * 2.5.2: Fix URL column size to support long URLs (especially with UTF-8 encoding)
921 * This fixes "url, source_url. The supplied values may be too long" errors
922 */
923 function mxchat_fix_url_column_size() {
924 global $wpdb;
925 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
926
927 // Check if table exists
928 $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
929 if (!$table_exists) {
930 return;
931 }
932
933 // Change url and source_url from VARCHAR to TEXT to handle long URLs
934 // This is especially important for URLs with UTF-8 encoded characters (Hebrew, Arabic, etc.)
935 $wpdb->query("ALTER TABLE {$table_name} MODIFY COLUMN url TEXT");
936 $wpdb->query("ALTER TABLE {$table_name} MODIFY COLUMN source_url TEXT");
937
938 //error_log("MxChat: Successfully updated url and source_url columns to TEXT type for long URL support");
939 }
940
941 /**
942 * Migrate deprecated AI models to their replacements
943 * Version 2.5.1: Migrate Claude 3.5 Sonnet (deprecated) to Claude 3.7 Sonnet
944 * Version 3.1.2: Convert chat transcripts table to utf8mb4 for emoji support
945 * Without utf8mb4, any bot response containing emojis silently fails to insert.
946 */
947 function mxchat_migrate_transcripts_charset() {
948 global $wpdb;
949 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
950 $wpdb->query("ALTER TABLE $table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
951 }
952
953 /**
954 * Version 3.0.55: Migrate GPT-4 series models (deprecated 2026-02-17) to GPT-5 series
955 */
956 function mxchat_migrate_deprecated_models() {
957 $options = get_option('mxchat_options', array());
958 $migrated = false;
959 $migration_message = '';
960
961 if (!isset($options['model'])) {
962 return;
963 }
964
965 $current_model = $options['model'];
966
967 // Migrate deprecated Claude models to Claude Opus 4.6 (recommended replacement per Anthropic)
968 $deprecated_claude_models = array(
969 'claude-3-5-sonnet-20240620', // Retired Oct 28, 2025
970 'claude-3-5-sonnet-20241022', // Retired Oct 28, 2025
971 'claude-3-7-sonnet-20250219', // Retiring Feb 19, 2026
972 'claude-3-opus-20240229', // Retired Jan 5, 2026
973 'claude-3-sonnet-20240229', // Legacy
974 'claude-3-haiku-20240307', // Legacy
975 );
976 if (in_array($current_model, $deprecated_claude_models, true)) {
977 $options['model'] = 'claude-opus-4-6';
978 $migrated = true;
979 $migration_message = sprintf(
980 'Your chatbot model has been automatically updated from %s to Claude Opus 4.6 due to Anthropic deprecating older Claude models.',
981 $current_model
982 );
983 }
984
985 // Migrate deprecated Claude Haiku 3.5 to Claude Haiku 4.5
986 if ($current_model === 'claude-3-5-haiku-20241022') {
987 $options['model'] = 'claude-haiku-4-5-20251001';
988 $migrated = true;
989 $migration_message = 'Your chatbot model has been automatically updated from Claude Haiku 3.5 to Claude Haiku 4.5 due to Anthropic deprecating the older model.';
990 }
991
992 // Migrate deprecated GPT-4 series and GPT-3.5 Turbo to GPT-5.1 Chat Latest
993 if (in_array($current_model, array('gpt-4o', 'gpt-4.1-2025-04-14', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'), true)) {
994 $options['model'] = 'gpt-5.1-chat-latest';
995 $migrated = true;
996 $migration_message = sprintf(
997 'Your chatbot model has been automatically updated from %s to GPT-5.1 Chat Latest due to OpenAI deprecating older models.',
998 $current_model
999 );
1000 }
1001
1002 // Migrate deprecated GPT-4o Mini and GPT-4.1 Mini to GPT-5 Mini
1003 if (in_array($current_model, array('gpt-4o-mini', 'gpt-4.1-mini'), true)) {
1004 $options['model'] = 'gpt-5-mini';
1005 $migrated = true;
1006 $migration_message = sprintf(
1007 'Your chatbot model has been automatically updated from %s to GPT-5 Mini due to OpenAI deprecating GPT-4 series models.',
1008 $current_model
1009 );
1010 }
1011
1012 if ($migrated) {
1013 update_option('mxchat_options', $options);
1014 update_option('mxchat_model_migrated_notice', true);
1015 update_option('mxchat_model_migration_message', $migration_message);
1016 }
1017 }
1018
1019 /**
1020 * Show admin notice after model migration
1021 */
1022 function mxchat_show_migration_notice() {
1023 if (get_option('mxchat_model_migrated_notice')) {
1024 $migration_message = get_option('mxchat_model_migration_message', __('Your chatbot model has been automatically updated due to a model deprecation.', 'mxchat'));
1025 ?>
1026 <div class="notice notice-info is-dismissible">
1027 <p>
1028 <strong><?php esc_html_e('MxChat Model Updated', 'mxchat'); ?></strong><br>
1029 <?php echo esc_html($migration_message); ?>
1030 </p>
1031 </div>
1032 <?php
1033 delete_option('mxchat_model_migrated_notice');
1034 delete_option('mxchat_model_migration_message');
1035 }
1036 }
1037
1038 function mxchat_activate() {
1039 global $wpdb;
1040 $charset_collate = $wpdb->get_charset_collate();
1041
1042 //error_log("MxChat: Running activation function");
1043
1044 // Create chat transcripts table with improved function
1045 mxchat_create_chat_transcripts_table();
1046
1047 // System Prompt Content Table - UPDATED: Use TEXT for url and source_url columns
1048 $system_prompt_table = $wpdb->prefix . 'mxchat_system_prompt_content';
1049 $sql_system_prompt = "CREATE TABLE $system_prompt_table (
1050 id MEDIUMINT(9) NOT NULL AUTO_INCREMENT,
1051 url TEXT NOT NULL,
1052 article_content LONGTEXT NOT NULL,
1053 embedding_vector LONGTEXT,
1054 source_url TEXT DEFAULT NULL,
1055 role_restriction VARCHAR(50) DEFAULT 'public',
1056 content_type VARCHAR(50) DEFAULT 'content',
1057 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1058 PRIMARY KEY (id),
1059 KEY content_type (content_type)
1060 ) $charset_collate;";
1061
1062 // Intents Table - NOW INCLUDES enabled_bots column from the start
1063 $intents_table = $wpdb->prefix . 'mxchat_intents';
1064 $sql_intents_table = "CREATE TABLE $intents_table (
1065 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
1066 intent_label VARCHAR(255) NOT NULL,
1067 phrases TEXT NOT NULL,
1068 embedding_vector LONGTEXT NOT NULL,
1069 callback_function VARCHAR(255) NOT NULL,
1070 similarity_threshold FLOAT DEFAULT 0.85,
1071 enabled TINYINT(1) NOT NULL DEFAULT 1,
1072 enabled_bots LONGTEXT DEFAULT NULL,
1073 PRIMARY KEY (id)
1074 ) $charset_collate;";
1075
1076 // Individual Intent Phrases Table - each phrase gets its own embedding vector
1077 $intent_phrases_table = $wpdb->prefix . 'mxchat_intent_phrases';
1078 $sql_intent_phrases_table = "CREATE TABLE $intent_phrases_table (
1079 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
1080 intent_id BIGINT(20) UNSIGNED NOT NULL,
1081 phrase TEXT NOT NULL,
1082 embedding_vector LONGTEXT NOT NULL,
1083 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
1084 PRIMARY KEY (id),
1085 KEY intent_id (intent_id)
1086 ) $charset_collate;";
1087
1088 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
1089
1090 // Create other tables
1091 dbDelta($sql_system_prompt);
1092 dbDelta($sql_intents_table);
1093 dbDelta($sql_intent_phrases_table);
1094
1095 // Create URL click tracking table
1096 mxchat_create_url_clicks_table();
1097
1098 // Create Pinecone roles table
1099 mxchat_create_pinecone_roles_table();
1100
1101 // NEW 2.5.2: Create queue processing tables
1102 mxchat_create_queue_tables();
1103
1104 // Create transcript translations table
1105 mxchat_create_translations_table();
1106
1107 // Create per-session satisfaction ratings table (v3.2.6)
1108 mxchat_create_session_ratings_table();
1109
1110 // Ensure additional columns in system prompt table
1111 $existing_system_columns = $wpdb->get_results("SHOW COLUMNS FROM $system_prompt_table");
1112 if (!empty($existing_system_columns)) {
1113 $existing_system_column_names = array_column($existing_system_columns, 'Field');
1114
1115 if (!in_array('embedding_vector', $existing_system_column_names)) {
1116 $wpdb->query("ALTER TABLE $system_prompt_table ADD COLUMN embedding_vector LONGTEXT");
1117 }
1118 if (!in_array('source_url', $existing_system_column_names)) {
1119 $wpdb->query("ALTER TABLE $system_prompt_table ADD COLUMN source_url TEXT DEFAULT NULL");
1120 }
1121 if (!in_array('role_restriction', $existing_system_column_names)) {
1122 $wpdb->query("ALTER TABLE $system_prompt_table ADD COLUMN role_restriction VARCHAR(50) DEFAULT 'public' AFTER source_url");
1123 }
1124 }
1125
1126 // Set default thresholds for existing intents
1127 $wpdb->query("UPDATE {$intents_table} SET similarity_threshold = 0.85 WHERE similarity_threshold IS NULL");
1128
1129 // Ensure enabled column exists in intents table
1130 $existing_intent_columns = $wpdb->get_results("SHOW COLUMNS FROM $intents_table");
1131 if (!empty($existing_intent_columns)) {
1132 $existing_intent_column_names = array_column($existing_intent_columns, 'Field');
1133
1134 if (!in_array('enabled', $existing_intent_column_names)) {
1135 $wpdb->query("ALTER TABLE $intents_table ADD COLUMN enabled TINYINT(1) NOT NULL DEFAULT 1");
1136 }
1137
1138 // Ensure enabled_bots column exists for existing installations
1139 if (!in_array('enabled_bots', $existing_intent_column_names)) {
1140 $wpdb->query("ALTER TABLE $intents_table ADD COLUMN enabled_bots LONGTEXT DEFAULT NULL AFTER enabled");
1141
1142 // Set existing actions to work with default bot
1143 $default_bots = json_encode(['default']);
1144 $wpdb->query($wpdb->prepare(
1145 "UPDATE {$intents_table} SET enabled_bots = %s WHERE enabled_bots IS NULL",
1146 $default_bots
1147 ));
1148 }
1149 }
1150
1151 // Run migration for existing installations
1152 mxchat_migrate_pinecone_roles_add_bot_id();
1153
1154 // 3.2.4: Backfill active embedding model option (replaces 3.2.3 column-based tracking)
1155 mxchat_backfill_active_embedding_model();
1156
1157 // Setup cron jobs
1158 mxchat_setup_cron_jobs();
1159
1160 // Update version
1161 update_option('mxchat_plugin_version', MXCHAT_VERSION);
1162
1163 //error_log("MxChat: Activation function completed");
1164 }
1165
1166 /**
1167 * Setup cron jobs on plugin activation
1168 */
1169 function mxchat_setup_cron_jobs() {
1170 // Clear any existing cron jobs first
1171 wp_clear_scheduled_hook('mxchat_reset_rate_limits');
1172
1173 // Check if WordPress cron is disabled
1174 if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
1175 // Set flag to use fallback system
1176 update_option('mxchat_use_fallback_rate_limits', true);
1177 update_option('mxchat_next_rate_limit_check', time() + 3600);
1178 // Deliberately NO early return (plan-bc08a6): transcript cleanup below
1179 // must still be scheduled. DISABLE_WP_CRON only changes HOW cron events
1180 // execute (a server-side runner hitting wp-cron.php instead of loopback
1181 // spawns) — scheduling still just writes the cron option. The old early
1182 // return here meant a deactivate/reactivate cycle on a DISABLE_WP_CRON
1183 // site permanently lost the transcript cleanup event while the retention
1184 // setting still claimed to be active.
1185 } else {
1186 // Schedule the rate limit reset cron job
1187 $result = wp_schedule_event(time() + 300, 'hourly', 'mxchat_reset_rate_limits');
1188
1189 if ($result === false) {
1190 // Fallback if scheduling fails
1191 update_option('mxchat_use_fallback_rate_limits', true);
1192 update_option('mxchat_next_rate_limit_check', time() + 3600);
1193 } else {
1194 // Clear fallback flags if cron scheduling succeeded
1195 delete_option('mxchat_use_fallback_rate_limits');
1196 }
1197 }
1198
1199 // Schedule transcript cleanup if configured (bucket dropdown OR custom retention-days > 0)
1200 $transcript_options = get_option('mxchat_transcripts_options', array());
1201 $cleanup_interval = isset($transcript_options['mxchat_auto_delete_transcripts']) ? $transcript_options['mxchat_auto_delete_transcripts'] : 'never';
1202 $custom_retention = isset($transcript_options['mxchat_retention_days']) ? (int) $transcript_options['mxchat_retention_days'] : 0;
1203
1204 if ($cleanup_interval !== 'never' || $custom_retention > 0) {
1205 // Check if not already scheduled
1206 if (!wp_next_scheduled('mxchat_cleanup_old_transcripts')) {
1207 // Schedule to run daily at 3 AM
1208 $next_run = strtotime('tomorrow 3:00 AM');
1209 wp_schedule_event($next_run, 'daily', 'mxchat_cleanup_old_transcripts');
1210 }
1211 }
1212 }
1213
1214 /**
1215 * Clean up on plugin deactivation
1216 */
1217 function mxchat_deactivate() {
1218 // Clear scheduled cron jobs
1219 wp_clear_scheduled_hook('mxchat_reset_rate_limits');
1220 wp_clear_scheduled_hook('mxchat_cleanup_old_transcripts');
1221 wp_clear_scheduled_hook('mxchat_send_delayed_transcript');
1222
1223 // Clear fallback options
1224 delete_option('mxchat_use_fallback_rate_limits');
1225 delete_option('mxchat_next_rate_limit_check');
1226 delete_option('mxchat_fallback_check_interval');
1227
1228 // NOTE: We do NOT delete queue tables on deactivation
1229 // This preserves data if user accidentally deactivates the plugin
1230 }
1231
1232 /**
1233 * Check if fallback rate limit cleanup is needed
1234 */
1235 function mxchat_check_fallback_rate_limits() {
1236 $use_fallback = get_option('mxchat_use_fallback_rate_limits', false);
1237
1238 if (!$use_fallback) {
1239 return;
1240 }
1241
1242 $next_check = get_option('mxchat_next_rate_limit_check', 0);
1243
1244 if (time() >= $next_check) {
1245 // Only run reset if the MxChat_Integrator class exists
1246 if (class_exists('MxChat_Integrator')) {
1247 $integrator = new MxChat_Integrator();
1248 if (method_exists($integrator, 'mxchat_reset_rate_limits')) {
1249 $integrator->mxchat_reset_rate_limits();
1250 update_option('mxchat_next_rate_limit_check', time() + 3600);
1251 }
1252 }
1253 }
1254 }
1255
1256 /**
1257 * Robust update checking with role restriction migration, model deprecation, and queue tables
1258 * CRITICAL: This runs on EVERY page load to ensure tables exist
1259 */
1260 function mxchat_check_for_update() {
1261 global $wpdb;
1262
1263 try {
1264 $current_version = get_option('mxchat_plugin_version', '0.0.0');
1265 $plugin_version = MXCHAT_VERSION;
1266
1267 // Always ensure critical tables exist (even if version matches)
1268 // This handles manual table deletion or fresh installs
1269 $chat_table = $wpdb->prefix . 'mxchat_chat_transcripts';
1270 $queue_table = $wpdb->prefix . 'mxchat_processing_queue';
1271
1272 $chat_exists = $wpdb->get_var("SHOW TABLES LIKE '$chat_table'") === $chat_table;
1273 $queue_exists = $wpdb->get_var("SHOW TABLES LIKE '$queue_table'") === $queue_table;
1274
1275 if (!$chat_exists || !$queue_exists) {
1276 //error_log("MxChat: Critical tables missing, running activation");
1277 mxchat_activate();
1278 }
1279
1280 // Version-specific migrations
1281 if ($current_version !== $plugin_version) {
1282 //error_log("MxChat: Version change detected: $current_version -> $plugin_version");
1283
1284 // Run live agent update BEFORE updating the stored version
1285 mxchat_handle_live_agent_update();
1286
1287 // Run theme migration notice for 3.0.1 (AI theme CSS structure changes)
1288 mxchat_handle_theme_migration_notice();
1289
1290 // Run role restriction migration for 2.4.1
1291 if (version_compare($current_version, '2.4.1', '<')) {
1292 mxchat_add_role_restriction_column();
1293 }
1294
1295 // Run enabled_bots column migration for 2.4.4
1296 if (version_compare($current_version, '2.4.4', '<')) {
1297 mxchat_add_enabled_bots_column();
1298 }
1299
1300 // Run model migration for 2.5.1 (Claude deprecation)
1301 if (version_compare($current_version, '2.5.1', '<')) {
1302 mxchat_migrate_deprecated_models();
1303 }
1304
1305 // 2.5.2: Ensure queue tables exist and fix URL column sizes for all users upgrading to 2.5.2
1306 if (version_compare($current_version, '2.5.2', '<')) {
1307 mxchat_create_queue_tables();
1308 mxchat_fix_url_column_size(); // NEW: Fix URL column size for long URLs
1309 //error_log("MxChat: Queue tables created and URL columns updated for upgrade to 2.5.2");
1310 }
1311
1312 // 2.6.0: Ensure rag_context column exists for retrieved documents feature
1313 if (version_compare($current_version, '2.6.0', '<')) {
1314 $chat_table = $wpdb->prefix . 'mxchat_chat_transcripts';
1315 mxchat_ensure_all_columns($chat_table);
1316 //error_log("MxChat: rag_context column migration for 2.6.0");
1317 }
1318
1319 // 3.0.5: Migrate deprecated Gemini embedding model
1320 if (version_compare($current_version, '3.0.5', '<')) {
1321 mxchat_migrate_gemini_embedding_model();
1322 }
1323
1324 // 3.0.6: Migrate deprecated OpenAI and Claude models
1325 if (version_compare($current_version, '3.0.6', '<')) {
1326 mxchat_migrate_deprecated_models();
1327 }
1328
1329 // 3.1.2: Convert chat transcripts table to utf8mb4 for emoji support
1330 if (version_compare($current_version, '3.1.2', '<')) {
1331 mxchat_migrate_transcripts_charset();
1332 }
1333
1334 // 3.1.7: Clean up stale shared session email/name entries
1335 if (version_compare($current_version, '3.1.7', '<')) {
1336 delete_option('mxchat_email_null');
1337 delete_option('mxchat_name_null');
1338 }
1339
1340 // 3.2.4: Backfill active embedding model option for the warning UI
1341 // (replaces the per-row column tracking from 3.2.3, which was reverted)
1342 if (version_compare($current_version, '3.2.4', '<')) {
1343 mxchat_backfill_active_embedding_model();
1344 }
1345
1346 // Run full activation to ensure everything is up to date
1347 mxchat_activate();
1348
1349 // Run migration functions
1350 mxchat_migrate_live_agent_status();
1351
1352 // Add the cleanup function for version 2.1.8
1353 if (version_compare($current_version, '2.1.8', '<')) {
1354 $deleted = mxchat_cleanup_orphaned_chat_history();
1355 }
1356
1357 // Update version LAST
1358 update_option('mxchat_plugin_version', $plugin_version);
1359
1360 //error_log("MxChat: Updated from version $current_version to $plugin_version");
1361 }
1362
1363 } catch (Exception $e) {
1364 //error_log('MxChat update error: ' . $e->getMessage());
1365 // Don't update version if there was an error
1366 }
1367 }
1368
1369 /**
1370 * Ensure tables exist on every admin load for fresh installations
1371 * This is a safety net for cases where activation hook doesn't fire
1372 */
1373 function mxchat_ensure_tables_exist() {
1374 global $wpdb;
1375
1376 // Only run for admin users to avoid performance impact
1377 if (!current_user_can('administrator')) {
1378 return;
1379 }
1380
1381 // Check if we've already verified tables in this session
1382 static $tables_checked = false;
1383 if ($tables_checked) {
1384 return;
1385 }
1386 $tables_checked = true;
1387
1388 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1389 $queue_table = $wpdb->prefix . 'mxchat_processing_queue';
1390
1391 $chat_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
1392 $queue_exists = $wpdb->get_var("SHOW TABLES LIKE '$queue_table'") === $queue_table;
1393
1394 if (!$chat_exists || !$queue_exists) {
1395 //error_log("MxChat: Tables missing on admin load, running activation");
1396 mxchat_activate();
1397 }
1398 }
1399
1400 /**
1401 * Clean up orphaned chat history options from the wp_options table
1402 * @return int Number of options deleted
1403 */
1404 function mxchat_cleanup_orphaned_chat_history() {
1405 global $wpdb;
1406 $count = 0;
1407
1408 // Get all option keys that match our pattern
1409 $history_options = $wpdb->get_results(
1410 "SELECT option_name FROM {$wpdb->options}
1411 WHERE option_name LIKE 'mxchat_history_%'"
1412 );
1413
1414 if (!empty($history_options)) {
1415 foreach ($history_options as $option) {
1416 // Extract the session ID from the option name
1417 $session_id = str_replace('mxchat_history_', '', $option->option_name);
1418
1419 // Check if this session still exists in the custom table
1420 $table_name = $wpdb->prefix . 'mxchat_chat_transcripts';
1421 $exists = $wpdb->get_var(
1422 $wpdb->prepare(
1423 "SELECT COUNT(*) FROM {$table_name} WHERE session_id = %s",
1424 $session_id
1425 )
1426 );
1427
1428 // If session doesn't exist in the main table, delete the option
1429 if ($exists == 0) {
1430 delete_option($option->option_name);
1431 // Also delete related metadata
1432 delete_option("mxchat_email_{$session_id}");
1433 delete_option("mxchat_name_{$session_id}");
1434 delete_option("mxchat_agent_name_{$session_id}");
1435 $count++;
1436 }
1437 }
1438 }
1439
1440 return $count;
1441 }
1442
1443 function mxchat_migrate_live_agent_status() {
1444 $options = get_option('mxchat_options', []);
1445
1446 // Check if live_agent_status exists
1447 if (isset($options['live_agent_status'])) {
1448 $current_status = $options['live_agent_status'];
1449 $needs_update = false;
1450
1451 // Convert to new format if needed
1452 if ($current_status === 'online') {
1453 $options['live_agent_status'] = 'on';
1454 $needs_update = true;
1455 } else if ($current_status === 'offline') {
1456 $options['live_agent_status'] = 'off';
1457 $needs_update = true;
1458 } else if (!in_array($current_status, ['on', 'off'])) {
1459 // Default to off for any unexpected values
1460 $options['live_agent_status'] = 'off';
1461 $needs_update = true;
1462 }
1463
1464 // Only update if needed
1465 if ($needs_update) {
1466 update_option('mxchat_options', $options);
1467 }
1468 } else {
1469 // If status doesn't exist, set default to off
1470 $options['live_agent_status'] = 'off';
1471 update_option('mxchat_options', $options);
1472 }
1473 }
1474
1475 function mxchat_handle_live_agent_update() {
1476 // Get the CURRENT stored version (before it gets updated)
1477 $current_version = get_option('mxchat_plugin_version', '0.0.0');
1478 $new_version = '2.2.2';
1479
1480 // Only run this once for the update to 2.2.2
1481 $update_handled = get_option('mxchat_live_agent_update_2_2_2_handled', false);
1482
1483 // Check if we're upgrading TO 2.2.2 and haven't handled this yet
1484 if (version_compare($current_version, $new_version, '<') && !$update_handled) {
1485 $options = get_option('mxchat_options', array());
1486
1487 // Check if live agent was previously enabled
1488 if (isset($options['live_agent_status']) && $options['live_agent_status'] === 'on') {
1489 // Disable live agent
1490 $options['live_agent_status'] = 'off';
1491 update_option('mxchat_options', $options);
1492
1493 // Set flag to show the notification banner
1494 update_option('mxchat_show_live_agent_disabled_notice', true);
1495 }
1496
1497 // Mark this update as handled
1498 update_option('mxchat_live_agent_update_2_2_2_handled', true);
1499 }
1500 }
1501
1502 /**
1503 * Handle theme migration notice for version 3.0.1
1504 * Shows a dismissible notice to Pro users about migrating AI-generated themes
1505 */
1506 function mxchat_handle_theme_migration_notice() {
1507 // Get the CURRENT stored version (before it gets updated)
1508 $current_version = get_option('mxchat_plugin_version', '0.0.0');
1509 $target_version = '3.0.1';
1510
1511 // Only run this once for the update to 3.0.1
1512 $update_handled = get_option('mxchat_theme_migration_update_3_0_1_handled', false);
1513
1514 // Check if we're upgrading TO 3.0.1 and haven't handled this yet
1515 if (version_compare($current_version, $target_version, '<') && !$update_handled) {
1516 // Check if Pro is activated - only show to Pro users
1517 $license_status = get_option('mxchat_license_status', 'inactive');
1518 $is_pro = ($license_status === 'active');
1519
1520 if ($is_pro) {
1521 // Set flag to show the theme migration notification banner
1522 update_option('mxchat_show_theme_migration_notice', true);
1523 }
1524
1525 // Mark this update as handled (whether Pro or not)
1526 update_option('mxchat_theme_migration_update_3_0_1_handled', true);
1527 }
1528 }
1529
1530 // Initialize plugin safely
1531 function mxchat_init() {
1532 // Include all class files first
1533 mxchat_include_classes();
1534
1535 // Run update check (this also ensures tables exist)
1536 mxchat_check_for_update();
1537
1538 // CRITICAL: Ensure tables exist on admin pages (safety net)
1539 add_action('admin_init', 'mxchat_ensure_tables_exist', 1);
1540
1541 // Add fallback rate limit check
1542 add_action('init', 'mxchat_check_fallback_rate_limits', 5);
1543
1544 // Add migration notice hook
1545 add_action('admin_notices', 'mxchat_show_migration_notice');
1546
1547 // Initialize classes with error handling
1548 try {
1549 // Initialize admin classes
1550 if (is_admin()) {
1551 if (class_exists('MxChat_Knowledge_Manager')) {
1552 $mxchat_knowledge_manager = new MxChat_Knowledge_Manager();
1553
1554 if (class_exists('MxChat_Admin')) {
1555 $mxchat_admin = new MxChat_Admin($mxchat_knowledge_manager);
1556 }
1557 }
1558
1559 // Initialize meta box class
1560 if (class_exists('MxChat_Meta_Box')) {
1561 new MxChat_Meta_Box();
1562 }
1563
1564 }
1565
1566 // Initialize content generator globally — it registers wp_head hook
1567 // for frontend CSS injection, plus wp_ajax_ hooks for admin.
1568 if (class_exists('MxChat_Content_Generator')) {
1569 new MxChat_Content_Generator();
1570 }
1571
1572 // Initialize cache purge globally — settings writes can happen on any
1573 // request type (admin screens, admin-ajax autosave, wp-cli), and the
1574 // deferred-purge cron event fires on front-end requests.
1575 if (class_exists('MxChat_Cache_Purge')) {
1576 MxChat_Cache_Purge::init();
1577 }
1578
1579 // Initialize REST API globally — endpoints must be registered on
1580 // every request (admin and frontend) so they're reachable via /wp-json/.
1581 // Endpoints are auth-gated and locked until the site owner generates
1582 // a token in MxChat → API Access.
1583 if (class_exists('MxChat_Rest_Api')) {
1584 new MxChat_Rest_Api();
1585 }
1586
1587 // Initialize public classes
1588 if (class_exists('MxChat_Public')) {
1589 $mxchat_public = new MxChat_Public();
1590 }
1591
1592 if (class_exists('MxChat_Integrator')) {
1593 global $mxchat_integrator;
1594 $mxchat_integrator = new MxChat_Integrator();
1595 }
1596
1597 } catch (Exception $e) {
1598 //error_log('MxChat initialization error: ' . $e->getMessage());
1599
1600 // Show admin notice if there's an error
1601 if (is_admin()) {
1602 add_action('admin_notices', function() use ($e) {
1603 echo '<div class="notice notice-error"><p>';
1604 echo '<strong>MxChat Error:</strong> Plugin initialization failed. ';
1605 echo 'Please check error logs or contact support. Error: ' . esc_html($e->getMessage());
1606 echo '</p></div>';
1607 });
1608 }
1609 }
1610 }
1611
1612 // Run initialization on plugins_loaded
1613 add_action('plugins_loaded', 'mxchat_init');
1614
1615 // Run migration check on admin init (for auto-updates without reactivation)
1616 add_action('admin_init', 'mxchat_check_and_run_migrations');
1617
1618 /**
1619 * Check and run migrations on admin init
1620 * This ensures migrations run even when plugin is auto-updated
1621 */
1622 function mxchat_check_and_run_migrations() {
1623 // Only run in admin and not on every request
1624 static $checked = false;
1625 if ($checked) {
1626 return;
1627 }
1628 $checked = true;
1629
1630 mxchat_migrate_pinecone_roles_add_bot_id();
1631 mxchat_migrate_add_content_type_column();
1632 mxchat_migrate_add_translations_table();
1633 mxchat_migrate_add_session_ratings_table();
1634 }
1635
1636 /**
1637 * Migration: Create per-session satisfaction ratings table (v3.2.6)
1638 * For users upgrading from versions before 3.2.6
1639 */
1640 function mxchat_migrate_add_session_ratings_table() {
1641 $migration_key = 'mxchat_session_ratings_table_created';
1642 if (get_option($migration_key)) {
1643 return;
1644 }
1645 mxchat_create_session_ratings_table();
1646 update_option($migration_key, '3.2.6');
1647 }
1648
1649 /**
1650 * Migration: Create transcript translations table (v3.0.4)
1651 * For users upgrading from versions before 3.0.4
1652 */
1653 function mxchat_migrate_add_translations_table() {
1654 $migration_key = 'mxchat_translations_table_created';
1655
1656 // Check if migration already ran
1657 if (get_option($migration_key)) {
1658 return;
1659 }
1660
1661 // Create the translations table
1662 mxchat_create_translations_table();
1663
1664 // Mark migration as complete
1665 update_option($migration_key, '3.0.4');
1666 }
1667
1668 /**
1669 * Migration: Update deprecated Gemini embedding model (v3.0.5)
1670 * Updates gemini-embedding-exp-03-07 to gemini-embedding-001 for users who had it selected
1671 */
1672 function mxchat_migrate_gemini_embedding_model() {
1673 $options = get_option('mxchat_options', array());
1674
1675 if (isset($options['embedding_model']) && $options['embedding_model'] === 'gemini-embedding-exp-03-07') {
1676 $options['embedding_model'] = 'gemini-embedding-001';
1677 update_option('mxchat_options', $options);
1678 }
1679 }
1680
1681 // Register activation hook
1682 register_activation_hook(__FILE__, 'mxchat_activate');
1683
1684 // Add cron schedule
1685 add_filter('cron_schedules', function($schedules) {
1686 $schedules['one_minute'] = array(
1687 'interval' => 60,
1688 'display' => 'Every Minute'
1689 );
1690 return $schedules;
1691 });
1692
1693 // Register deactivation hook
1694 register_deactivation_hook(__FILE__, 'mxchat_deactivate');
1695
1696 /**
1697 * Per-session satisfaction rating: AJAX save handler (v3.2.6).
1698 * Records one 👍/👎 + optional feedback per chat session. The UNIQUE KEY on
1699 * session_id makes this naturally idempotent — only the first rating per
1700 * session is stored; duplicate POSTs are silent no-ops.
1701 */
1702 function mxchat_save_session_rating() {
1703 global $wpdb;
1704
1705 $session_id = isset($_POST['session_id']) ? sanitize_text_field(wp_unslash($_POST['session_id'])) : '';
1706 $bot_id = isset($_POST['bot_id']) ? sanitize_text_field(wp_unslash($_POST['bot_id'])) : 'default';
1707 $rating_raw = isset($_POST['rating']) ? (int) $_POST['rating'] : 0;
1708 $feedback = isset($_POST['feedback']) ? sanitize_textarea_field(wp_unslash($_POST['feedback'])) : '';
1709
1710 if ($session_id === '' || ($rating_raw !== 1 && $rating_raw !== -1)) {
1711 wp_send_json_error(array('message' => 'invalid_input'), 400);
1712 }
1713
1714 if (strlen($feedback) > 1000) {
1715 $feedback = substr($feedback, 0, 1000);
1716 }
1717
1718 $table_name = $wpdb->prefix . 'mxchat_session_ratings';
1719 $existing = $wpdb->get_var($wpdb->prepare(
1720 "SELECT id FROM $table_name WHERE session_id = %s LIMIT 1",
1721 $session_id
1722 ));
1723
1724 if ($existing) {
1725 if ($feedback !== '') {
1726 $wpdb->update(
1727 $table_name,
1728 array('rating_feedback' => $feedback),
1729 array('id' => (int) $existing),
1730 array('%s'),
1731 array('%d')
1732 );
1733 }
1734 wp_send_json_success(array('updated' => true));
1735 }
1736
1737 $inserted = $wpdb->insert(
1738 $table_name,
1739 array(
1740 'session_id' => $session_id,
1741 'bot_id' => $bot_id !== '' ? $bot_id : 'default',
1742 'rating_value' => $rating_raw,
1743 'rating_feedback' => $feedback !== '' ? $feedback : null,
1744 'created_at' => current_time('mysql'),
1745 ),
1746 array('%s', '%s', '%d', '%s', '%s')
1747 );
1748
1749 if ($inserted === false) {
1750 wp_send_json_error(array('message' => 'db_insert_failed'), 500);
1751 }
1752
1753 wp_send_json_success(array('saved' => true));
1754 }
1755 add_action('wp_ajax_mxchat_save_rating', 'mxchat_save_session_rating');
1756 add_action('wp_ajax_nopriv_mxchat_save_rating', 'mxchat_save_session_rating');