PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.4
3.5.9 3.5.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 1 year ago gdpr.php 1 year ago search.php 1 year ago security.php 1 year ago tasks.php 1 year ago wand.php 1 year ago
chatbot.php
1125 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', '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
202 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId ) ) {
203 return $this->create_rest_response( [
204 'success' => false,
205 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
206 ], 403 );
207 }
208
209 try {
210 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream );
211 $final_res = $this->build_final_res(
212 $botId,
213 $newMessage,
214 $newFileId,
215 $params,
216 $data['reply'],
217 $data['images'],
218 $data['actions'],
219 $data['usage'],
220 $data['responseId'] ?? null
221 );
222 return $this->create_rest_response( $final_res, 200 );
223 }
224 catch ( Exception $e ) {
225 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
226 return $this->create_rest_response( [
227 'success' => false,
228 'message' => $message
229 ], 500 );
230 }
231 }
232
233 private function sanitize_items( $items, $supported_types, $type_name ) {
234 if ( empty( $items ) ) {
235 return $items;
236 }
237 $sanitized_items = [];
238 foreach ( $items as $item ) {
239 if ( isset( $supported_types[$item['type']] ) ) {
240 $is_valid = true;
241 foreach ( $supported_types[$item['type']] as $param ) {
242 if ( !isset( $item['data'][$param] ) ) {
243 $is_valid = false;
244 Meow_MWAI_Logging::warn( "The query was rejected - missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
245 break;
246 }
247 }
248 if ( $is_valid ) {
249 $sanitized_items[] = $item;
250 }
251 }
252 else {
253 Meow_MWAI_Logging::warn( "The query was rejected - unsupported {$type_name} type: {$item['type']}." );
254 }
255 }
256 return $sanitized_items;
257 }
258
259 public function sanitize_actions( $actions ) {
260 $supported_action_types = [
261 'function' => ['name', 'args'],
262 'javascript' => ['snippet'],
263 ];
264 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
265 }
266
267 public function sanitize_blocks( $blocks ) {
268 $supported_block_types = [
269 'content' => ['html'],
270 ];
271 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
272 }
273
274 public function sanitize_shortcuts( $shortcuts ) {
275 $supported_shortcut_types = [
276 'message' => ['label', 'message'],
277 'callback' => ['label', 'onClick'],
278 ];
279 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
280 }
281
282 #region Messages Integrity Check
283
284 public function messages_integrity_diff( $messages1, $messages2 ) {
285 // Ensure both parameters are arrays
286 if ( !is_array( $messages1 ) ) {
287 $messages1 = [];
288 }
289 if ( !is_array( $messages2 ) ) {
290 $messages2 = [];
291 }
292
293 // Collect messages with role not 'user' from messages1
294 $messagesList1 = [];
295 foreach ( $messages1 as $msg ) {
296 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
297 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
298 if ( $role && $role != 'user' ) {
299 $messageData = [ 'role' => $role, 'content' => $content ];
300 $messagesList1[] = $messageData;
301 }
302 }
303
304 // Collect messages with role not 'user' from messages2
305 $messagesList2 = [];
306 foreach ( $messages2 as $msg ) {
307 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
308 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
309 if ( $role && $role != 'user' ) {
310 $messageData = [ 'role' => $role, 'content' => $content ];
311 $messagesList2[] = $messageData;
312 }
313 }
314
315 // Count occurrences of each message in messagesList1
316 $counts1 = [];
317 foreach ( $messagesList1 as $msg ) {
318 $key = serialize( $msg );
319 if ( isset( $counts1[ $key ] ) ) {
320 $counts1[ $key ]++;
321 }
322 else {
323 $counts1[ $key ] = 1;
324 }
325 }
326
327 // Count occurrences of each message in messagesList2
328 $counts2 = [];
329 foreach ( $messagesList2 as $msg ) {
330 $key = serialize( $msg );
331 if ( isset( $counts2[ $key ] ) ) {
332 $counts2[ $key ]++;
333 }
334 else {
335 $counts2[ $key ] = 1;
336 }
337 }
338
339 // Compare counts to find unmatched messages
340 $all_keys = array_unique( array_merge( array_keys( $counts1 ), array_keys( $counts2 ) ) );
341
342 $diffs = [];
343 foreach ( $all_keys as $key ) {
344 $count1 = isset( $counts1[ $key ] ) ? $counts1[ $key ] : 0;
345 $count2 = isset( $counts2[ $key ] ) ? $counts2[ $key ] : 0;
346 if ( $count1 != $count2 ) {
347 $message = unserialize( $key );
348 $diffs[] = [
349 'message' => $message,
350 'count_in_messages1' => $count1,
351 'count_in_messages2' => $count2
352 ];
353 }
354 }
355
356 return $diffs;
357 }
358
359 private function calculate_messages_checksum( $messages ) {
360 $messages_to_hash = [];
361 foreach ( $messages as $msg ) {
362 $role = is_array( $msg ) ? ( $msg['role'] ?? '' ) : ( is_object( $msg ) ? ( $msg->role ?? '' ) : '' );
363 $content = is_array( $msg ) ? ( $msg['content'] ?? '' ) : ( is_object( $msg ) ? ( $msg->content ?? '' ) : '' );
364 if ( in_array( $role, ['assistant', 'system'] ) ) {
365 $messages_to_hash[] = [ 'role' => $role, 'content' => $content ];
366 }
367 }
368 return md5( json_encode( $messages_to_hash ) );
369 }
370
371 #endregion
372
373 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false ) {
374 try {
375 $chatbot = null;
376 $customId = $params['customId'] ?? null;
377
378 // Custom Chatbot
379 if ( $customId ) {
380 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
381 }
382 // Registered Chatbot
383 if ( !$chatbot && $botId ) {
384 $chatbot = $this->core->get_chatbot( $botId );
385 }
386
387 if ( !$chatbot ) {
388 Meow_MWAI_Logging::warn( 'The query was rejected - no chatbot was found.' );
389 throw new Exception( 'Sorry, your query has been rejected.' );
390 }
391
392 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
393 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int) $textInputMaxLength ) {
394 Meow_MWAI_Logging::warn( 'The query was rejected - message was too long.' );
395 throw new Exception( 'Sorry, your query has been rejected.' );
396 }
397
398 // We need to check the integrity of the messages sent by the client.
399 // This is important to ensure that the messages are not tampered with.
400
401 // Messages Integrity Check with Checksums
402 $chatId = $params['chatId'] ?? 'default';
403 $checksum_key = 'mwai_chatbot_checksum_' . $chatId;
404 $stored_checksum = get_transient( $checksum_key );
405 $client_messages = $params['messages'] ?? [];
406 $client_checksum = $this->calculate_messages_checksum( $client_messages );
407 if ( $stored_checksum && $stored_checksum !== $client_checksum ) {
408 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.' );
409 }
410
411 // Messages Integrity Check with Discussions
412 if ( $this->core->get_option( 'chatbot_discussions' ) && isset( $params['chatId'] ) ) {
413 $discussion = $this->core->discussions->get_discussion( $botId ? $botId : $customId, $params['chatId'] );
414 if ( $discussion ) {
415 $messages = $discussion['messages'];
416 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
417 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
418 if ( count( $diffs ) > 0 ) {
419 Meow_MWAI_Logging::warn( "Integrity Check: It seems the messages in the discussion #{$discussion['id']} do not match the ones sent by the client." );
420 }
421
422 // Maintain conversation state for Responses API by loading previousResponseId
423 // This enables stateful conversations where only new messages are sent
424 if ( empty( $params['previousResponseId'] ) && !empty( $discussion['extra'] ) ) {
425 $extra = json_decode( $discussion['extra'], true );
426 if ( !empty( $extra['responseId'] ) ) {
427 // Response IDs expire after 30 days per OpenAI's policy
428 // Check if the stored response is still valid
429 $responseDate = !empty( $extra['responseDate'] ) ? strtotime( $extra['responseDate'] ) : 0;
430 $thirtyDaysAgo = time() - ( 30 * 24 * 60 * 60 );
431
432 if ( $responseDate > $thirtyDaysAgo ) {
433 // Use the stored response ID for stateful conversation
434 $params['previousResponseId'] = $extra['responseId'];
435 }
436 }
437 }
438 }
439 else {
440 // No discussion yet? We still need to check the startSentence.
441 $startSentence = isset( $chatbot['startSentence'] ) ? $chatbot['startSentence'] : null;
442 $messages = [];
443 if ( !empty( $startSentence ) ) {
444 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
445 }
446 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
447 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
448 if ( count( $diffs ) > 0 ) {
449 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 ) );
450 }
451 }
452 }
453
454 // Create QueryText
455 $context = null;
456 $streamCallback = null;
457 $mode = $chatbot['mode'] ?? 'chat';
458
459 if ( $mode === 'images' ) {
460 // If there's an uploaded file, use EditImage query instead
461 if ( !empty( $newFileId ) ) {
462 $query = new Meow_MWAI_Query_EditImage( $newMessage );
463
464 // Handle the uploaded image
465 $url = $this->core->files->get_url( $newFileId );
466 $mimeType = $this->core->files->get_mime_type( $newFileId );
467 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
468
469 if ( $isIMG ) {
470 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
471 $fileId = $this->core->files->get_id_from_refId( $newFileId );
472 $this->core->files->update_purpose( $fileId, 'vision' );
473 }
474 }
475 else {
476 $query = new Meow_MWAI_Query_Image( $newMessage );
477 }
478
479 // Handle Params
480 $newParams = [];
481 foreach ( $chatbot as $key => $value ) {
482 $newParams[$key] = $value;
483 }
484 if ( is_array( $params ) ) {
485 foreach ( $params as $key => $value ) {
486 $newParams[$key] = $value;
487 }
488 }
489 $params = apply_filters( 'mwai_chatbot_params', $newParams );
490 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
491
492 // For assistant mode, ensure we have the correct envId
493 if ( $mode === 'assistant' && !empty( $params['assistantId'] ) && empty( $params['envId'] ) ) {
494 // Find the environment that contains this assistant
495 $envs = $this->core->get_option( 'ai_envs' );
496 foreach ( $envs as $env ) {
497 if ( isset( $env['assistants'] ) ) {
498 foreach ( $env['assistants'] as $assistant ) {
499 if ( $assistant['id'] === $params['assistantId'] ) {
500 $params['envId'] = $env['id'];
501 Meow_MWAI_Logging::info( "Assistant mode: Setting envId to '{$env['id']}' for assistant '{$params['assistantId']}'" );
502 break 2;
503 }
504 }
505 }
506 }
507 }
508
509 // Debug log for embeddings
510 if ( !empty( $params['embeddingsEnvId'] ) ) {
511 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
512 } else {
513 // Log all params to debug
514 $paramKeys = array_keys( $params );
515 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
516 }
517
518 $query->inject_params( $params );
519 }
520 else {
521 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
522 new Meow_MWAI_Query_Text( $newMessage, 1024 );
523
524 // Handle Params
525 $newParams = [];
526 foreach ( $chatbot as $key => $value ) {
527 $newParams[$key] = $value;
528 }
529 if ( is_array( $params ) ) {
530 foreach ( $params as $key => $value ) {
531 $newParams[$key] = $value;
532 }
533 }
534 $params = apply_filters( 'mwai_chatbot_params', $newParams );
535 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
536
537 // For assistant mode, ensure we have the correct envId
538 if ( $mode === 'assistant' && !empty( $params['assistantId'] ) && empty( $params['envId'] ) ) {
539 // Find the environment that contains this assistant
540 $envs = $this->core->get_option( 'ai_envs' );
541 foreach ( $envs as $env ) {
542 if ( isset( $env['assistants'] ) ) {
543 foreach ( $env['assistants'] as $assistant ) {
544 if ( $assistant['id'] === $params['assistantId'] ) {
545 $params['envId'] = $env['id'];
546 Meow_MWAI_Logging::info( "Assistant mode: Setting envId to '{$env['id']}' for assistant '{$params['assistantId']}'" );
547 break 2;
548 }
549 }
550 }
551 }
552 }
553
554 // Debug log for embeddings
555 if ( !empty( $params['embeddingsEnvId'] ) ) {
556 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
557 } else {
558 // Log all params to debug
559 $paramKeys = array_keys( $params );
560 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
561 }
562
563 $query->inject_params( $params );
564
565 $storeId = null;
566 if ( $mode === 'assistant' ) {
567 $chatId = $params['chatId'] ?? null;
568 if ( !empty( $chatId ) ) {
569 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
570 if ( isset( $discussion['storeId'] ) ) {
571 $storeId = $discussion['storeId'];
572 $query->setStoreId( $storeId );
573 }
574 }
575 }
576
577 // Support for Uploaded Image
578 if ( !empty( $newFileId ) ) {
579
580 // Get extension and mime type
581 $isImage = $this->core->files->is_image( $newFileId );
582
583 if ( $mode === 'assistant' && !$isImage ) {
584 $url = $this->core->files->get_path( $newFileId );
585 $data = $this->core->files->get_data( $newFileId );
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( $newFileId );
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 $newFileId = $openAiRefId;
622 $scope = $params['fileSearch'];
623 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
624 $id = $this->core->files->get_id_from_refId( $newFileId );
625 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
626 }
627 }
628 else {
629 $url = $this->core->files->get_url( $newFileId );
630 $mimeType = $this->core->files->get_mime_type( $newFileId );
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( $newFileId );
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 if ( !empty( $customId ) ) {
858 if ( $botId !== null ) {
859 $parentBotId = $botId;
860 $botId = null;
861 }
862 }
863 unset( $atts['id'] );
864 return [
865 'chatbot' => $chatbot,
866 'botId' => $botId,
867 'customId' => $customId,
868 'parentBotId' => $parentBotId
869 ];
870 }
871
872 public function chat_shortcode( $atts ) {
873 $atts = empty( $atts ) ? [] : $atts;
874
875 foreach ( $atts as $key => $value ) {
876 $atts[ $key ] = urldecode( $value );
877 }
878
879 // Let the user override the chatbot params
880 $atts = apply_filters( 'mwai_chatbot_params', $atts );
881
882 // Resolve the bot info
883 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
884 if ( isset( $resolvedBot['error'] ) ) {
885 return $resolvedBot['error'];
886 }
887 $chatbot = $resolvedBot['chatbot'];
888 $botId = $resolvedBot['botId'];
889 $customId = $resolvedBot['customId'];
890 $parentBotId = $resolvedBot['parentBotId'];
891
892 // Rename the keys of the atts into camelCase to match the internal params system.
893 $atts = array_map( function ( $key, $value ) {
894 $key = str_replace( '_', ' ', $key );
895 $key = ucwords( $key );
896 $key = str_replace( ' ', '', $key );
897 $key = lcfirst( $key );
898 return [ $key => $value ];
899 }, array_keys( $atts ), $atts );
900 $atts = array_merge( ...$atts );
901
902 if ( !empty( $parentBotId ) ) {
903 $atts['parentBotId'] = $parentBotId;
904 }
905
906 $frontParams = [];
907 // Define text parameters that need sanitization (excluding those that support HTML)
908 $textParams = ['aiName', 'userName', 'guestName', 'textSend', 'textClear', 'textInputPlaceholder',
909 'startSentence', 'iconText', 'iconAlt', 'headerSubtitle'];
910 // Parameters that support HTML content
911 $htmlParams = ['textCompliance'];
912
913 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
914 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
915 if ( isset( $atts[$param] ) ) {
916 if ( $param === 'localMemory' ) {
917 $frontParams[$param] = $atts[$param] === 'true';
918 }
919 else if ( in_array( $param, $textParams ) ) {
920 // Sanitize text parameters to prevent XSS
921 $frontParams[$param] = sanitize_text_field( $atts[$param] );
922 }
923 else if ( in_array( $param, $htmlParams ) ) {
924 // For HTML parameters, use wp_kses_post to allow safe HTML
925 $frontParams[$param] = wp_kses_post( $atts[$param] );
926 }
927 else {
928 $frontParams[$param] = $atts[$param];
929 }
930 }
931 // If not, let's use the chatbot's default values
932 else if ( isset( $chatbot[$param] ) ) {
933 $frontParams[$param] = $chatbot[$param];
934 }
935
936 // Apply the placeholders
937 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
938 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
939 }
940 }
941
942 // Server Params
943 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
944 // we are using the default or a specific chatbot.
945 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
946 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
947 $hasFrontOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_FRONT_PARAMS ) ) > 0;
948 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasFrontOverrides );
949
950 $serverParams = [];
951 if ( $hasOverrides ) {
952 // Server parameters don't need sanitization as they're processed server-side
953 // and not rendered in HTML. They may contain code, HTML, etc. for AI context.
954 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
955 if ( isset( $atts[$param] ) ) {
956 $serverParams[$param] = $atts[$param];
957 }
958 else {
959 // For custom chatbots, don't inherit embeddingsEnvId from the default chatbot
960 if ( $param === 'embeddingsEnvId' && !empty( $customId ) ) {
961 $serverParams[$param] = '';
962 }
963 else {
964 $serverParams[$param] = $chatbot[$param] ?? null;
965 }
966 }
967 }
968 }
969
970 // Front Params
971 $frontSystem = $this->build_front_params( $botId, $customId );
972
973 // Clean Params
974 $frontParams = $this->clean_params( $frontParams );
975 $frontSystem = $this->clean_params( $frontSystem );
976 $serverParams = $this->clean_params( $serverParams );
977
978 // Server-side: Keep the System Params
979 if ( $hasOverrides ) {
980 if ( empty( $customId ) ) {
981 $customId = md5( json_encode( $serverParams ) );
982 $frontSystem['customId'] = $customId;
983 }
984 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
985 }
986
987 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
988 $filterParams = [
989 'step' => 'init',
990 'botId' => $botId,
991 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
992 ];
993 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
994 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
995 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
996 $frontSystem['actions'] = $this->sanitize_actions( $actions );
997 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
998 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
999
1000 // Client-side: Prepare JSON for Front Params and System Params
1001 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1002 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1003 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1004 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1005 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
1006
1007 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
1008
1009 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1010 }
1011
1012 public function chatbot_discussions( $atts ) {
1013 $atts = empty( $atts ) ? [] : $atts;
1014
1015 // Resolve the bot info
1016 $resolvedBot = $this->resolveBotInfo( $atts );
1017 if ( isset( $resolvedBot['error'] ) ) {
1018 return $resolvedBot['error'];
1019 }
1020 $chatbot = $resolvedBot['chatbot'];
1021 $botId = $resolvedBot['botId'];
1022 $customId = $resolvedBot['customId'];
1023
1024 // Rename the keys of the atts into camelCase to match the internal params system.
1025 $atts = array_map( function ( $key, $value ) {
1026 $key = str_replace( '_', ' ', $key );
1027 $key = ucwords( $key );
1028 $key = str_replace( ' ', '', $key );
1029 $key = lcfirst( $key );
1030 return [ $key => $value ];
1031 }, array_keys( $atts ), $atts );
1032 $atts = array_merge( ...$atts );
1033
1034 // Front Params
1035 $frontParams = [];
1036 // All discussion params are text params that need sanitization
1037 $textParams = ['textNewChat'];
1038
1039 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
1040 if ( isset( $atts[$param] ) ) {
1041 // Sanitize text parameters
1042 $frontParams[$param] = in_array( $param, $textParams ) ? sanitize_text_field( $atts[$param] ) : $atts[$param];
1043 }
1044 else if ( isset( $chatbot[$param] ) ) {
1045 $frontParams[$param] = $chatbot[$param];
1046 }
1047 }
1048
1049 // Server Params
1050 $serverParams = [];
1051 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
1052 if ( isset( $atts[$param] ) ) {
1053 $serverParams[$param] = $atts[$param];
1054 }
1055 }
1056
1057 // Front System
1058 $frontSystem = $this->build_front_params( $botId, $customId );
1059 // Get refresh interval from settings
1060 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
1061 if ( $refresh_interval === 'Never' ) {
1062 $frontSystem['refreshInterval'] = 0;
1063 }
1064 elseif ( $refresh_interval === 'Manual' ) {
1065 $frontSystem['refreshInterval'] = -1;
1066 }
1067 elseif ( is_numeric( $refresh_interval ) ) {
1068 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
1069 }
1070 else {
1071 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
1072 }
1073 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
1074
1075 // Get paging setting
1076 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
1077 if ( $paging_option === 'None' ) {
1078 $frontSystem['paging'] = 0; // No pagination
1079 }
1080 else {
1081 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
1082 }
1083
1084 // Get metadata settings
1085 $frontSystem['metadata'] = [
1086 'enabled' => $this->core->get_option( 'chatbot_discussions_metadata_enabled' ),
1087 'startDate' => $this->core->get_option( 'chatbot_discussions_metadata_start_date' ),
1088 'lastUpdate' => $this->core->get_option( 'chatbot_discussions_metadata_last_update' ),
1089 'messageCount' => $this->core->get_option( 'chatbot_discussions_metadata_message_count' )
1090 ];
1091
1092 // Clean Params
1093 $frontParams = $this->clean_params( $frontParams );
1094 $frontSystem = $this->clean_params( $frontSystem );
1095 $serverParams = $this->clean_params( $serverParams );
1096
1097 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1098 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1099 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1100 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1101
1102 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1103 }
1104
1105 public function clean_params( &$params ) {
1106 foreach ( $params as $param => $value ) {
1107 if ( $param === 'restNonce' ) {
1108 continue;
1109 }
1110 if ( empty( $value ) || is_array( $value ) ) {
1111 continue;
1112 }
1113 $lowerCaseValue = strtolower( $value );
1114 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
1115 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
1116 }
1117 else if ( is_numeric( $value ) ) {
1118 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
1119 }
1120 }
1121 return $params;
1122 }
1123
1124 }
1125