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

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