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

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