PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.2.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.2.2
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / modules / chatbot.php
ai-engine / classes / modules Last commit date
advisor.php 7 months ago chatbot.php 7 months ago discussions.php 8 months ago files.php 8 months ago forms-manager.php 10 months ago gdpr.php 11 months ago search.php 1 year ago security.php 1 year ago tasks-examples.php 9 months ago tasks.php 7 months ago wand.php 7 months ago
chatbot.php
1319 lines
1 <?php
2
3 // Params for the chatbot (front and server)
4 define( 'MWAI_CHATBOT_FRONT_PARAMS', [ 'id', 'customId', 'aiName', 'userName', 'guestName', 'aiAvatar', 'userAvatar', 'guestAvatar', 'aiAvatarUrl', 'userAvatarUrl', 'guestAvatarUrl', 'textSend', 'textClear', 'imageUpload', 'fileUpload', 'multiUpload', 'maxUploads', 'fileUploads', 'fileSearch', 'mode', 'textInputPlaceholder', 'textInputMaxLength', 'textCompliance', 'startSentence', 'localMemory', 'themeId', 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', 'centerOpen', 'width', 'openDelay', 'iconBubble', 'windowAnimation', 'fullscreen', 'copyButton', 'headerSubtitle', 'popupTitle', 'containerType', 'headerType', 'messagesType', 'inputType', 'footerType', 'talkMode' ] );
5
6 define( 'MWAI_CHATBOT_SERVER_PARAMS', [ 'id', 'envId', 'scope', 'mode', 'contentAware', 'context', 'startSentence', 'embeddingsEnvId', 'embeddingsIndex', 'embeddingsNamespace', 'assistantId', 'instructions', 'resolution', 'voice', 'talkMode', 'model', 'temperature', 'maxTokens', 'contextMaxLength', 'maxResults', 'apiKey', 'functions', 'mcpServers', 'tools', 'historyStrategy', 'previousResponseId', 'parentBotId', 'crossSite', 'promptId', 'promptVariables', 'reasoningEffort', 'verbosity' ] );
7
8 // Params for the discussions (front and server)
9 define( 'MWAI_DISCUSSIONS_FRONT_PARAMS', [ 'themeId', 'textNewChat' ] );
10 define( 'MWAI_DISCUSSIONS_SERVER_PARAMS', [ 'customId' ] );
11
12 class Meow_MWAI_Modules_Chatbot {
13 private $core = null;
14 private $namespace = 'mwai-ui/v1';
15 private $siteWideChatId = null;
16
17 public function __construct() {
18 global $mwai_core;
19 $this->core = $mwai_core;
20 $this->siteWideChatId = $this->core->get_option( 'botId' );
21
22 add_shortcode( 'mwai_chatbot', [ $this, 'chat_shortcode' ] );
23 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
24 add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] );
25 add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ] );
26 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
27 add_shortcode( 'mwai_discussions', [ $this, 'chatbot_discussions' ] );
28 }
29 }
30
31 public function register_scripts() {
32 // Load JS
33 $physical_file = trailingslashit( MWAI_PATH ) . 'app/chatbot.js';
34 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
35 wp_register_script( 'mwai_chatbot', trailingslashit( MWAI_URL )
36 . 'app/chatbot.js', [ 'wp-element' ], $cache_buster, false );
37
38 // Actual loading of the scripts
39 $hasSiteWideChat = $this->siteWideChatId && $this->siteWideChatId !== 'none';
40
41 // Don't load chatbot scripts on the Site Editor to avoid conflicts
42 $current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
43 $is_site_editor = $current_screen && $current_screen->base === 'site-editor';
44
45 if ( ( is_admin() && !$is_site_editor ) || $hasSiteWideChat ) {
46 $themeId = null;
47 if ( $hasSiteWideChat ) {
48 $bot = $this->core->get_chatbot( $this->siteWideChatId );
49 if ( $bot && isset( $bot['themeId'] ) ) {
50 $themeId = $bot['themeId'];
51 }
52 }
53 $this->enqueue_scripts( is_admin() ? null : $themeId );
54 if ( $hasSiteWideChat ) {
55 // Chatbot Injection
56 add_action( 'wp_footer', [ $this, 'inject_chat' ] );
57 }
58 }
59 }
60
61 public function enqueue_scripts( $themeId = null ) {
62 wp_enqueue_script( 'mwai_chatbot' );
63 if ( $this->core->get_option( 'syntax_highlight' ) ) {
64 wp_enqueue_script( 'mwai_highlight' );
65 }
66 if ( $themeId ) {
67 $this->core->enqueue_theme( $themeId );
68 }
69 else {
70 $this->core->enqueue_themes();
71 }
72 }
73
74 /**
75 * Helper method to create REST responses with automatic token refresh
76 *
77 * @param array $data The response data
78 * @param int $status HTTP status code
79 * @return WP_REST_Response
80 */
81 protected function create_rest_response( $data, $status = 200 ) {
82 // Always check if we need to provide a new nonce
83 $current_nonce = $this->core->get_nonce( true );
84 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
85
86 // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
87 // We'll refresh if the nonce is older than 10 hours to be safe
88 $should_refresh = false;
89
90 if ( $request_nonce ) {
91 // Try to determine the age of the nonce
92 // WordPress uses a tick system where each tick is 12 hours
93 // If we're in the second half of the nonce's life, refresh it
94 $time = time();
95 $nonce_tick = wp_nonce_tick();
96
97 // Verify if the nonce is still valid but getting old
98 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
99 if ( $verify === 2 ) {
100 // Nonce is valid but was generated 12-24 hours ago
101 $should_refresh = true;
102 // Log will be written when token is included in response
103 }
104 }
105
106 // If the nonce has changed or should be refreshed, include the new one
107 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
108 $data['new_token'] = $current_nonce;
109
110 // Log if server debug mode is enabled
111 if ( $this->core->get_option( 'server_debug_mode' ) ) {
112 error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
113 }
114 }
115
116 return new WP_REST_Response( $data, $status );
117 }
118
119 public function rest_api_init() {
120 register_rest_route( $this->namespace, '/chats/submit', [
121 'methods' => 'POST',
122 'callback' => [ $this, 'rest_chat' ],
123 'permission_callback' => [ $this->core, 'check_rest_nonce' ]
124 ] );
125 }
126
127 public function basics_security_check( $botId, $customId, $newMessage, $newFileId ) {
128 if ( !$botId && !$customId ) {
129 Meow_MWAI_Logging::warn( 'The query was rejected - no botId nor id was specified.' );
130 return false;
131 }
132
133 if ( $newFileId ) {
134 return true;
135 }
136
137 // Handle null or convert to string for strlen
138 $messageStr = $newMessage === null ? '' : (string)$newMessage;
139 $length = strlen( $messageStr );
140 if ( $length < 1 ) {
141 Meow_MWAI_Logging::warn( 'The query was rejected - message was too short.' );
142 return false;
143 }
144 return true;
145 }
146
147 public function build_final_res( $botId, $newMessage, $newFileId, $params, $reply, $images, $actions, $usage, $responseId = null ) {
148 $filterParams = [
149 'step' => 'reply',
150 'botId' => $botId,
151 'reply' => $reply,
152 'images' => $images,
153 'newMessage' => $newMessage,
154 'newFileId' => $newFileId,
155 'params' => $params,
156 'usage' => $usage,
157 'messages' => $params['messages'] ?? [],
158 'isNewConversation' => empty( $params['messages'] ) || count( $params['messages'] ) <= 1,
159 ];
160 $actions = apply_filters( 'mwai_chatbot_actions', $actions, $filterParams );
161 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
162 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
163 $actions = $this->sanitize_actions( $actions );
164 $blocks = $this->sanitize_blocks( $blocks );
165 $shortcuts = $this->sanitize_shortcuts( $shortcuts );
166 $result = [
167 'success' => true,
168 'reply' => $reply,
169 'images' => $images,
170 'actions' => $actions,
171 'shortcuts' => $shortcuts,
172 'blocks' => $blocks,
173 'usage' => $usage
174 ];
175
176 // Add response ID if available
177 if ( !empty( $responseId ) ) {
178 $result['responseId'] = $responseId;
179 }
180
181 // Check if token needs refresh
182 $current_nonce = $this->core->get_nonce( true );
183 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
184
185 $should_refresh = false;
186 if ( $request_nonce ) {
187 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
188 if ( $verify === 2 ) {
189 // Nonce is valid but was generated 12-24 hours ago
190 $should_refresh = true;
191 }
192 }
193
194 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
195 $result['new_token'] = $current_nonce;
196 }
197
198 return $result;
199 }
200
201 public function rest_chat( $request ) {
202 $params = $request->get_json_params();
203 $botId = $params['botId'] ?? null;
204 $customId = $params['customId'] ?? null;
205 $stream = $params['stream'] ?? false;
206 $newMessage = trim( $params['newMessage'] ?? '' );
207 $newFileId = $params['newFileId'] ?? null;
208 $newFileIds = $params['newFileIds'] ?? [];
209 $crossSite = $params['crossSite'] ?? false;
210
211 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId ) ) {
212 return $this->create_rest_response( [
213 'success' => false,
214 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
215 ], 403 );
216 }
217
218 try {
219 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream, $newFileIds );
220 $final_res = $this->build_final_res(
221 $botId,
222 $newMessage,
223 $newFileId,
224 $params,
225 $data['reply'],
226 $data['images'],
227 $data['actions'],
228 $data['usage'],
229 $data['responseId'] ?? null
230 );
231 return $this->create_rest_response( $final_res, 200 );
232 }
233 catch ( Exception $e ) {
234 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
235
236 // If we're in streaming mode, send the error through the stream
237 if ( $stream ) {
238 // Log the error
239 error_log( '[AI Engine Chatbot Error] ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
240
241 // Send error event through stream
242 $errorData = [
243 'type' => 'error',
244 'data' => 'Oops! Something went wrong on the server. Please try again, and if you are the site developer, check the PHP Error Logs for details.'
245 ];
246 echo 'data: ' . json_encode( $errorData ) . "\n\n";
247 if ( ob_get_level() > 0 ) {
248 ob_end_flush();
249 }
250 flush();
251 die();
252 }
253
254 // For non-streaming, return normal error response
255 return $this->create_rest_response( [
256 'success' => false,
257 'message' => $message
258 ], 500 );
259 }
260 }
261
262 private function sanitize_items( $items, $supported_types, $type_name ) {
263 if ( empty( $items ) ) {
264 return $items;
265 }
266 $sanitized_items = [];
267 foreach ( $items as $item ) {
268 if ( isset( $supported_types[$item['type']] ) ) {
269 $is_valid = true;
270 foreach ( $supported_types[$item['type']] as $param ) {
271 if ( !isset( $item['data'][$param] ) ) {
272 $is_valid = false;
273 Meow_MWAI_Logging::warn( "The query was rejected - missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
274 break;
275 }
276 }
277 if ( $is_valid ) {
278 $sanitized_items[] = $item;
279 }
280 }
281 else {
282 Meow_MWAI_Logging::warn( "The query was rejected - unsupported {$type_name} type: {$item['type']}." );
283 }
284 }
285 return $sanitized_items;
286 }
287
288 public function sanitize_actions( $actions ) {
289 $supported_action_types = [
290 'function' => ['name', 'args'],
291 'javascript' => ['snippet'],
292 ];
293 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
294 }
295
296 public function sanitize_blocks( $blocks ) {
297 $supported_block_types = [
298 'content' => ['html'],
299 ];
300 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
301 }
302
303 public function sanitize_shortcuts( $shortcuts ) {
304 $supported_shortcut_types = [
305 'message' => ['label', 'message'],
306 'action' => ['label', 'message', 'action'],
307 'callback' => ['label', 'onClick'],
308 ];
309 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
310 }
311
312 #region Messages Integrity Check
313
314 public function messages_integrity_diff( $messages1, $messages2 ) {
315 // Ensure both parameters are arrays
316 if ( !is_array( $messages1 ) ) {
317 $messages1 = [];
318 }
319 if ( !is_array( $messages2 ) ) {
320 $messages2 = [];
321 }
322
323 // Collect messages with role not 'user' from messages1
324 $messagesList1 = [];
325 foreach ( $messages1 as $msg ) {
326 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
327 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
328 if ( $role && $role != 'user' ) {
329 $messageData = [ 'role' => $role, 'content' => $content ];
330 $messagesList1[] = $messageData;
331 }
332 }
333
334 // Collect messages with role not 'user' from messages2
335 $messagesList2 = [];
336 foreach ( $messages2 as $msg ) {
337 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
338 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
339 if ( $role && $role != 'user' ) {
340 $messageData = [ 'role' => $role, 'content' => $content ];
341 $messagesList2[] = $messageData;
342 }
343 }
344
345 // Count occurrences of each message in messagesList1
346 $counts1 = [];
347 foreach ( $messagesList1 as $msg ) {
348 $key = serialize( $msg );
349 if ( isset( $counts1[ $key ] ) ) {
350 $counts1[ $key ]++;
351 }
352 else {
353 $counts1[ $key ] = 1;
354 }
355 }
356
357 // Count occurrences of each message in messagesList2
358 $counts2 = [];
359 foreach ( $messagesList2 as $msg ) {
360 $key = serialize( $msg );
361 if ( isset( $counts2[ $key ] ) ) {
362 $counts2[ $key ]++;
363 }
364 else {
365 $counts2[ $key ] = 1;
366 }
367 }
368
369 // Compare counts to find unmatched messages
370 $all_keys = array_unique( array_merge( array_keys( $counts1 ), array_keys( $counts2 ) ) );
371
372 $diffs = [];
373 foreach ( $all_keys as $key ) {
374 $count1 = isset( $counts1[ $key ] ) ? $counts1[ $key ] : 0;
375 $count2 = isset( $counts2[ $key ] ) ? $counts2[ $key ] : 0;
376 if ( $count1 != $count2 ) {
377 $message = unserialize( $key );
378 $diffs[] = [
379 'message' => $message,
380 'count_in_messages1' => $count1,
381 'count_in_messages2' => $count2
382 ];
383 }
384 }
385
386 return $diffs;
387 }
388
389 private function calculate_messages_checksum( $messages ) {
390 $messages_to_hash = [];
391 foreach ( $messages as $msg ) {
392 $role = is_array( $msg ) ? ( $msg['role'] ?? '' ) : ( is_object( $msg ) ? ( $msg->role ?? '' ) : '' );
393 $content = is_array( $msg ) ? ( $msg['content'] ?? '' ) : ( is_object( $msg ) ? ( $msg->content ?? '' ) : '' );
394 if ( in_array( $role, ['assistant', 'system'] ) ) {
395 $messages_to_hash[] = [ 'role' => $role, 'content' => $content ];
396 }
397 }
398 return md5( json_encode( $messages_to_hash ) );
399 }
400
401 #endregion
402
403 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false, $newFileIds = [] ) {
404 $query = null; // Initialize query variable to avoid undefined variable errors
405 try {
406 $chatbot = null;
407 $customId = $params['customId'] ?? null;
408
409 // Custom Chatbot
410 if ( $customId ) {
411 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
412 }
413 // Registered Chatbot
414 if ( !$chatbot && $botId ) {
415 $chatbot = $this->core->get_chatbot( $botId );
416 }
417 // Fall back to default chatbot if no chatbot found yet
418 if ( !$chatbot ) {
419 $chatbot = $this->core->get_chatbot( 'default' );
420 }
421
422 if ( !$chatbot ) {
423 Meow_MWAI_Logging::warn( 'The query was rejected - no chatbot was found.' );
424 throw new Exception( 'Sorry, your query has been rejected.' );
425 }
426
427 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
428 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int) $textInputMaxLength ) {
429 Meow_MWAI_Logging::warn( 'The query was rejected - message was too long.' );
430 throw new Exception( 'Sorry, your query has been rejected.' );
431 }
432
433 // We need to check the integrity of the messages sent by the client.
434 // This is important to ensure that the messages are not tampered with.
435
436 // Messages Integrity Check with Checksums
437 $chatId = $params['chatId'] ?? 'default';
438 $checksum_key = 'mwai_chatbot_checksum_' . $chatId;
439 $stored_checksum = get_transient( $checksum_key );
440 $client_messages = $params['messages'] ?? [];
441 $client_checksum = $this->calculate_messages_checksum( $client_messages );
442 if ( $stored_checksum && $stored_checksum !== $client_checksum ) {
443 Meow_MWAI_Logging::warn( 'Integrity Check: Messages integrity check failed. Assistant or system messages sent by the client do not match stored messages. Please enable the Discussions module for better logs.' );
444 }
445
446 // Messages Integrity Check with Discussions
447 if ( $this->core->get_option( 'chatbot_discussions' ) && $this->core->discussions && isset( $params['chatId'] ) ) {
448 $discussion = $this->core->discussions->get_discussion( $botId ? $botId : $customId, $params['chatId'] );
449 if ( $discussion ) {
450 $messages = $discussion['messages'];
451 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
452 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
453 if ( count( $diffs ) > 0 ) {
454 Meow_MWAI_Logging::warn( "Integrity Check: It seems the messages in the discussion #{$discussion['id']} do not match the ones sent by the client." );
455 }
456
457 // Maintain conversation state for Responses API by loading previousResponseId
458 // This enables stateful conversations where only new messages are sent
459 if ( empty( $params['previousResponseId'] ) && !empty( $discussion['extra'] ) ) {
460 $extra = json_decode( $discussion['extra'], true );
461 if ( !empty( $extra['responseId'] ) ) {
462 // Response IDs expire after 30 days per OpenAI's policy
463 // Check if the stored response is still valid
464 $responseDate = !empty( $extra['responseDate'] ) ? strtotime( $extra['responseDate'] ) : 0;
465 $thirtyDaysAgo = time() - ( 30 * 24 * 60 * 60 );
466
467 if ( $responseDate > $thirtyDaysAgo ) {
468 // Use the stored response ID for stateful conversation
469 $params['previousResponseId'] = $extra['responseId'];
470 }
471 }
472 }
473 }
474 else {
475 // No discussion yet? We still need to check the startSentence.
476 $startSentence = isset( $chatbot['startSentence'] ) ? $chatbot['startSentence'] : null;
477 $messages = [];
478 if ( !empty( $startSentence ) ) {
479 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
480 }
481 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
482 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
483 if ( count( $diffs ) > 0 ) {
484 Meow_MWAI_Logging::warn( 'Integrity Check: It seems the messages in the discussion do not match the ones sent by the client: ' . json_encode( $diffs ) );
485 }
486 }
487 }
488
489 // Create QueryText
490 $context = null;
491 $streamCallback = null;
492 $mode = $chatbot['mode'] ?? 'chat';
493
494 if ( $mode === 'images' ) {
495 // Check for uploaded files
496 $fileForImage = null;
497 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
498 $fileForImage = $newFileIds[0];
499 }
500 elseif ( !empty( $newFileId ) ) {
501 $fileForImage = $newFileId;
502 }
503
504 // If there's an uploaded file, use EditImage query instead
505 if ( !empty( $fileForImage ) ) {
506 $query = new Meow_MWAI_Query_EditImage( $newMessage );
507
508 // Handle the uploaded image
509 $url = $this->core->files->get_url( $fileForImage );
510 $mimeType = $this->core->files->get_mime_type( $fileForImage );
511 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
512
513 if ( $isIMG ) {
514 $query->add_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
515 $fileId = $this->core->files->get_id_from_refId( $fileForImage );
516 $this->core->files->update_purpose( $fileId, 'vision' );
517 }
518 }
519 else {
520 $query = new Meow_MWAI_Query_Image( $newMessage );
521 }
522
523 // Handle Params
524 $newParams = [];
525 foreach ( $chatbot as $key => $value ) {
526 $newParams[$key] = $value;
527 }
528 if ( is_array( $params ) ) {
529 foreach ( $params as $key => $value ) {
530 $newParams[$key] = $value;
531 }
532 }
533
534 // Map 'environment' field to 'envId' for compatibility
535 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
536 $newParams['envId'] = $newParams['environment'];
537 }
538
539 $params = apply_filters( 'mwai_chatbot_params', $newParams );
540 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
541
542 // Debug log for embeddings
543 if ( !empty( $params['embeddingsEnvId'] ) ) {
544 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
545 }
546 else {
547 // Log all params to debug
548 $paramKeys = array_keys( $params );
549 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
550 }
551
552 $query->inject_params( $params );
553 }
554 else {
555 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
556 new Meow_MWAI_Query_Text( $newMessage, 4096 );
557
558 // Handle Params
559 $newParams = [];
560 foreach ( $chatbot as $key => $value ) {
561 $newParams[$key] = $value;
562 }
563 if ( is_array( $params ) ) {
564 foreach ( $params as $key => $value ) {
565 $newParams[$key] = $value;
566 }
567 }
568
569 // Map 'environment' field to 'envId' for compatibility
570 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
571 $newParams['envId'] = $newParams['environment'];
572 }
573
574 $params = apply_filters( 'mwai_chatbot_params', $newParams );
575 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
576
577 // Debug log for embeddings
578 if ( !empty( $params['embeddingsEnvId'] ) ) {
579 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
580 }
581 else {
582 // Log all params to debug
583 $paramKeys = array_keys( $params );
584 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
585 }
586
587 // In Prompt mode, clear out features that are not supported before injecting params
588 if ( $mode === 'prompt' ) {
589 // Clear embeddings/context settings
590 unset( $params['embeddingsEnvId'] );
591 unset( $params['embeddingsIndex'] );
592 unset( $params['embeddingsNamespace'] );
593 unset( $params['contentAware'] );
594 unset( $params['context'] );
595
596 // Clear function calling and MCP servers
597 unset( $params['functions'] );
598 unset( $params['mcpServers'] );
599
600 // Clear tools
601 unset( $params['tools'] );
602
603 // Clear temperature, reasoning, verbosity as they're configured in the prompt
604 unset( $params['temperature'] );
605 unset( $params['reasoningEffort'] );
606 unset( $params['verbosity'] );
607 unset( $params['maxTokens'] );
608 }
609
610 $query->inject_params( $params );
611
612 // Handle Prompt mode specifics
613 if ( $mode === 'prompt' && !empty( $params['promptId'] ) ) {
614 $promptData = [
615 'id' => $params['promptId']
616 ];
617
618 // TODO: Prompt Variables support - might be added later
619 // Add prompt version if provided
620 // if ( !empty( $params['promptVersion'] ) ) {
621 // $promptData['version'] = $params['promptVersion'];
622 // }
623
624 // Add prompt variables if provided
625 // if ( !empty( $params['promptVariables'] ) ) {
626 // try {
627 // $variables = is_string( $params['promptVariables'] ) ?
628 // json_decode( $params['promptVariables'], true ) :
629 // $params['promptVariables'];
630 // if ( $variables ) {
631 // $promptData['variables'] = $variables;
632 // }
633 // } catch ( Exception $e ) {
634 // // Invalid JSON, skip variables
635 // }
636 // }
637
638 $query->setExtraParam( 'prompt', $promptData );
639 }
640
641 $storeId = null;
642 if ( $mode === 'assistant' ) {
643 $chatId = $params['chatId'] ?? null;
644 if ( !empty( $chatId ) && $this->core->discussions ) {
645 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
646 if ( isset( $discussion['storeId'] ) ) {
647 $storeId = $discussion['storeId'];
648 $query->setStoreId( $storeId );
649 }
650 }
651 }
652
653 // Support for Multiple Uploaded Files
654 $filesToProcess = [];
655 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
656 $filesToProcess = $newFileIds;
657 }
658 elseif ( !empty( $newFileId ) ) {
659 $filesToProcess[] = $newFileId;
660 }
661
662 // Support for Uploaded Image/Files
663 if ( !empty( $filesToProcess ) ) {
664 // Process all files for multi-upload support
665 foreach ( $filesToProcess as $fileToProcess ) {
666 // Get extension and mime type
667 $isImage = $this->core->files->is_image( $fileToProcess );
668
669 if ( $mode === 'assistant' && !$isImage ) {
670 // DEPRECATED: Assistants API and File Search are deprecated
671 // After August 26, 2026, this entire block should be removed
672 error_log( '[AI Engine] WARNING: Assistant File Search is deprecated and will be removed after August 26, 2026. Consider using regular chat with PDF uploads instead.' );
673
674 $url = $this->core->files->get_path( $fileToProcess );
675 $data = $this->core->files->get_data( $fileToProcess );
676 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
677 $filename = basename( $url );
678
679 // Upload the file
680 $file = $openai->upload_file( $filename, $data, 'assistants' );
681
682 // Create a store
683 if ( empty( $storeId ) ) {
684 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
685 if ( !empty( $query->chatId ) ) {
686 $chatbotName .= '_' . $query->chatId;
687 }
688 $metadata = [];
689 if ( !empty( $chatbot['assistantId'] ) ) {
690 $metadata['assistantId'] = $chatbot['assistantId'];
691 }
692 if ( !empty( $query->chatId ) ) {
693 $metadata['chatId'] = $query->chatId;
694 }
695 $expiry = $this->core->get_option( 'image_expires' );
696 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
697 $query->setStoreId( $storeId );
698 }
699
700 // Add the file to the store - wait a moment for store to be ready
701 sleep( 1 );
702 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
703
704 if ( empty( $storeFileId ) ) {
705 throw new Exception( 'Failed to add file to vector store.' );
706 }
707
708 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
709 $openAiRefId = $file['id'];
710 $internalFileId = $this->core->files->get_id_from_refId( $fileToProcess );
711 $this->core->files->update_refId( $internalFileId, $openAiRefId );
712 $this->core->files->update_envId( $internalFileId, $query->envId );
713 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
714 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
715 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
716 $fileToProcess = $openAiRefId;
717 $scope = $params['fileSearch'];
718 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
719 $id = $this->core->files->get_id_from_refId( $fileToProcess );
720 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
721 }
722 }
723 else {
724 // Keep track of the internal file ID (before any OpenAI processing)
725 // Important: $fileToProcess is our internal database refId, not OpenAI's file_id
726 $internalRefId = $fileToProcess;
727 $url = $this->core->files->get_url( $internalRefId );
728 $mimeType = $this->core->files->get_mime_type( $internalRefId );
729 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
730 $purposeType = $isIMG ? 'vision' : 'files';
731
732 // Create DroppedFile object - provider-agnostic approach
733 // Images use URL (can be sent as base64 or URL in messages)
734 // PDFs use refId (engines will upload to their Files API as needed)
735 if ( $isIMG ) {
736 $droppedFile = Meow_MWAI_Query_DroppedFile::from_url( $url, $purposeType, $mimeType );
737 } else {
738 // For PDFs and documents, use refId so engines can access file data directly
739 $droppedFile = Meow_MWAI_Query_DroppedFile::from_refId( $internalRefId, $purposeType, $mimeType );
740 }
741
742 // IMPORTANT: Always use add_file() to add to attachedFiles array
743 // This is the unified approach for both single and multi-file uploads
744 // Engines will check attachedFiles array first, then fall back to attachedFile (legacy)
745 $query->add_file( $droppedFile );
746
747 // Update metadata using the internal refId (not OpenAI file ID)
748 $fileId = $this->core->files->get_id_from_refId( $internalRefId );
749 $this->core->files->update_envId( $fileId, $query->envId );
750 $this->core->files->update_purpose( $fileId, $purposeType );
751 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
752 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
753 }
754 }
755 }
756
757 // Takeover
758 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
759 if ( !empty( $takeoverAnswer ) ) {
760 $rawText = apply_filters( 'mwai_chatbot_reply', $takeoverAnswer, $query, $params, [] );
761 return [
762 'reply' => $rawText,
763 'chatId' => $this->core->fix_chat_id( $query, $params ),
764 'images' => null,
765 'actions' => [],
766 'usage' => null
767 ];
768 }
769
770 // Moderation
771 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
772 $this->core->get_option( 'shortcode_chat_moderation' );
773 if ( $moderationEnabled ) {
774 global $mwai;
775 $isFlagged = $mwai->moderationCheck( $query->get_message() );
776 if ( $isFlagged ) {
777 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
778 }
779 }
780
781 // Setup streaming if enabled (before embeddings to capture those events)
782 $streamCallback = null;
783 $debugEvents = [];
784
785 if ( $stream ) {
786 $streamCallback = function ( $reply ) use ( $query ) {
787 // Support both legacy string data and new Event objects
788 if ( is_string( $reply ) ) {
789 $this->core->stream_push( [ 'type' => 'live', 'data' => $reply ], $query );
790 }
791 else {
792 $this->core->stream_push( $reply, $query );
793 }
794 };
795 if ( headers_sent( $filename, $linenum ) ) {
796 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
797 }
798 header( 'Cache-Control: no-cache' );
799 header( 'Content-Type: text/event-stream' );
800 // This is useful to disable buffering in nginx through headers.
801 header( 'X-Accel-Buffering: no' );
802 ob_implicit_flush( true );
803 if ( ob_get_level() > 0 ) {
804 ob_end_flush();
805 }
806 }
807 else if ( $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ) ) {
808 // For non-streaming debug mode, collect events
809 $streamCallback = function ( $event ) use ( &$debugEvents ) {
810 if ( is_object( $event ) && method_exists( $event, 'toArray' ) ) {
811 $debugEvents[] = $event->toArray();
812 }
813 };
814 }
815
816 // Awareness & Embeddings
817 $context = $this->core->retrieve_context( $params, $query, $streamCallback );
818 if ( !empty( $context ) ) {
819 $query->set_context( $context['content'] );
820 }
821
822 // Function Aware
823 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
824 }
825
826 // Process Query
827
828 $reply = $this->core->run_query( $query, $streamCallback, true );
829 $rawText = $reply->result;
830 $extra = [];
831 if ( $context ) {
832 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
833 }
834 // Store response ID for Responses API stateful conversations
835 // CRITICAL: Must store even when function calls are present
836 // This enables the feedback query to use previous_response_id
837 if ( !empty( $reply->id ) ) {
838 $extra['responseId'] = $reply->id;
839 $extra['responseDate'] = gmdate( 'Y-m-d H:i:s' ); // Track age for 30-day expiry
840 }
841 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
842
843 // Integrity Check: We need to store the checksum of the messages sent by the client.
844 $stored_messages = $client_messages;
845 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
846 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
847 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
848 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
849
850 // Actions
851 $actions = [];
852 if ( $reply->needClientActions ) {
853 foreach ( $reply->needClientActions as $action ) {
854 $actions[] = [
855 'type' => 'function',
856 'data' => [
857 'name' => $action['function']->name,
858 'args' => $action['arguments']
859 ]
860 ];
861 }
862 }
863
864 $restRes = [
865 'reply' => $rawText,
866 'chatId' => $this->core->fix_chat_id( $query, $params ),
867 'images' => $reply->get_type() === 'images' ? $reply->results : null,
868 'actions' => $actions,
869 'usage' => $reply->usage
870 ];
871
872 // Add debug events if collected
873 if ( !empty( $debugEvents ) ) {
874 $restRes['debugEvents'] = $debugEvents;
875 }
876
877 // Add response ID if available (for Responses API)
878 if ( !empty( $reply->id ) ) {
879 $restRes['responseId'] = $reply->id;
880 }
881
882 // Process Reply
883 if ( $stream ) {
884 $final_res = $this->build_final_res(
885 $botId,
886 $newMessage,
887 $newFileId,
888 $params,
889 $restRes['reply'],
890 $restRes['images'],
891 $restRes['actions'],
892 $restRes['usage'],
893 $restRes['responseId'] ?? null
894 );
895 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
896 die();
897 }
898 else {
899 return $restRes;
900 }
901
902 }
903 catch ( Exception $e ) {
904 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
905 if ( $stream ) {
906 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
907 die();
908 }
909 else {
910 throw $e;
911 }
912 }
913 }
914
915 public function inject_chat() {
916 $params = $this->core->get_chatbot( $this->siteWideChatId );
917 $clean_params = [];
918 if ( !empty( $params ) ) {
919 $clean_params['window'] = true;
920 $clean_params['id'] = $this->siteWideChatId;
921 echo $this->chat_shortcode( $clean_params );
922 }
923 return null;
924 }
925
926 public function build_front_params( $botId, $customId, $crossSite = false ) {
927 $frontSystem = [
928 'botId' => ( $customId && $customId !== '' ) ? null : sanitize_text_field( $botId ),
929 'customId' => ( $customId && $customId !== '' ) ? sanitize_text_field( $customId ) : null,
930 'userData' => $this->core->get_user_data(),
931 'sessionId' => $this->core->get_session_id(),
932 // IMPORTANT: REST nonce handling differs by user state:
933 // - Logged-in users: get_nonce() returns a user-specific nonce created in current session context
934 // - Logged-out users: get_nonce() returns null, they'll fetch via /start_session endpoint
935 // This prevents rest_cookie_invalid_nonce errors for logged-in users by ensuring the nonce
936 // matches their authentication context from the start.
937 'restNonce' => $crossSite ? null : $this->core->get_nonce(),
938 'contextId' => get_the_ID(),
939 'pluginUrl' => MWAI_URL,
940 'restUrl' => untrailingslashit( get_rest_url() ),
941 'stream' => $this->core->get_option( 'ai_streaming' ),
942 'debugMode' => $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ),
943 'eventLogs' => $this->core->get_option( 'event_logs' ),
944 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
945 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
946 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
947 'crossSite' => $crossSite
948 ];
949 return $frontSystem;
950 }
951
952 public function resolveBotInfo( &$atts ) {
953 $chatbot = null;
954 $botId = $atts['id'] ?? null;
955 $customId = $atts['custom_id'] ?? null;
956 $parentBotId = null;
957
958 if ( !$botId && !$customId ) {
959 $botId = 'default';
960 }
961 if ( $botId ) {
962 $chatbot = $this->core->get_chatbot( $botId );
963 if ( !$chatbot ) {
964 $botId = $botId ?: 'N/A';
965 $safe_botId = esc_html( $botId );
966 return [
967 'error' => "AI Engine: Chatbot '{$safe_botId}' not found. If you meant to set an ID for your custom chatbot, please use 'custom_id' instead of 'id'.",
968 ];
969 }
970 }
971 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
972
973 if ( !empty( $customId ) ) {
974 if ( $botId !== null ) {
975 $parentBotId = $botId;
976 $botId = null;
977 }
978 }
979
980 unset( $atts['id'] );
981 return [
982 'chatbot' => $chatbot,
983 'botId' => $botId,
984 'customId' => $customId,
985 'parentBotId' => $parentBotId
986 ];
987 }
988
989 public function chat_shortcode( $atts ) {
990 $atts = empty( $atts ) ? [] : $atts;
991
992 foreach ( $atts as $key => $value ) {
993 $atts[ $key ] = urldecode( $value );
994 }
995
996 // Let the user override the chatbot params
997 $atts = apply_filters( 'mwai_chatbot_params', $atts );
998
999 // Resolve the bot info
1000 $resolvedBot = $this->resolveBotInfo( $atts );
1001 if ( isset( $resolvedBot['error'] ) ) {
1002 return $resolvedBot['error'];
1003 }
1004 $chatbot = $resolvedBot['chatbot'];
1005 $botId = $resolvedBot['botId'];
1006 $customId = $resolvedBot['customId'];
1007 $parentBotId = $resolvedBot['parentBotId'];
1008
1009 // Rename the keys of the atts into camelCase to match the internal params system.
1010 $atts = array_map( function ( $key, $value ) {
1011 $key = str_replace( '_', ' ', $key );
1012 $key = ucwords( $key );
1013 $key = str_replace( ' ', '', $key );
1014 $key = lcfirst( $key );
1015 return [ $key => $value ];
1016 }, array_keys( $atts ), $atts );
1017 $atts = array_merge( ...$atts );
1018
1019 if ( !empty( $parentBotId ) ) {
1020 $atts['parentBotId'] = $parentBotId;
1021 }
1022
1023 $frontParams = [];
1024 // Define text parameters that need sanitization (excluding those that support HTML)
1025 $textParams = ['aiName', 'userName', 'guestName', 'textSend', 'textClear', 'textInputPlaceholder',
1026 'startSentence', 'iconText', 'iconAlt', 'headerSubtitle', 'popupTitle'];
1027 // Parameters that support HTML content
1028 $htmlParams = ['textCompliance'];
1029 // Boolean parameters that need special handling
1030 $booleanParams = ['window', 'copyButton', 'fullscreen', 'localMemory', 'iconBubble', 'centerOpen',
1031 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch'];
1032
1033 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
1034 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
1035 if ( isset( $atts[$param] ) ) {
1036 if ( $param === 'localMemory' ) {
1037 $frontParams[$param] = $atts[$param] === 'true';
1038 }
1039 else if ( in_array( $param, $textParams ) ) {
1040 // Sanitize text parameters to prevent XSS
1041 $frontParams[$param] = sanitize_text_field( $atts[$param] );
1042 }
1043 else if ( in_array( $param, $htmlParams ) ) {
1044 // For HTML parameters, use wp_kses_post to allow safe HTML
1045 $frontParams[$param] = wp_kses_post( $atts[$param] );
1046 }
1047 else if ( in_array( $param, $booleanParams ) ) {
1048 // Convert to proper boolean
1049 // Handle various boolean representations from shortcode attributes
1050 $value = $atts[$param];
1051 if ( is_bool( $value ) ) {
1052 $frontParams[$param] = $value;
1053 } else if ( is_string( $value ) ) {
1054 $frontParams[$param] = !empty( $value ) && $value !== 'false' && $value !== '0' && $value !== 'no';
1055 } else {
1056 $frontParams[$param] = !empty( $value );
1057 }
1058 }
1059 else {
1060 $frontParams[$param] = $atts[$param];
1061 }
1062 }
1063 // If not, let's use the chatbot's default values
1064 else if ( isset( $chatbot[$param] ) ) {
1065 if ( in_array( $param, $booleanParams ) ) {
1066 // Convert to proper boolean for chatbot defaults too
1067 // Handle various boolean representations
1068 $value = $chatbot[$param];
1069
1070 if ( is_bool( $value ) ) {
1071 $frontParams[$param] = $value;
1072 } else if ( is_string( $value ) ) {
1073 $frontParams[$param] = !empty( $value ) && $value !== 'false' && $value !== '0';
1074 } else {
1075 $frontParams[$param] = !empty( $value );
1076 }
1077 }
1078 else {
1079 $frontParams[$param] = $chatbot[$param];
1080 }
1081 }
1082
1083 // Apply the placeholders
1084 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
1085 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
1086 }
1087 }
1088
1089 // Ensure upload params are synced
1090 // fileUpload (checkbox) determines if uploads are enabled
1091 // maxUploads (number) determines how many files can be uploaded
1092 $fileUploadEnabled = !empty( $frontParams['fileUpload'] ) || !empty( $frontParams['imageUpload'] );
1093 $maxFiles = isset( $frontParams['maxUploads'] ) ? max( 1, (int) $frontParams['maxUploads'] ) : 1;
1094
1095 // Sync all params for backward compatibility
1096 $frontParams['fileUpload'] = $fileUploadEnabled;
1097 $frontParams['imageUpload'] = $fileUploadEnabled;
1098 $frontParams['fileUploads'] = $fileUploadEnabled ? $maxFiles : 0;
1099 $frontParams['multiUpload'] = $fileUploadEnabled && $maxFiles > 1;
1100 $frontParams['maxUploads'] = $maxFiles;
1101
1102 // Server Params
1103 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
1104 // we are using the default or a specific chatbot.
1105 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
1106
1107 // Parameters that are purely visual/UI and shouldn't trigger custom ID
1108 $visualOnlyParams = [
1109 // Bot selectors
1110 'id', 'custom_id',
1111 // System-added params
1112 'crossSite',
1113 // Visual/UI parameters that don't affect AI behavior
1114 'aiName', 'userName', 'guestName', // Display names
1115 'aiAvatar', 'userAvatar', 'guestAvatar', 'aiAvatarUrl', 'userAvatarUrl', 'guestAvatarUrl', // Avatars
1116 'textSend', 'textClear', 'textInputPlaceholder', 'textCompliance', // UI text labels
1117 'textInputMaxLength', // Input constraint (visual)
1118 'themeId', // Theme selection
1119 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', // Window/icon settings
1120 'centerOpen', 'width', 'openDelay', 'iconBubble', 'windowAnimation', 'fullscreen', // Window behavior
1121 'copyButton', 'headerSubtitle', 'popupTitle', // UI features
1122 'containerType', 'headerType', 'messagesType', 'inputType', 'footerType' // UI style variants
1123 ];
1124
1125 // Remove visual-only params from override detection
1126 $attsForOverrideCheck = array_diff_key( $atts, array_flip( $visualOnlyParams ) );
1127
1128 // Only these front params affect behavior and should trigger custom ID:
1129 // - mode: chat vs. prompt mode
1130 // - startSentence: initial AI message
1131 // - localMemory: affects data persistence
1132 // - imageUpload, fileUpload, multiUpload, fileSearch: affect capabilities
1133 $behavioralFrontParams = ['mode', 'startSentence', 'localMemory', 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch'];
1134
1135 $hasServerOverrides = count( array_intersect( array_keys( $attsForOverrideCheck ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
1136 $hasBehavioralFrontOverrides = count( array_intersect( array_keys( $attsForOverrideCheck ), $behavioralFrontParams ) ) > 0;
1137 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasBehavioralFrontOverrides );
1138
1139 $serverParams = [];
1140 if ( $hasOverrides ) {
1141 // Server parameters don't need sanitization as they're processed server-side
1142 // and not rendered in HTML. They may contain code, HTML, etc. for AI context.
1143 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
1144 if ( isset( $atts[$param] ) ) {
1145 $serverParams[$param] = $atts[$param];
1146 }
1147 else {
1148 // For custom chatbots, don't inherit embeddingsEnvId from the default chatbot
1149 if ( $param === 'embeddingsEnvId' && !empty( $customId ) ) {
1150 $serverParams[$param] = '';
1151 }
1152 else {
1153 $serverParams[$param] = $chatbot[$param] ?? null;
1154 }
1155 }
1156 }
1157 }
1158
1159 // Front Params
1160 $frontSystem = $this->build_front_params( $botId, $customId );
1161
1162 // Clean Params
1163 $frontParams = $this->clean_params( $frontParams );
1164 $frontSystem = $this->clean_params( $frontSystem );
1165 $serverParams = $this->clean_params( $serverParams );
1166
1167 // Server-side: Keep the System Params
1168 if ( $hasOverrides ) {
1169 if ( empty( $customId ) ) {
1170 $customId = md5( json_encode( $serverParams ) );
1171 $frontSystem['customId'] = $customId;
1172 }
1173 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
1174 }
1175
1176 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
1177 $filterParams = [
1178 'step' => 'init',
1179 'botId' => $botId,
1180 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
1181 ];
1182 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
1183 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
1184 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
1185 $frontSystem['actions'] = $this->sanitize_actions( $actions );
1186 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
1187 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
1188
1189 // Client-side: Prepare JSON for Front Params and System Params
1190 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1191 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1192 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1193 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1194 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
1195
1196 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
1197
1198 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1199 }
1200
1201 public function chatbot_discussions( $atts ) {
1202 $atts = empty( $atts ) ? [] : $atts;
1203
1204 // Resolve the bot info
1205 $resolvedBot = $this->resolveBotInfo( $atts );
1206 if ( isset( $resolvedBot['error'] ) ) {
1207 return $resolvedBot['error'];
1208 }
1209 $chatbot = $resolvedBot['chatbot'];
1210 $botId = $resolvedBot['botId'];
1211 $customId = $resolvedBot['customId'];
1212
1213 // Rename the keys of the atts into camelCase to match the internal params system.
1214 $atts = array_map( function ( $key, $value ) {
1215 $key = str_replace( '_', ' ', $key );
1216 $key = ucwords( $key );
1217 $key = str_replace( ' ', '', $key );
1218 $key = lcfirst( $key );
1219 return [ $key => $value ];
1220 }, array_keys( $atts ), $atts );
1221 $atts = array_merge( ...$atts );
1222
1223 // Front Params
1224 $frontParams = [];
1225 // All discussion params are text params that need sanitization
1226 $textParams = ['textNewChat'];
1227
1228 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
1229 if ( isset( $atts[$param] ) ) {
1230 // Sanitize text parameters
1231 $frontParams[$param] = in_array( $param, $textParams ) ? sanitize_text_field( $atts[$param] ) : $atts[$param];
1232 }
1233 else if ( isset( $chatbot[$param] ) ) {
1234 $frontParams[$param] = $chatbot[$param];
1235 }
1236 }
1237
1238 // Server Params
1239 $serverParams = [];
1240 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
1241 if ( isset( $atts[$param] ) ) {
1242 $serverParams[$param] = $atts[$param];
1243 }
1244 }
1245
1246 // Front System
1247 $frontSystem = $this->build_front_params( $botId, $customId );
1248 // Get refresh interval from settings
1249 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
1250 if ( $refresh_interval === 'Never' ) {
1251 $frontSystem['refreshInterval'] = 0;
1252 }
1253 elseif ( $refresh_interval === 'Manual' ) {
1254 $frontSystem['refreshInterval'] = -1;
1255 }
1256 elseif ( is_numeric( $refresh_interval ) ) {
1257 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
1258 }
1259 else {
1260 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
1261 }
1262 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
1263
1264 // Get paging setting
1265 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
1266 if ( $paging_option === 'None' ) {
1267 $frontSystem['paging'] = 0; // No pagination
1268 }
1269 else {
1270 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
1271 }
1272
1273 // Get metadata settings
1274 $frontSystem['metadata'] = [
1275 'enabled' => $this->core->get_option( 'chatbot_discussions_metadata_enabled' ),
1276 'startDate' => $this->core->get_option( 'chatbot_discussions_metadata_start_date' ),
1277 'lastUpdate' => $this->core->get_option( 'chatbot_discussions_metadata_last_update' ),
1278 'messageCount' => $this->core->get_option( 'chatbot_discussions_metadata_message_count' )
1279 ];
1280
1281 // Clean Params
1282 $frontParams = $this->clean_params( $frontParams );
1283 $frontSystem = $this->clean_params( $frontSystem );
1284 $serverParams = $this->clean_params( $serverParams );
1285
1286 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1287 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1288 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1289 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1290
1291 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1292 }
1293
1294 public function clean_params( &$params ) {
1295 foreach ( $params as $param => $value ) {
1296 if ( $param === 'restNonce' ) {
1297 continue;
1298 }
1299 // Skip only if value is null or an array - but not if it's false or 0
1300 if ( is_null( $value ) || is_array( $value ) ) {
1301 continue;
1302 }
1303 // Handle empty strings
1304 if ( $value === '' ) {
1305 continue;
1306 }
1307 $lowerCaseValue = is_string( $value ) ? strtolower( $value ) : '';
1308 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
1309 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
1310 }
1311 else if ( is_numeric( $value ) ) {
1312 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
1313 }
1314 }
1315 return $params;
1316 }
1317
1318 }
1319