PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.0.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.0.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 1 year ago chatbot.php 10 months ago discussions.php 11 months ago files.php 11 months ago forms-manager.php 10 months ago gdpr.php 11 months ago search.php 1 year ago security.php 1 year ago tasks.php 1 year ago wand.php 10 months ago
chatbot.php
1183 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', 'fileSearch', 'mode', 'textInputPlaceholder', 'textInputMaxLength', 'textCompliance', 'startSentence', 'localMemory', 'themeId', 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', 'centerOpen', 'width', 'openDelay', 'iconBubble', 'windowAnimation', 'fullscreen', 'copyButton', 'headerSubtitle', 'containerType', 'headerType', 'messagesType', 'inputType', 'footerType' ] );
5
6 define( 'MWAI_CHATBOT_SERVER_PARAMS', [ 'id', 'envId', 'scope', 'mode', 'contentAware', 'context', 'startSentence', 'embeddingsEnvId', 'embeddingsIndex', 'embeddingsNamespace', 'assistantId', 'instructions', 'resolution', 'voice', 'model', 'temperature', 'maxTokens', 'contextMaxLength', 'maxResults', 'apiKey', 'functions', 'mcpServers', 'tools', 'historyStrategy', 'previousResponseId', 'parentBotId', 'crossSite' ] );
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 if ( is_admin() || $hasSiteWideChat ) {
41 $themeId = null;
42 if ( $hasSiteWideChat ) {
43 $bot = $this->core->get_chatbot( $this->siteWideChatId );
44 if ( $bot && isset( $bot['themeId'] ) ) {
45 $themeId = $bot['themeId'];
46 }
47 }
48 $this->enqueue_scripts( is_admin() ? null : $themeId );
49 if ( $hasSiteWideChat ) {
50 // Chatbot Injection
51 add_action( 'wp_footer', [ $this, 'inject_chat' ] );
52 }
53 }
54 }
55
56 public function enqueue_scripts( $themeId = null ) {
57 wp_enqueue_script( 'mwai_chatbot' );
58 if ( $this->core->get_option( 'syntax_highlight' ) ) {
59 wp_enqueue_script( 'mwai_highlight' );
60 }
61 if ( $themeId ) {
62 $this->core->enqueue_theme( $themeId );
63 }
64 else {
65 $this->core->enqueue_themes();
66 }
67 }
68
69 /**
70 * Helper method to create REST responses with automatic token refresh
71 *
72 * @param array $data The response data
73 * @param int $status HTTP status code
74 * @return WP_REST_Response
75 */
76 protected function create_rest_response( $data, $status = 200 ) {
77 // Always check if we need to provide a new nonce
78 $current_nonce = $this->core->get_nonce( true );
79 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
80
81 // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
82 // We'll refresh if the nonce is older than 10 hours to be safe
83 $should_refresh = false;
84
85 if ( $request_nonce ) {
86 // Try to determine the age of the nonce
87 // WordPress uses a tick system where each tick is 12 hours
88 // If we're in the second half of the nonce's life, refresh it
89 $time = time();
90 $nonce_tick = wp_nonce_tick();
91
92 // Verify if the nonce is still valid but getting old
93 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
94 if ( $verify === 2 ) {
95 // Nonce is valid but was generated 12-24 hours ago
96 $should_refresh = true;
97 // Log will be written when token is included in response
98 }
99 }
100
101 // If the nonce has changed or should be refreshed, include the new one
102 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
103 $data['new_token'] = $current_nonce;
104
105 // Log if server debug mode is enabled
106 if ( $this->core->get_option( 'server_debug_mode' ) ) {
107 error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
108 }
109 }
110
111 return new WP_REST_Response( $data, $status );
112 }
113
114 public function rest_api_init() {
115 register_rest_route( $this->namespace, '/chats/submit', [
116 'methods' => 'POST',
117 'callback' => [ $this, 'rest_chat' ],
118 'permission_callback' => [ $this->core, 'check_rest_nonce' ]
119 ] );
120 }
121
122 public function basics_security_check( $botId, $customId, $newMessage, $newFileId ) {
123 if ( !$botId && !$customId ) {
124 Meow_MWAI_Logging::warn( 'The query was rejected - no botId nor id was specified.' );
125 return false;
126 }
127
128 if ( $newFileId ) {
129 return true;
130 }
131
132 $length = strlen( empty( $newMessage ) ? '' : $newMessage );
133 if ( $length < 1 ) {
134 Meow_MWAI_Logging::warn( 'The query was rejected - message was too short.' );
135 return false;
136 }
137 return true;
138 }
139
140 public function build_final_res( $botId, $newMessage, $newFileId, $params, $reply, $images, $actions, $usage, $responseId = null ) {
141 $filterParams = [
142 'step' => 'reply',
143 'botId' => $botId,
144 'reply' => $reply,
145 'images' => $images,
146 'newMessage' => $newMessage,
147 'newFileId' => $newFileId,
148 'params' => $params,
149 'usage' => $usage,
150 'messages' => $params['messages'] ?? [],
151 'isNewConversation' => empty( $params['messages'] ) || count( $params['messages'] ) <= 1,
152 ];
153 $actions = apply_filters( 'mwai_chatbot_actions', $actions, $filterParams );
154 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
155 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
156 $actions = $this->sanitize_actions( $actions );
157 $blocks = $this->sanitize_blocks( $blocks );
158 $shortcuts = $this->sanitize_shortcuts( $shortcuts );
159 $result = [
160 'success' => true,
161 'reply' => $reply,
162 'images' => $images,
163 'actions' => $actions,
164 'shortcuts' => $shortcuts,
165 'blocks' => $blocks,
166 'usage' => $usage
167 ];
168
169 // Add response ID if available
170 if ( !empty( $responseId ) ) {
171 $result['responseId'] = $responseId;
172 }
173
174 // Check if token needs refresh
175 $current_nonce = $this->core->get_nonce( true );
176 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
177
178 $should_refresh = false;
179 if ( $request_nonce ) {
180 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
181 if ( $verify === 2 ) {
182 // Nonce is valid but was generated 12-24 hours ago
183 $should_refresh = true;
184 }
185 }
186
187 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
188 $result['new_token'] = $current_nonce;
189 }
190
191 return $result;
192 }
193
194 public function rest_chat( $request ) {
195 $params = $request->get_json_params();
196 $botId = $params['botId'] ?? null;
197 $customId = $params['customId'] ?? null;
198 $stream = $params['stream'] ?? false;
199 $newMessage = trim( $params['newMessage'] ?? '' );
200 $newFileId = $params['newFileId'] ?? null;
201 $newFileIds = $params['newFileIds'] ?? [];
202 $crossSite = $params['crossSite'] ?? false;
203
204 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId ) ) {
205 return $this->create_rest_response( [
206 'success' => false,
207 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
208 ], 403 );
209 }
210
211 try {
212 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream, $newFileIds );
213 $final_res = $this->build_final_res(
214 $botId,
215 $newMessage,
216 $newFileId,
217 $params,
218 $data['reply'],
219 $data['images'],
220 $data['actions'],
221 $data['usage'],
222 $data['responseId'] ?? null
223 );
224 return $this->create_rest_response( $final_res, 200 );
225 }
226 catch ( Exception $e ) {
227 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
228 return $this->create_rest_response( [
229 'success' => false,
230 'message' => $message
231 ], 500 );
232 }
233 }
234
235 private function sanitize_items( $items, $supported_types, $type_name ) {
236 if ( empty( $items ) ) {
237 return $items;
238 }
239 $sanitized_items = [];
240 foreach ( $items as $item ) {
241 if ( isset( $supported_types[$item['type']] ) ) {
242 $is_valid = true;
243 foreach ( $supported_types[$item['type']] as $param ) {
244 if ( !isset( $item['data'][$param] ) ) {
245 $is_valid = false;
246 Meow_MWAI_Logging::warn( "The query was rejected - missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
247 break;
248 }
249 }
250 if ( $is_valid ) {
251 $sanitized_items[] = $item;
252 }
253 }
254 else {
255 Meow_MWAI_Logging::warn( "The query was rejected - unsupported {$type_name} type: {$item['type']}." );
256 }
257 }
258 return $sanitized_items;
259 }
260
261 public function sanitize_actions( $actions ) {
262 $supported_action_types = [
263 'function' => ['name', 'args'],
264 'javascript' => ['snippet'],
265 ];
266 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
267 }
268
269 public function sanitize_blocks( $blocks ) {
270 $supported_block_types = [
271 'content' => ['html'],
272 ];
273 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
274 }
275
276 public function sanitize_shortcuts( $shortcuts ) {
277 $supported_shortcut_types = [
278 'message' => ['label', 'message'],
279 'action' => ['label', 'message', 'action'],
280 'callback' => ['label', 'onClick'],
281 ];
282 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
283 }
284
285 #region Messages Integrity Check
286
287 public function messages_integrity_diff( $messages1, $messages2 ) {
288 // Ensure both parameters are arrays
289 if ( !is_array( $messages1 ) ) {
290 $messages1 = [];
291 }
292 if ( !is_array( $messages2 ) ) {
293 $messages2 = [];
294 }
295
296 // Collect messages with role not 'user' from messages1
297 $messagesList1 = [];
298 foreach ( $messages1 as $msg ) {
299 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
300 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
301 if ( $role && $role != 'user' ) {
302 $messageData = [ 'role' => $role, 'content' => $content ];
303 $messagesList1[] = $messageData;
304 }
305 }
306
307 // Collect messages with role not 'user' from messages2
308 $messagesList2 = [];
309 foreach ( $messages2 as $msg ) {
310 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
311 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
312 if ( $role && $role != 'user' ) {
313 $messageData = [ 'role' => $role, 'content' => $content ];
314 $messagesList2[] = $messageData;
315 }
316 }
317
318 // Count occurrences of each message in messagesList1
319 $counts1 = [];
320 foreach ( $messagesList1 as $msg ) {
321 $key = serialize( $msg );
322 if ( isset( $counts1[ $key ] ) ) {
323 $counts1[ $key ]++;
324 }
325 else {
326 $counts1[ $key ] = 1;
327 }
328 }
329
330 // Count occurrences of each message in messagesList2
331 $counts2 = [];
332 foreach ( $messagesList2 as $msg ) {
333 $key = serialize( $msg );
334 if ( isset( $counts2[ $key ] ) ) {
335 $counts2[ $key ]++;
336 }
337 else {
338 $counts2[ $key ] = 1;
339 }
340 }
341
342 // Compare counts to find unmatched messages
343 $all_keys = array_unique( array_merge( array_keys( $counts1 ), array_keys( $counts2 ) ) );
344
345 $diffs = [];
346 foreach ( $all_keys as $key ) {
347 $count1 = isset( $counts1[ $key ] ) ? $counts1[ $key ] : 0;
348 $count2 = isset( $counts2[ $key ] ) ? $counts2[ $key ] : 0;
349 if ( $count1 != $count2 ) {
350 $message = unserialize( $key );
351 $diffs[] = [
352 'message' => $message,
353 'count_in_messages1' => $count1,
354 'count_in_messages2' => $count2
355 ];
356 }
357 }
358
359 return $diffs;
360 }
361
362 private function calculate_messages_checksum( $messages ) {
363 $messages_to_hash = [];
364 foreach ( $messages as $msg ) {
365 $role = is_array( $msg ) ? ( $msg['role'] ?? '' ) : ( is_object( $msg ) ? ( $msg->role ?? '' ) : '' );
366 $content = is_array( $msg ) ? ( $msg['content'] ?? '' ) : ( is_object( $msg ) ? ( $msg->content ?? '' ) : '' );
367 if ( in_array( $role, ['assistant', 'system'] ) ) {
368 $messages_to_hash[] = [ 'role' => $role, 'content' => $content ];
369 }
370 }
371 return md5( json_encode( $messages_to_hash ) );
372 }
373
374 #endregion
375
376 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false, $newFileIds = [] ) {
377 try {
378 $chatbot = null;
379 $customId = $params['customId'] ?? null;
380
381 // Custom Chatbot
382 if ( $customId ) {
383 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
384 }
385 // Registered Chatbot
386 if ( !$chatbot && $botId ) {
387 $chatbot = $this->core->get_chatbot( $botId );
388 }
389
390 if ( !$chatbot ) {
391 Meow_MWAI_Logging::warn( 'The query was rejected - no chatbot was found.' );
392 throw new Exception( 'Sorry, your query has been rejected.' );
393 }
394
395 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
396 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int) $textInputMaxLength ) {
397 Meow_MWAI_Logging::warn( 'The query was rejected - message was too long.' );
398 throw new Exception( 'Sorry, your query has been rejected.' );
399 }
400
401 // We need to check the integrity of the messages sent by the client.
402 // This is important to ensure that the messages are not tampered with.
403
404 // Messages Integrity Check with Checksums
405 $chatId = $params['chatId'] ?? 'default';
406 $checksum_key = 'mwai_chatbot_checksum_' . $chatId;
407 $stored_checksum = get_transient( $checksum_key );
408 $client_messages = $params['messages'] ?? [];
409 $client_checksum = $this->calculate_messages_checksum( $client_messages );
410 if ( $stored_checksum && $stored_checksum !== $client_checksum ) {
411 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.' );
412 }
413
414 // Messages Integrity Check with Discussions
415 if ( $this->core->get_option( 'chatbot_discussions' ) && $this->core->discussions && isset( $params['chatId'] ) ) {
416 $discussion = $this->core->discussions->get_discussion( $botId ? $botId : $customId, $params['chatId'] );
417 if ( $discussion ) {
418 $messages = $discussion['messages'];
419 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
420 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
421 if ( count( $diffs ) > 0 ) {
422 Meow_MWAI_Logging::warn( "Integrity Check: It seems the messages in the discussion #{$discussion['id']} do not match the ones sent by the client." );
423 }
424
425 // Maintain conversation state for Responses API by loading previousResponseId
426 // This enables stateful conversations where only new messages are sent
427 if ( empty( $params['previousResponseId'] ) && !empty( $discussion['extra'] ) ) {
428 $extra = json_decode( $discussion['extra'], true );
429 if ( !empty( $extra['responseId'] ) ) {
430 // Response IDs expire after 30 days per OpenAI's policy
431 // Check if the stored response is still valid
432 $responseDate = !empty( $extra['responseDate'] ) ? strtotime( $extra['responseDate'] ) : 0;
433 $thirtyDaysAgo = time() - ( 30 * 24 * 60 * 60 );
434
435 if ( $responseDate > $thirtyDaysAgo ) {
436 // Use the stored response ID for stateful conversation
437 $params['previousResponseId'] = $extra['responseId'];
438 }
439 }
440 }
441 }
442 else {
443 // No discussion yet? We still need to check the startSentence.
444 $startSentence = isset( $chatbot['startSentence'] ) ? $chatbot['startSentence'] : null;
445 $messages = [];
446 if ( !empty( $startSentence ) ) {
447 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
448 }
449 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
450 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
451 if ( count( $diffs ) > 0 ) {
452 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 ) );
453 }
454 }
455 }
456
457 // Create QueryText
458 $context = null;
459 $streamCallback = null;
460 $mode = $chatbot['mode'] ?? 'chat';
461
462 if ( $mode === 'images' ) {
463 // Check for uploaded files
464 $fileForImage = null;
465 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
466 $fileForImage = $newFileIds[0];
467 } elseif ( !empty( $newFileId ) ) {
468 $fileForImage = $newFileId;
469 }
470
471 // If there's an uploaded file, use EditImage query instead
472 if ( !empty( $fileForImage ) ) {
473 $query = new Meow_MWAI_Query_EditImage( $newMessage );
474
475 // Handle the uploaded image
476 $url = $this->core->files->get_url( $fileForImage );
477 $mimeType = $this->core->files->get_mime_type( $fileForImage );
478 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
479
480 if ( $isIMG ) {
481 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
482 $fileId = $this->core->files->get_id_from_refId( $fileForImage );
483 $this->core->files->update_purpose( $fileId, 'vision' );
484 }
485 }
486 else {
487 $query = new Meow_MWAI_Query_Image( $newMessage );
488 }
489
490 // Handle Params
491 $newParams = [];
492 foreach ( $chatbot as $key => $value ) {
493 $newParams[$key] = $value;
494 }
495 if ( is_array( $params ) ) {
496 foreach ( $params as $key => $value ) {
497 $newParams[$key] = $value;
498 }
499 }
500
501 // Map 'environment' field to 'envId' for compatibility
502 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
503 $newParams['envId'] = $newParams['environment'];
504 }
505
506 $params = apply_filters( 'mwai_chatbot_params', $newParams );
507 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
508
509
510 // Debug log for embeddings
511 if ( !empty( $params['embeddingsEnvId'] ) ) {
512 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
513 } else {
514 // Log all params to debug
515 $paramKeys = array_keys( $params );
516 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
517 }
518
519 $query->inject_params( $params );
520 }
521 else {
522 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
523 new Meow_MWAI_Query_Text( $newMessage, 4096 );
524
525 // Handle Params
526 $newParams = [];
527 foreach ( $chatbot as $key => $value ) {
528 $newParams[$key] = $value;
529 }
530 if ( is_array( $params ) ) {
531 foreach ( $params as $key => $value ) {
532 $newParams[$key] = $value;
533 }
534 }
535
536 // Map 'environment' field to 'envId' for compatibility
537 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
538 $newParams['envId'] = $newParams['environment'];
539 }
540
541 $params = apply_filters( 'mwai_chatbot_params', $newParams );
542 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
543
544
545 // Debug log for embeddings
546 if ( !empty( $params['embeddingsEnvId'] ) ) {
547 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
548 } else {
549 // Log all params to debug
550 $paramKeys = array_keys( $params );
551 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
552 }
553
554 $query->inject_params( $params );
555
556 $storeId = null;
557 if ( $mode === 'assistant' ) {
558 $chatId = $params['chatId'] ?? null;
559 if ( !empty( $chatId ) && $this->core->discussions ) {
560 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
561 if ( isset( $discussion['storeId'] ) ) {
562 $storeId = $discussion['storeId'];
563 $query->setStoreId( $storeId );
564 }
565 }
566 }
567
568 // Support for Multiple Uploaded Files
569 $filesToProcess = [];
570 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
571 $filesToProcess = $newFileIds;
572 } elseif ( !empty( $newFileId ) ) {
573 $filesToProcess[] = $newFileId;
574 }
575
576 // Support for Uploaded Image/Files
577 if ( !empty( $filesToProcess ) ) {
578 // For now, we only process the first file to maintain backward compatibility
579 // TODO: In the future, we could support multiple files in the query
580 $fileToProcess = $filesToProcess[0];
581
582 // Get extension and mime type
583 $isImage = $this->core->files->is_image( $fileToProcess );
584
585 if ( $mode === 'assistant' && !$isImage ) {
586 $url = $this->core->files->get_path( $fileToProcess );
587 $data = $this->core->files->get_data( $fileToProcess );
588 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
589 $filename = basename( $url );
590
591 // Upload the file
592 $file = $openai->upload_file( $filename, $data, 'assistants' );
593
594 // Create a store
595 if ( empty( $storeId ) ) {
596 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
597 if ( !empty( $query->chatId ) ) {
598 $chatbotName .= '_' . $query->chatId;
599 }
600 $metadata = [];
601 if ( !empty( $chatbot['assistantId'] ) ) {
602 $metadata['assistantId'] = $chatbot['assistantId'];
603 }
604 if ( !empty( $query->chatId ) ) {
605 $metadata['chatId'] = $query->chatId;
606 }
607 $expiry = $this->core->get_option( 'image_expires' );
608 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
609 $query->setStoreId( $storeId );
610 }
611
612 // Add the file to the store - wait a moment for store to be ready
613 sleep( 1 );
614 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
615
616 if ( empty( $storeFileId ) ) {
617 throw new Exception( 'Failed to add file to vector store.' );
618 }
619
620 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
621 $openAiRefId = $file['id'];
622 $internalFileId = $this->core->files->get_id_from_refId( $fileToProcess );
623 $this->core->files->update_refId( $internalFileId, $openAiRefId );
624 $this->core->files->update_envId( $internalFileId, $query->envId );
625 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
626 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
627 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
628 $fileToProcess = $openAiRefId;
629 $scope = $params['fileSearch'];
630 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
631 $id = $this->core->files->get_id_from_refId( $fileToProcess );
632 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
633 }
634 }
635 else {
636 $url = $this->core->files->get_url( $fileToProcess );
637 $mimeType = $this->core->files->get_mime_type( $fileToProcess );
638 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
639 $purposeType = $isIMG ? 'vision' : 'files';
640 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, $purposeType, $mimeType ) );
641 $fileId = $this->core->files->get_id_from_refId( $fileToProcess );
642 $this->core->files->update_envId( $fileId, $query->envId );
643 $this->core->files->update_purpose( $fileId, $purposeType );
644 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
645 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
646 }
647 }
648
649 // Takeover
650 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
651 if ( !empty( $takeoverAnswer ) ) {
652 $rawText = apply_filters( 'mwai_chatbot_reply', $takeoverAnswer, $query, $params, [] );
653 return [
654 'reply' => $rawText,
655 'chatId' => $this->core->fix_chat_id( $query, $params ),
656 'images' => null,
657 'actions' => [],
658 'usage' => null
659 ];
660 }
661
662 // Moderation
663 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
664 $this->core->get_option( 'shortcode_chat_moderation' );
665 if ( $moderationEnabled ) {
666 global $mwai;
667 $isFlagged = $mwai->moderationCheck( $query->get_message() );
668 if ( $isFlagged ) {
669 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
670 }
671 }
672
673 // Setup streaming if enabled (before embeddings to capture those events)
674 $streamCallback = null;
675 $debugEvents = [];
676
677 if ( $stream ) {
678 $streamCallback = function ( $reply ) use ( $query ) {
679 // Support both legacy string data and new Event objects
680 if ( is_string( $reply ) ) {
681 $this->core->stream_push( [ 'type' => 'live', 'data' => $reply ], $query );
682 }
683 else {
684 $this->core->stream_push( $reply, $query );
685 }
686 };
687 if ( headers_sent( $filename, $linenum ) ) {
688 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
689 }
690 header( 'Cache-Control: no-cache' );
691 header( 'Content-Type: text/event-stream' );
692 // This is useful to disable buffering in nginx through headers.
693 header( 'X-Accel-Buffering: no' );
694 ob_implicit_flush( true );
695 if ( ob_get_level() > 0 ) {
696 ob_end_flush();
697 }
698 }
699 else if ( $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ) ) {
700 // For non-streaming debug mode, collect events
701 $streamCallback = function ( $event ) use ( &$debugEvents ) {
702 if ( is_object( $event ) && method_exists( $event, 'toArray' ) ) {
703 $debugEvents[] = $event->toArray();
704 }
705 };
706 }
707
708 // Awareness & Embeddings
709 $context = $this->core->retrieve_context( $params, $query, $streamCallback );
710 if ( !empty( $context ) ) {
711 $query->set_context( $context['content'] );
712 }
713
714 // Function Aware
715 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
716 }
717
718 // Process Query
719
720 $reply = $this->core->run_query( $query, $streamCallback, true );
721 $rawText = $reply->result;
722 $extra = [];
723 if ( $context ) {
724 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
725 }
726 // Store response ID for Responses API stateful conversations
727 // CRITICAL: Must store even when function calls are present
728 // This enables the feedback query to use previous_response_id
729 if ( !empty( $reply->id ) ) {
730 $extra['responseId'] = $reply->id;
731 $extra['responseDate'] = gmdate( 'Y-m-d H:i:s' ); // Track age for 30-day expiry
732 }
733 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
734
735 // Integrity Check: We need to store the checksum of the messages sent by the client.
736 $stored_messages = $client_messages;
737 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
738 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
739 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
740 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
741
742 // Actions
743 $actions = [];
744 if ( $reply->needClientActions ) {
745 foreach ( $reply->needClientActions as $action ) {
746 $actions[] = [
747 'type' => 'function',
748 'data' => [
749 'name' => $action['function']->name,
750 'args' => $action['arguments']
751 ]
752 ];
753 }
754 }
755
756 $restRes = [
757 'reply' => $rawText,
758 'chatId' => $this->core->fix_chat_id( $query, $params ),
759 'images' => $reply->get_type() === 'images' ? $reply->results : null,
760 'actions' => $actions,
761 'usage' => $reply->usage
762 ];
763
764 // Add debug events if collected
765 if ( !empty( $debugEvents ) ) {
766 $restRes['debugEvents'] = $debugEvents;
767 }
768
769 // Add response ID if available (for Responses API)
770 if ( !empty( $reply->id ) ) {
771 $restRes['responseId'] = $reply->id;
772 }
773
774 // Process Reply
775 if ( $stream ) {
776 $final_res = $this->build_final_res(
777 $botId,
778 $newMessage,
779 $newFileId,
780 $params,
781 $restRes['reply'],
782 $restRes['images'],
783 $restRes['actions'],
784 $restRes['usage'],
785 $restRes['responseId'] ?? null
786 );
787 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
788 die();
789 }
790 else {
791 return $restRes;
792 }
793
794 }
795 catch ( Exception $e ) {
796 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
797 if ( $stream ) {
798 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
799 die();
800 }
801 else {
802 throw $e;
803 }
804 }
805 }
806
807 public function inject_chat() {
808 $params = $this->core->get_chatbot( $this->siteWideChatId );
809 $clean_params = [];
810 if ( !empty( $params ) ) {
811 $clean_params['window'] = true;
812 $clean_params['id'] = $this->siteWideChatId;
813 echo $this->chat_shortcode( $clean_params );
814 }
815 return null;
816 }
817
818 public function build_front_params( $botId, $customId, $crossSite = false ) {
819 $frontSystem = [
820 'botId' => ( $customId && $customId !== '' ) ? null : sanitize_text_field( $botId ),
821 'customId' => ( $customId && $customId !== '' ) ? sanitize_text_field( $customId ) : null,
822 'userData' => $this->core->get_user_data(),
823 'sessionId' => $this->core->get_session_id(),
824 // IMPORTANT: REST nonce handling differs by user state:
825 // - Logged-in users: get_nonce() returns a user-specific nonce created in current session context
826 // - Logged-out users: get_nonce() returns null, they'll fetch via /start_session endpoint
827 // This prevents rest_cookie_invalid_nonce errors for logged-in users by ensuring the nonce
828 // matches their authentication context from the start.
829 'restNonce' => $crossSite ? null : $this->core->get_nonce(),
830 'contextId' => get_the_ID(),
831 'pluginUrl' => MWAI_URL,
832 'restUrl' => untrailingslashit( get_rest_url() ),
833 'stream' => $this->core->get_option( 'ai_streaming' ),
834 'debugMode' => $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ),
835 'eventLogs' => $this->core->get_option( 'event_logs' ),
836 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
837 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
838 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
839 'crossSite' => $crossSite
840 ];
841 return $frontSystem;
842 }
843
844 public function resolveBotInfo( &$atts ) {
845 $chatbot = null;
846 $botId = $atts['id'] ?? null;
847 $customId = $atts['custom_id'] ?? null;
848 $parentBotId = null;
849
850 if ( !$botId && !$customId ) {
851 $botId = 'default';
852 }
853 if ( $botId ) {
854 $chatbot = $this->core->get_chatbot( $botId );
855 if ( !$chatbot ) {
856 $botId = $botId ?: 'N/A';
857 $safe_botId = esc_html( $botId );
858 return [
859 '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'.",
860 ];
861 }
862 }
863 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
864
865
866 if ( !empty( $customId ) ) {
867 if ( $botId !== null ) {
868 $parentBotId = $botId;
869 $botId = null;
870 }
871 }
872
873 unset( $atts['id'] );
874 return [
875 'chatbot' => $chatbot,
876 'botId' => $botId,
877 'customId' => $customId,
878 'parentBotId' => $parentBotId
879 ];
880 }
881
882 public function chat_shortcode( $atts ) {
883 $atts = empty( $atts ) ? [] : $atts;
884
885 foreach ( $atts as $key => $value ) {
886 $atts[ $key ] = urldecode( $value );
887 }
888
889 // Let the user override the chatbot params
890 $atts = apply_filters( 'mwai_chatbot_params', $atts );
891
892 // Resolve the bot info
893 $resolvedBot = $this->resolveBotInfo( $atts );
894 if ( isset( $resolvedBot['error'] ) ) {
895 return $resolvedBot['error'];
896 }
897 $chatbot = $resolvedBot['chatbot'];
898 $botId = $resolvedBot['botId'];
899 $customId = $resolvedBot['customId'];
900 $parentBotId = $resolvedBot['parentBotId'];
901
902 // Rename the keys of the atts into camelCase to match the internal params system.
903 $atts = array_map( function ( $key, $value ) {
904 $key = str_replace( '_', ' ', $key );
905 $key = ucwords( $key );
906 $key = str_replace( ' ', '', $key );
907 $key = lcfirst( $key );
908 return [ $key => $value ];
909 }, array_keys( $atts ), $atts );
910 $atts = array_merge( ...$atts );
911
912 if ( !empty( $parentBotId ) ) {
913 $atts['parentBotId'] = $parentBotId;
914 }
915
916 $frontParams = [];
917 // Define text parameters that need sanitization (excluding those that support HTML)
918 $textParams = ['aiName', 'userName', 'guestName', 'textSend', 'textClear', 'textInputPlaceholder',
919 'startSentence', 'iconText', 'iconAlt', 'headerSubtitle'];
920 // Parameters that support HTML content
921 $htmlParams = ['textCompliance'];
922 // Boolean parameters that need special handling
923 $booleanParams = ['window', 'copyButton', 'fullscreen', 'localMemory', 'iconBubble', 'centerOpen',
924 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch'];
925
926
927 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
928 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
929 if ( isset( $atts[$param] ) ) {
930 if ( $param === 'localMemory' ) {
931 $frontParams[$param] = $atts[$param] === 'true';
932 }
933 else if ( in_array( $param, $textParams ) ) {
934 // Sanitize text parameters to prevent XSS
935 $frontParams[$param] = sanitize_text_field( $atts[$param] );
936 }
937 else if ( in_array( $param, $htmlParams ) ) {
938 // For HTML parameters, use wp_kses_post to allow safe HTML
939 $frontParams[$param] = wp_kses_post( $atts[$param] );
940 }
941 else if ( in_array( $param, $booleanParams ) ) {
942 // Convert to proper boolean
943 $frontParams[$param] = !empty( $atts[$param] ) && $atts[$param] !== 'false';
944 }
945 else {
946 $frontParams[$param] = $atts[$param];
947 }
948 }
949 // If not, let's use the chatbot's default values
950 else if ( isset( $chatbot[$param] ) ) {
951 if ( in_array( $param, $booleanParams ) ) {
952 // Convert to proper boolean for chatbot defaults too
953 $frontParams[$param] = !empty( $chatbot[$param] ) && $chatbot[$param] !== 'false';
954 }
955 else {
956 $frontParams[$param] = $chatbot[$param];
957 }
958 }
959
960 // Apply the placeholders
961 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
962 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
963 }
964 }
965
966 // Server Params
967 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
968 // we are using the default or a specific chatbot.
969 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
970
971 // Parameters that are purely visual/UI and shouldn't trigger custom ID
972 $visualOnlyParams = [
973 // Bot selectors
974 'id', 'custom_id',
975 // System-added params
976 'crossSite',
977 // Visual/UI parameters that don't affect AI behavior
978 'aiName', 'userName', 'guestName', // Display names
979 'aiAvatar', 'userAvatar', 'guestAvatar', 'aiAvatarUrl', 'userAvatarUrl', 'guestAvatarUrl', // Avatars
980 'textSend', 'textClear', 'textInputPlaceholder', 'textCompliance', // UI text labels
981 'textInputMaxLength', // Input constraint (visual)
982 'themeId', // Theme selection
983 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', // Window/icon settings
984 'centerOpen', 'width', 'openDelay', 'iconBubble', 'windowAnimation', 'fullscreen', // Window behavior
985 'copyButton', 'headerSubtitle', // UI features
986 'containerType', 'headerType', 'messagesType', 'inputType', 'footerType' // UI style variants
987 ];
988
989 // Remove visual-only params from override detection
990 $attsForOverrideCheck = array_diff_key( $atts, array_flip( $visualOnlyParams ) );
991
992 // Only these front params affect behavior and should trigger custom ID:
993 // - mode: chat vs. prompt mode
994 // - startSentence: initial AI message
995 // - localMemory: affects data persistence
996 // - imageUpload, fileUpload, multiUpload, fileSearch: affect capabilities
997 $behavioralFrontParams = ['mode', 'startSentence', 'localMemory', 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch'];
998
999 $hasServerOverrides = count( array_intersect( array_keys( $attsForOverrideCheck ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
1000 $hasBehavioralFrontOverrides = count( array_intersect( array_keys( $attsForOverrideCheck ), $behavioralFrontParams ) ) > 0;
1001 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasBehavioralFrontOverrides );
1002
1003 $serverParams = [];
1004 if ( $hasOverrides ) {
1005 // Server parameters don't need sanitization as they're processed server-side
1006 // and not rendered in HTML. They may contain code, HTML, etc. for AI context.
1007 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
1008 if ( isset( $atts[$param] ) ) {
1009 $serverParams[$param] = $atts[$param];
1010 }
1011 else {
1012 // For custom chatbots, don't inherit embeddingsEnvId from the default chatbot
1013 if ( $param === 'embeddingsEnvId' && !empty( $customId ) ) {
1014 $serverParams[$param] = '';
1015 }
1016 else {
1017 $serverParams[$param] = $chatbot[$param] ?? null;
1018 }
1019 }
1020 }
1021 }
1022
1023 // Front Params
1024 $frontSystem = $this->build_front_params( $botId, $customId );
1025
1026 // Clean Params
1027 $frontParams = $this->clean_params( $frontParams );
1028 $frontSystem = $this->clean_params( $frontSystem );
1029 $serverParams = $this->clean_params( $serverParams );
1030
1031 // Server-side: Keep the System Params
1032 if ( $hasOverrides ) {
1033 if ( empty( $customId ) ) {
1034 $customId = md5( json_encode( $serverParams ) );
1035 $frontSystem['customId'] = $customId;
1036 }
1037 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
1038 }
1039
1040 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
1041 $filterParams = [
1042 'step' => 'init',
1043 'botId' => $botId,
1044 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
1045 ];
1046 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
1047 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
1048 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
1049 $frontSystem['actions'] = $this->sanitize_actions( $actions );
1050 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
1051 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
1052
1053 // Client-side: Prepare JSON for Front Params and System Params
1054 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1055 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1056 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1057 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1058 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
1059
1060 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
1061
1062 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1063 }
1064
1065 public function chatbot_discussions( $atts ) {
1066 $atts = empty( $atts ) ? [] : $atts;
1067
1068 // Resolve the bot info
1069 $resolvedBot = $this->resolveBotInfo( $atts );
1070 if ( isset( $resolvedBot['error'] ) ) {
1071 return $resolvedBot['error'];
1072 }
1073 $chatbot = $resolvedBot['chatbot'];
1074 $botId = $resolvedBot['botId'];
1075 $customId = $resolvedBot['customId'];
1076
1077 // Rename the keys of the atts into camelCase to match the internal params system.
1078 $atts = array_map( function ( $key, $value ) {
1079 $key = str_replace( '_', ' ', $key );
1080 $key = ucwords( $key );
1081 $key = str_replace( ' ', '', $key );
1082 $key = lcfirst( $key );
1083 return [ $key => $value ];
1084 }, array_keys( $atts ), $atts );
1085 $atts = array_merge( ...$atts );
1086
1087 // Front Params
1088 $frontParams = [];
1089 // All discussion params are text params that need sanitization
1090 $textParams = ['textNewChat'];
1091
1092 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
1093 if ( isset( $atts[$param] ) ) {
1094 // Sanitize text parameters
1095 $frontParams[$param] = in_array( $param, $textParams ) ? sanitize_text_field( $atts[$param] ) : $atts[$param];
1096 }
1097 else if ( isset( $chatbot[$param] ) ) {
1098 $frontParams[$param] = $chatbot[$param];
1099 }
1100 }
1101
1102 // Server Params
1103 $serverParams = [];
1104 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
1105 if ( isset( $atts[$param] ) ) {
1106 $serverParams[$param] = $atts[$param];
1107 }
1108 }
1109
1110 // Front System
1111 $frontSystem = $this->build_front_params( $botId, $customId );
1112 // Get refresh interval from settings
1113 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
1114 if ( $refresh_interval === 'Never' ) {
1115 $frontSystem['refreshInterval'] = 0;
1116 }
1117 elseif ( $refresh_interval === 'Manual' ) {
1118 $frontSystem['refreshInterval'] = -1;
1119 }
1120 elseif ( is_numeric( $refresh_interval ) ) {
1121 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
1122 }
1123 else {
1124 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
1125 }
1126 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
1127
1128 // Get paging setting
1129 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
1130 if ( $paging_option === 'None' ) {
1131 $frontSystem['paging'] = 0; // No pagination
1132 }
1133 else {
1134 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
1135 }
1136
1137 // Get metadata settings
1138 $frontSystem['metadata'] = [
1139 'enabled' => $this->core->get_option( 'chatbot_discussions_metadata_enabled' ),
1140 'startDate' => $this->core->get_option( 'chatbot_discussions_metadata_start_date' ),
1141 'lastUpdate' => $this->core->get_option( 'chatbot_discussions_metadata_last_update' ),
1142 'messageCount' => $this->core->get_option( 'chatbot_discussions_metadata_message_count' )
1143 ];
1144
1145 // Clean Params
1146 $frontParams = $this->clean_params( $frontParams );
1147 $frontSystem = $this->clean_params( $frontSystem );
1148 $serverParams = $this->clean_params( $serverParams );
1149
1150 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1151 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1152 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1153 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1154
1155 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1156 }
1157
1158 public function clean_params( &$params ) {
1159 foreach ( $params as $param => $value ) {
1160 if ( $param === 'restNonce' ) {
1161 continue;
1162 }
1163 // Skip only if value is null or an array - but not if it's false or 0
1164 if ( is_null( $value ) || is_array( $value ) ) {
1165 continue;
1166 }
1167 // Handle empty strings
1168 if ( $value === '' ) {
1169 continue;
1170 }
1171 $lowerCaseValue = is_string( $value ) ? strtolower( $value ) : '';
1172 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
1173 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
1174 }
1175 else if ( is_numeric( $value ) ) {
1176 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
1177 }
1178 }
1179 return $params;
1180 }
1181
1182 }
1183