PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.2.10
MxChat – AI Chatbot & Content Generation for WordPress v3.2.10
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.10, at mxchat-basic.php

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