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

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