PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.8
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.8
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 11 months ago discussions.php 11 months ago files.php 11 months ago gdpr.php 11 months ago search.php 1 year ago security.php 1 year ago tasks.php 1 year ago wand.php 1 year ago
chatbot.php
1132 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', 'iconBubble', 'fullscreen', 'copyButton', 'headerSubtitle' ] );
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' ] );
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
203 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId ) ) {
204 return $this->create_rest_response( [
205 'success' => false,
206 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
207 ], 403 );
208 }
209
210 try {
211 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream, $newFileIds );
212 $final_res = $this->build_final_res(
213 $botId,
214 $newMessage,
215 $newFileId,
216 $params,
217 $data['reply'],
218 $data['images'],
219 $data['actions'],
220 $data['usage'],
221 $data['responseId'] ?? null
222 );
223 return $this->create_rest_response( $final_res, 200 );
224 }
225 catch ( Exception $e ) {
226 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
227 return $this->create_rest_response( [
228 'success' => false,
229 'message' => $message
230 ], 500 );
231 }
232 }
233
234 private function sanitize_items( $items, $supported_types, $type_name ) {
235 if ( empty( $items ) ) {
236 return $items;
237 }
238 $sanitized_items = [];
239 foreach ( $items as $item ) {
240 if ( isset( $supported_types[$item['type']] ) ) {
241 $is_valid = true;
242 foreach ( $supported_types[$item['type']] as $param ) {
243 if ( !isset( $item['data'][$param] ) ) {
244 $is_valid = false;
245 Meow_MWAI_Logging::warn( "The query was rejected - missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
246 break;
247 }
248 }
249 if ( $is_valid ) {
250 $sanitized_items[] = $item;
251 }
252 }
253 else {
254 Meow_MWAI_Logging::warn( "The query was rejected - unsupported {$type_name} type: {$item['type']}." );
255 }
256 }
257 return $sanitized_items;
258 }
259
260 public function sanitize_actions( $actions ) {
261 $supported_action_types = [
262 'function' => ['name', 'args'],
263 'javascript' => ['snippet'],
264 ];
265 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
266 }
267
268 public function sanitize_blocks( $blocks ) {
269 $supported_block_types = [
270 'content' => ['html'],
271 ];
272 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
273 }
274
275 public function sanitize_shortcuts( $shortcuts ) {
276 $supported_shortcut_types = [
277 'message' => ['label', 'message'],
278 'callback' => ['label', 'onClick'],
279 ];
280 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
281 }
282
283 #region Messages Integrity Check
284
285 public function messages_integrity_diff( $messages1, $messages2 ) {
286 // Ensure both parameters are arrays
287 if ( !is_array( $messages1 ) ) {
288 $messages1 = [];
289 }
290 if ( !is_array( $messages2 ) ) {
291 $messages2 = [];
292 }
293
294 // Collect messages with role not 'user' from messages1
295 $messagesList1 = [];
296 foreach ( $messages1 as $msg ) {
297 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
298 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
299 if ( $role && $role != 'user' ) {
300 $messageData = [ 'role' => $role, 'content' => $content ];
301 $messagesList1[] = $messageData;
302 }
303 }
304
305 // Collect messages with role not 'user' from messages2
306 $messagesList2 = [];
307 foreach ( $messages2 as $msg ) {
308 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
309 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
310 if ( $role && $role != 'user' ) {
311 $messageData = [ 'role' => $role, 'content' => $content ];
312 $messagesList2[] = $messageData;
313 }
314 }
315
316 // Count occurrences of each message in messagesList1
317 $counts1 = [];
318 foreach ( $messagesList1 as $msg ) {
319 $key = serialize( $msg );
320 if ( isset( $counts1[ $key ] ) ) {
321 $counts1[ $key ]++;
322 }
323 else {
324 $counts1[ $key ] = 1;
325 }
326 }
327
328 // Count occurrences of each message in messagesList2
329 $counts2 = [];
330 foreach ( $messagesList2 as $msg ) {
331 $key = serialize( $msg );
332 if ( isset( $counts2[ $key ] ) ) {
333 $counts2[ $key ]++;
334 }
335 else {
336 $counts2[ $key ] = 1;
337 }
338 }
339
340 // Compare counts to find unmatched messages
341 $all_keys = array_unique( array_merge( array_keys( $counts1 ), array_keys( $counts2 ) ) );
342
343 $diffs = [];
344 foreach ( $all_keys as $key ) {
345 $count1 = isset( $counts1[ $key ] ) ? $counts1[ $key ] : 0;
346 $count2 = isset( $counts2[ $key ] ) ? $counts2[ $key ] : 0;
347 if ( $count1 != $count2 ) {
348 $message = unserialize( $key );
349 $diffs[] = [
350 'message' => $message,
351 'count_in_messages1' => $count1,
352 'count_in_messages2' => $count2
353 ];
354 }
355 }
356
357 return $diffs;
358 }
359
360 private function calculate_messages_checksum( $messages ) {
361 $messages_to_hash = [];
362 foreach ( $messages as $msg ) {
363 $role = is_array( $msg ) ? ( $msg['role'] ?? '' ) : ( is_object( $msg ) ? ( $msg->role ?? '' ) : '' );
364 $content = is_array( $msg ) ? ( $msg['content'] ?? '' ) : ( is_object( $msg ) ? ( $msg->content ?? '' ) : '' );
365 if ( in_array( $role, ['assistant', 'system'] ) ) {
366 $messages_to_hash[] = [ 'role' => $role, 'content' => $content ];
367 }
368 }
369 return md5( json_encode( $messages_to_hash ) );
370 }
371
372 #endregion
373
374 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false, $newFileIds = [] ) {
375 try {
376 $chatbot = null;
377 $customId = $params['customId'] ?? null;
378
379 // Custom Chatbot
380 if ( $customId ) {
381 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
382 }
383 // Registered Chatbot
384 if ( !$chatbot && $botId ) {
385 $chatbot = $this->core->get_chatbot( $botId );
386 }
387
388 if ( !$chatbot ) {
389 Meow_MWAI_Logging::warn( 'The query was rejected - no chatbot was found.' );
390 throw new Exception( 'Sorry, your query has been rejected.' );
391 }
392
393 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
394 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int) $textInputMaxLength ) {
395 Meow_MWAI_Logging::warn( 'The query was rejected - message was too long.' );
396 throw new Exception( 'Sorry, your query has been rejected.' );
397 }
398
399 // We need to check the integrity of the messages sent by the client.
400 // This is important to ensure that the messages are not tampered with.
401
402 // Messages Integrity Check with Checksums
403 $chatId = $params['chatId'] ?? 'default';
404 $checksum_key = 'mwai_chatbot_checksum_' . $chatId;
405 $stored_checksum = get_transient( $checksum_key );
406 $client_messages = $params['messages'] ?? [];
407 $client_checksum = $this->calculate_messages_checksum( $client_messages );
408 if ( $stored_checksum && $stored_checksum !== $client_checksum ) {
409 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.' );
410 }
411
412 // Messages Integrity Check with Discussions
413 if ( $this->core->get_option( 'chatbot_discussions' ) && $this->core->discussions && isset( $params['chatId'] ) ) {
414 $discussion = $this->core->discussions->get_discussion( $botId ? $botId : $customId, $params['chatId'] );
415 if ( $discussion ) {
416 $messages = $discussion['messages'];
417 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
418 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
419 if ( count( $diffs ) > 0 ) {
420 Meow_MWAI_Logging::warn( "Integrity Check: It seems the messages in the discussion #{$discussion['id']} do not match the ones sent by the client." );
421 }
422
423 // Maintain conversation state for Responses API by loading previousResponseId
424 // This enables stateful conversations where only new messages are sent
425 if ( empty( $params['previousResponseId'] ) && !empty( $discussion['extra'] ) ) {
426 $extra = json_decode( $discussion['extra'], true );
427 if ( !empty( $extra['responseId'] ) ) {
428 // Response IDs expire after 30 days per OpenAI's policy
429 // Check if the stored response is still valid
430 $responseDate = !empty( $extra['responseDate'] ) ? strtotime( $extra['responseDate'] ) : 0;
431 $thirtyDaysAgo = time() - ( 30 * 24 * 60 * 60 );
432
433 if ( $responseDate > $thirtyDaysAgo ) {
434 // Use the stored response ID for stateful conversation
435 $params['previousResponseId'] = $extra['responseId'];
436 }
437 }
438 }
439 }
440 else {
441 // No discussion yet? We still need to check the startSentence.
442 $startSentence = isset( $chatbot['startSentence'] ) ? $chatbot['startSentence'] : null;
443 $messages = [];
444 if ( !empty( $startSentence ) ) {
445 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
446 }
447 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
448 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
449 if ( count( $diffs ) > 0 ) {
450 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 ) );
451 }
452 }
453 }
454
455 // Create QueryText
456 $context = null;
457 $streamCallback = null;
458 $mode = $chatbot['mode'] ?? 'chat';
459
460 if ( $mode === 'images' ) {
461 // Check for uploaded files
462 $fileForImage = null;
463 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
464 $fileForImage = $newFileIds[0];
465 } elseif ( !empty( $newFileId ) ) {
466 $fileForImage = $newFileId;
467 }
468
469 // If there's an uploaded file, use EditImage query instead
470 if ( !empty( $fileForImage ) ) {
471 $query = new Meow_MWAI_Query_EditImage( $newMessage );
472
473 // Handle the uploaded image
474 $url = $this->core->files->get_url( $fileForImage );
475 $mimeType = $this->core->files->get_mime_type( $fileForImage );
476 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
477
478 if ( $isIMG ) {
479 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
480 $fileId = $this->core->files->get_id_from_refId( $fileForImage );
481 $this->core->files->update_purpose( $fileId, 'vision' );
482 }
483 }
484 else {
485 $query = new Meow_MWAI_Query_Image( $newMessage );
486 }
487
488 // Handle Params
489 $newParams = [];
490 foreach ( $chatbot as $key => $value ) {
491 $newParams[$key] = $value;
492 }
493 if ( is_array( $params ) ) {
494 foreach ( $params as $key => $value ) {
495 $newParams[$key] = $value;
496 }
497 }
498
499 // Map 'environment' field to 'envId' for compatibility
500 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
501 $newParams['envId'] = $newParams['environment'];
502 }
503
504 $params = apply_filters( 'mwai_chatbot_params', $newParams );
505 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
506
507
508 // Debug log for embeddings
509 if ( !empty( $params['embeddingsEnvId'] ) ) {
510 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
511 } else {
512 // Log all params to debug
513 $paramKeys = array_keys( $params );
514 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
515 }
516
517 $query->inject_params( $params );
518 }
519 else {
520 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
521 new Meow_MWAI_Query_Text( $newMessage, 1024 );
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
543 // Debug log for embeddings
544 if ( !empty( $params['embeddingsEnvId'] ) ) {
545 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
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 $storeId = null;
555 if ( $mode === 'assistant' ) {
556 $chatId = $params['chatId'] ?? null;
557 if ( !empty( $chatId ) && $this->core->discussions ) {
558 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
559 if ( isset( $discussion['storeId'] ) ) {
560 $storeId = $discussion['storeId'];
561 $query->setStoreId( $storeId );
562 }
563 }
564 }
565
566 // Support for Multiple Uploaded Files
567 $filesToProcess = [];
568 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
569 $filesToProcess = $newFileIds;
570 } elseif ( !empty( $newFileId ) ) {
571 $filesToProcess[] = $newFileId;
572 }
573
574 // Support for Uploaded Image/Files
575 if ( !empty( $filesToProcess ) ) {
576 // For now, we only process the first file to maintain backward compatibility
577 // TODO: In the future, we could support multiple files in the query
578 $fileToProcess = $filesToProcess[0];
579
580 // Get extension and mime type
581 $isImage = $this->core->files->is_image( $fileToProcess );
582
583 if ( $mode === 'assistant' && !$isImage ) {
584 $url = $this->core->files->get_path( $fileToProcess );
585 $data = $this->core->files->get_data( $fileToProcess );
586 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
587 $filename = basename( $url );
588
589 // Upload the file
590 $file = $openai->upload_file( $filename, $data, 'assistants' );
591
592 // Create a store
593 if ( empty( $storeId ) ) {
594 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
595 if ( !empty( $query->chatId ) ) {
596 $chatbotName .= '_' . $query->chatId;
597 }
598 $metadata = [];
599 if ( !empty( $chatbot['assistantId'] ) ) {
600 $metadata['assistantId'] = $chatbot['assistantId'];
601 }
602 if ( !empty( $query->chatId ) ) {
603 $metadata['chatId'] = $query->chatId;
604 }
605 $expiry = $this->core->get_option( 'image_expires' );
606 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
607 $query->setStoreId( $storeId );
608 }
609
610 // Add the file to the store
611 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
612
613 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
614 $openAiRefId = $file['id'];
615 $internalFileId = $this->core->files->get_id_from_refId( $fileToProcess );
616 $this->core->files->update_refId( $internalFileId, $openAiRefId );
617 $this->core->files->update_envId( $internalFileId, $query->envId );
618 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
619 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
620 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
621 $fileToProcess = $openAiRefId;
622 $scope = $params['fileSearch'];
623 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
624 $id = $this->core->files->get_id_from_refId( $fileToProcess );
625 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
626 }
627 }
628 else {
629 $url = $this->core->files->get_url( $fileToProcess );
630 $mimeType = $this->core->files->get_mime_type( $fileToProcess );
631 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
632 $purposeType = $isIMG ? 'vision' : 'files';
633 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, $purposeType, $mimeType ) );
634 $fileId = $this->core->files->get_id_from_refId( $fileToProcess );
635 $this->core->files->update_envId( $fileId, $query->envId );
636 $this->core->files->update_purpose( $fileId, $purposeType );
637 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
638 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
639 }
640 }
641
642 // Takeover
643 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
644 if ( !empty( $takeoverAnswer ) ) {
645 $rawText = apply_filters( 'mwai_chatbot_reply', $takeoverAnswer, $query, $params, [] );
646 return [
647 'reply' => $rawText,
648 'chatId' => $this->core->fix_chat_id( $query, $params ),
649 'images' => null,
650 'actions' => [],
651 'usage' => null
652 ];
653 }
654
655 // Moderation
656 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
657 $this->core->get_option( 'shortcode_chat_moderation' );
658 if ( $moderationEnabled ) {
659 global $mwai;
660 $isFlagged = $mwai->moderationCheck( $query->get_message() );
661 if ( $isFlagged ) {
662 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
663 }
664 }
665
666 // Setup streaming if enabled (before embeddings to capture those events)
667 $streamCallback = null;
668 $debugEvents = [];
669
670 if ( $stream ) {
671 $streamCallback = function ( $reply ) use ( $query ) {
672 // Support both legacy string data and new Event objects
673 if ( is_string( $reply ) ) {
674 $this->core->stream_push( [ 'type' => 'live', 'data' => $reply ], $query );
675 }
676 else {
677 $this->core->stream_push( $reply, $query );
678 }
679 };
680 if ( headers_sent( $filename, $linenum ) ) {
681 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
682 }
683 header( 'Cache-Control: no-cache' );
684 header( 'Content-Type: text/event-stream' );
685 // This is useful to disable buffering in nginx through headers.
686 header( 'X-Accel-Buffering: no' );
687 ob_implicit_flush( true );
688 if ( ob_get_level() > 0 ) {
689 ob_end_flush();
690 }
691 }
692 else if ( $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ) ) {
693 // For non-streaming debug mode, collect events
694 $streamCallback = function ( $event ) use ( &$debugEvents ) {
695 if ( is_object( $event ) && method_exists( $event, 'toArray' ) ) {
696 $debugEvents[] = $event->toArray();
697 }
698 };
699 }
700
701 // Awareness & Embeddings
702 $context = $this->core->retrieve_context( $params, $query, $streamCallback );
703 if ( !empty( $context ) ) {
704 $query->set_context( $context['content'] );
705 }
706
707 // Function Aware
708 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
709 }
710
711 // Process Query
712
713 $reply = $this->core->run_query( $query, $streamCallback, true );
714 $rawText = $reply->result;
715 $extra = [];
716 if ( $context ) {
717 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
718 }
719 // Store response ID for Responses API stateful conversations
720 // CRITICAL: Must store even when function calls are present
721 // This enables the feedback query to use previous_response_id
722 if ( !empty( $reply->id ) ) {
723 $extra['responseId'] = $reply->id;
724 $extra['responseDate'] = gmdate( 'Y-m-d H:i:s' ); // Track age for 30-day expiry
725 }
726 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
727
728 // Integrity Check: We need to store the checksum of the messages sent by the client.
729 $stored_messages = $client_messages;
730 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
731 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
732 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
733 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
734
735 // Actions
736 $actions = [];
737 if ( $reply->needClientActions ) {
738 foreach ( $reply->needClientActions as $action ) {
739 $actions[] = [
740 'type' => 'function',
741 'data' => [
742 'name' => $action['function']->name,
743 'args' => $action['arguments']
744 ]
745 ];
746 }
747 }
748
749 $restRes = [
750 'reply' => $rawText,
751 'chatId' => $this->core->fix_chat_id( $query, $params ),
752 'images' => $reply->get_type() === 'images' ? $reply->results : null,
753 'actions' => $actions,
754 'usage' => $reply->usage
755 ];
756
757 // Add debug events if collected
758 if ( !empty( $debugEvents ) ) {
759 $restRes['debugEvents'] = $debugEvents;
760 }
761
762 // Add response ID if available (for Responses API)
763 if ( !empty( $reply->id ) ) {
764 $restRes['responseId'] = $reply->id;
765 }
766
767 // Process Reply
768 if ( $stream ) {
769 $final_res = $this->build_final_res(
770 $botId,
771 $newMessage,
772 $newFileId,
773 $params,
774 $restRes['reply'],
775 $restRes['images'],
776 $restRes['actions'],
777 $restRes['usage'],
778 $restRes['responseId'] ?? null
779 );
780 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
781 die();
782 }
783 else {
784 return $restRes;
785 }
786
787 }
788 catch ( Exception $e ) {
789 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
790 if ( $stream ) {
791 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
792 die();
793 }
794 else {
795 throw $e;
796 }
797 }
798 }
799
800 public function inject_chat() {
801 $params = $this->core->get_chatbot( $this->siteWideChatId );
802 $clean_params = [];
803 if ( !empty( $params ) ) {
804 $clean_params['window'] = true;
805 $clean_params['id'] = $this->siteWideChatId;
806 echo $this->chat_shortcode( $clean_params );
807 }
808 return null;
809 }
810
811 public function build_front_params( $botId, $customId ) {
812 $frontSystem = [
813 'botId' => $customId ? null : sanitize_text_field( $botId ),
814 'customId' => sanitize_text_field( $customId ),
815 'userData' => $this->core->get_user_data(),
816 'sessionId' => $this->core->get_session_id(),
817 // IMPORTANT: REST nonce handling differs by user state:
818 // - Logged-in users: get_nonce() returns a user-specific nonce created in current session context
819 // - Logged-out users: get_nonce() returns null, they'll fetch via /start_session endpoint
820 // This prevents rest_cookie_invalid_nonce errors for logged-in users by ensuring the nonce
821 // matches their authentication context from the start.
822 'restNonce' => $this->core->get_nonce(),
823 'contextId' => get_the_ID(),
824 'pluginUrl' => MWAI_URL,
825 'restUrl' => untrailingslashit( get_rest_url() ),
826 'stream' => $this->core->get_option( 'ai_streaming' ),
827 'debugMode' => $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ),
828 'eventLogs' => $this->core->get_option( 'event_logs' ),
829 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
830 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
831 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
832 'virtual_keyboard_fix' => $this->core->get_option( 'virtual_keyboard_fix' )
833 ];
834 return $frontSystem;
835 }
836
837 public function resolveBotInfo( &$atts ) {
838 $chatbot = null;
839 $botId = $atts['id'] ?? null;
840 $customId = $atts['custom_id'] ?? null;
841 $parentBotId = null;
842
843 if ( !$botId && !$customId ) {
844 $botId = 'default';
845 }
846 if ( $botId ) {
847 $chatbot = $this->core->get_chatbot( $botId );
848 if ( !$chatbot ) {
849 $botId = $botId ?: 'N/A';
850 $safe_botId = esc_html( $botId );
851 return [
852 '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'.",
853 ];
854 }
855 }
856 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
857
858
859 if ( !empty( $customId ) ) {
860 if ( $botId !== null ) {
861 $parentBotId = $botId;
862 $botId = null;
863 }
864 }
865 unset( $atts['id'] );
866 return [
867 'chatbot' => $chatbot,
868 'botId' => $botId,
869 'customId' => $customId,
870 'parentBotId' => $parentBotId
871 ];
872 }
873
874 public function chat_shortcode( $atts ) {
875 $atts = empty( $atts ) ? [] : $atts;
876
877 foreach ( $atts as $key => $value ) {
878 $atts[ $key ] = urldecode( $value );
879 }
880
881 // Let the user override the chatbot params
882 $atts = apply_filters( 'mwai_chatbot_params', $atts );
883
884 // Resolve the bot info
885 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
886 if ( isset( $resolvedBot['error'] ) ) {
887 return $resolvedBot['error'];
888 }
889 $chatbot = $resolvedBot['chatbot'];
890 $botId = $resolvedBot['botId'];
891 $customId = $resolvedBot['customId'];
892 $parentBotId = $resolvedBot['parentBotId'];
893
894 // Rename the keys of the atts into camelCase to match the internal params system.
895 $atts = array_map( function ( $key, $value ) {
896 $key = str_replace( '_', ' ', $key );
897 $key = ucwords( $key );
898 $key = str_replace( ' ', '', $key );
899 $key = lcfirst( $key );
900 return [ $key => $value ];
901 }, array_keys( $atts ), $atts );
902 $atts = array_merge( ...$atts );
903
904 if ( !empty( $parentBotId ) ) {
905 $atts['parentBotId'] = $parentBotId;
906 }
907
908 $frontParams = [];
909 // Define text parameters that need sanitization (excluding those that support HTML)
910 $textParams = ['aiName', 'userName', 'guestName', 'textSend', 'textClear', 'textInputPlaceholder',
911 'startSentence', 'iconText', 'iconAlt', 'headerSubtitle'];
912 // Parameters that support HTML content
913 $htmlParams = ['textCompliance'];
914
915 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
916 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
917 if ( isset( $atts[$param] ) ) {
918 if ( $param === 'localMemory' ) {
919 $frontParams[$param] = $atts[$param] === 'true';
920 }
921 else if ( in_array( $param, $textParams ) ) {
922 // Sanitize text parameters to prevent XSS
923 $frontParams[$param] = sanitize_text_field( $atts[$param] );
924 }
925 else if ( in_array( $param, $htmlParams ) ) {
926 // For HTML parameters, use wp_kses_post to allow safe HTML
927 $frontParams[$param] = wp_kses_post( $atts[$param] );
928 }
929 else {
930 $frontParams[$param] = $atts[$param];
931 }
932 }
933 // If not, let's use the chatbot's default values
934 else if ( isset( $chatbot[$param] ) ) {
935 $frontParams[$param] = $chatbot[$param];
936 }
937
938 // Apply the placeholders
939 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
940 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
941 }
942 }
943
944 // Server Params
945 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
946 // we are using the default or a specific chatbot.
947 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
948 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
949 $hasFrontOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_FRONT_PARAMS ) ) > 0;
950 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasFrontOverrides );
951
952 $serverParams = [];
953 if ( $hasOverrides ) {
954 // Server parameters don't need sanitization as they're processed server-side
955 // and not rendered in HTML. They may contain code, HTML, etc. for AI context.
956 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
957 if ( isset( $atts[$param] ) ) {
958 $serverParams[$param] = $atts[$param];
959 }
960 else {
961 // For custom chatbots, don't inherit embeddingsEnvId from the default chatbot
962 if ( $param === 'embeddingsEnvId' && !empty( $customId ) ) {
963 $serverParams[$param] = '';
964 }
965 else {
966 $serverParams[$param] = $chatbot[$param] ?? null;
967 }
968 }
969 }
970 }
971
972 // Front Params
973 $frontSystem = $this->build_front_params( $botId, $customId );
974
975 // Clean Params
976 $frontParams = $this->clean_params( $frontParams );
977 $frontSystem = $this->clean_params( $frontSystem );
978 $serverParams = $this->clean_params( $serverParams );
979
980 // Server-side: Keep the System Params
981 if ( $hasOverrides ) {
982 if ( empty( $customId ) ) {
983 $customId = md5( json_encode( $serverParams ) );
984 $frontSystem['customId'] = $customId;
985 }
986 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
987 }
988
989 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
990 $filterParams = [
991 'step' => 'init',
992 'botId' => $botId,
993 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
994 ];
995 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
996 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
997 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
998 $frontSystem['actions'] = $this->sanitize_actions( $actions );
999 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
1000 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
1001
1002 // Client-side: Prepare JSON for Front Params and System Params
1003 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1004 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1005 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1006 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1007 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
1008
1009 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
1010
1011 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1012 }
1013
1014 public function chatbot_discussions( $atts ) {
1015 $atts = empty( $atts ) ? [] : $atts;
1016
1017 // Resolve the bot info
1018 $resolvedBot = $this->resolveBotInfo( $atts );
1019 if ( isset( $resolvedBot['error'] ) ) {
1020 return $resolvedBot['error'];
1021 }
1022 $chatbot = $resolvedBot['chatbot'];
1023 $botId = $resolvedBot['botId'];
1024 $customId = $resolvedBot['customId'];
1025
1026 // Rename the keys of the atts into camelCase to match the internal params system.
1027 $atts = array_map( function ( $key, $value ) {
1028 $key = str_replace( '_', ' ', $key );
1029 $key = ucwords( $key );
1030 $key = str_replace( ' ', '', $key );
1031 $key = lcfirst( $key );
1032 return [ $key => $value ];
1033 }, array_keys( $atts ), $atts );
1034 $atts = array_merge( ...$atts );
1035
1036 // Front Params
1037 $frontParams = [];
1038 // All discussion params are text params that need sanitization
1039 $textParams = ['textNewChat'];
1040
1041 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
1042 if ( isset( $atts[$param] ) ) {
1043 // Sanitize text parameters
1044 $frontParams[$param] = in_array( $param, $textParams ) ? sanitize_text_field( $atts[$param] ) : $atts[$param];
1045 }
1046 else if ( isset( $chatbot[$param] ) ) {
1047 $frontParams[$param] = $chatbot[$param];
1048 }
1049 }
1050
1051 // Server Params
1052 $serverParams = [];
1053 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
1054 if ( isset( $atts[$param] ) ) {
1055 $serverParams[$param] = $atts[$param];
1056 }
1057 }
1058
1059 // Front System
1060 $frontSystem = $this->build_front_params( $botId, $customId );
1061 // Get refresh interval from settings
1062 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
1063 if ( $refresh_interval === 'Never' ) {
1064 $frontSystem['refreshInterval'] = 0;
1065 }
1066 elseif ( $refresh_interval === 'Manual' ) {
1067 $frontSystem['refreshInterval'] = -1;
1068 }
1069 elseif ( is_numeric( $refresh_interval ) ) {
1070 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
1071 }
1072 else {
1073 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
1074 }
1075 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
1076
1077 // Get paging setting
1078 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
1079 if ( $paging_option === 'None' ) {
1080 $frontSystem['paging'] = 0; // No pagination
1081 }
1082 else {
1083 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
1084 }
1085
1086 // Get metadata settings
1087 $frontSystem['metadata'] = [
1088 'enabled' => $this->core->get_option( 'chatbot_discussions_metadata_enabled' ),
1089 'startDate' => $this->core->get_option( 'chatbot_discussions_metadata_start_date' ),
1090 'lastUpdate' => $this->core->get_option( 'chatbot_discussions_metadata_last_update' ),
1091 'messageCount' => $this->core->get_option( 'chatbot_discussions_metadata_message_count' )
1092 ];
1093
1094 // Clean Params
1095 $frontParams = $this->clean_params( $frontParams );
1096 $frontSystem = $this->clean_params( $frontSystem );
1097 $serverParams = $this->clean_params( $serverParams );
1098
1099 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1100 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1101 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1102 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1103
1104 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1105 }
1106
1107 public function clean_params( &$params ) {
1108 foreach ( $params as $param => $value ) {
1109 if ( $param === 'restNonce' ) {
1110 continue;
1111 }
1112 // Skip only if value is null or an array - but not if it's false or 0
1113 if ( is_null( $value ) || is_array( $value ) ) {
1114 continue;
1115 }
1116 // Handle empty strings
1117 if ( $value === '' ) {
1118 continue;
1119 }
1120 $lowerCaseValue = is_string( $value ) ? strtolower( $value ) : '';
1121 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
1122 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
1123 }
1124 else if ( is_numeric( $value ) ) {
1125 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
1126 }
1127 }
1128 return $params;
1129 }
1130
1131 }
1132