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

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