PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.6
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
1105 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
490 // Map 'environment' field to 'envId' for compatibility
491 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
492 $newParams['envId'] = $newParams['environment'];
493 }
494
495 $params = apply_filters( 'mwai_chatbot_params', $newParams );
496 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
497
498
499 // Debug log for embeddings
500 if ( !empty( $params['embeddingsEnvId'] ) ) {
501 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
502 } else {
503 // Log all params to debug
504 $paramKeys = array_keys( $params );
505 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
506 }
507
508 $query->inject_params( $params );
509 }
510 else {
511 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
512 new Meow_MWAI_Query_Text( $newMessage, 1024 );
513
514 // Handle Params
515 $newParams = [];
516 foreach ( $chatbot as $key => $value ) {
517 $newParams[$key] = $value;
518 }
519 if ( is_array( $params ) ) {
520 foreach ( $params as $key => $value ) {
521 $newParams[$key] = $value;
522 }
523 }
524
525 // Map 'environment' field to 'envId' for compatibility
526 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
527 $newParams['envId'] = $newParams['environment'];
528 }
529
530 $params = apply_filters( 'mwai_chatbot_params', $newParams );
531 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
532
533
534 // Debug log for embeddings
535 if ( !empty( $params['embeddingsEnvId'] ) ) {
536 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
537 } else {
538 // Log all params to debug
539 $paramKeys = array_keys( $params );
540 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
541 }
542
543 $query->inject_params( $params );
544
545 $storeId = null;
546 if ( $mode === 'assistant' ) {
547 $chatId = $params['chatId'] ?? null;
548 if ( !empty( $chatId ) ) {
549 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
550 if ( isset( $discussion['storeId'] ) ) {
551 $storeId = $discussion['storeId'];
552 $query->setStoreId( $storeId );
553 }
554 }
555 }
556
557 // Support for Uploaded Image
558 if ( !empty( $newFileId ) ) {
559
560 // Get extension and mime type
561 $isImage = $this->core->files->is_image( $newFileId );
562
563 if ( $mode === 'assistant' && !$isImage ) {
564 $url = $this->core->files->get_path( $newFileId );
565 $data = $this->core->files->get_data( $newFileId );
566 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
567 $filename = basename( $url );
568
569 // Upload the file
570 $file = $openai->upload_file( $filename, $data, 'assistants' );
571
572 // Create a store
573 if ( empty( $storeId ) ) {
574 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
575 if ( !empty( $query->chatId ) ) {
576 $chatbotName .= '_' . $query->chatId;
577 }
578 $metadata = [];
579 if ( !empty( $chatbot['assistantId'] ) ) {
580 $metadata['assistantId'] = $chatbot['assistantId'];
581 }
582 if ( !empty( $query->chatId ) ) {
583 $metadata['chatId'] = $query->chatId;
584 }
585 $expiry = $this->core->get_option( 'image_expires' );
586 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
587 $query->setStoreId( $storeId );
588 }
589
590 // Add the file to the store
591 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
592
593 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
594 $openAiRefId = $file['id'];
595 $internalFileId = $this->core->files->get_id_from_refId( $newFileId );
596 $this->core->files->update_refId( $internalFileId, $openAiRefId );
597 $this->core->files->update_envId( $internalFileId, $query->envId );
598 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
599 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
600 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
601 $newFileId = $openAiRefId;
602 $scope = $params['fileSearch'];
603 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
604 $id = $this->core->files->get_id_from_refId( $newFileId );
605 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
606 }
607 }
608 else {
609 $url = $this->core->files->get_url( $newFileId );
610 $mimeType = $this->core->files->get_mime_type( $newFileId );
611 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
612 $purposeType = $isIMG ? 'vision' : 'files';
613 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, $purposeType, $mimeType ) );
614 $fileId = $this->core->files->get_id_from_refId( $newFileId );
615 $this->core->files->update_envId( $fileId, $query->envId );
616 $this->core->files->update_purpose( $fileId, $purposeType );
617 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
618 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
619 }
620 }
621
622 // Takeover
623 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
624 if ( !empty( $takeoverAnswer ) ) {
625 $rawText = apply_filters( 'mwai_chatbot_reply', $takeoverAnswer, $query, $params, [] );
626 return [
627 'reply' => $rawText,
628 'chatId' => $this->core->fix_chat_id( $query, $params ),
629 'images' => null,
630 'actions' => [],
631 'usage' => null
632 ];
633 }
634
635 // Moderation
636 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
637 $this->core->get_option( 'shortcode_chat_moderation' );
638 if ( $moderationEnabled ) {
639 global $mwai;
640 $isFlagged = $mwai->moderationCheck( $query->get_message() );
641 if ( $isFlagged ) {
642 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
643 }
644 }
645
646 // Setup streaming if enabled (before embeddings to capture those events)
647 $streamCallback = null;
648 $debugEvents = [];
649
650 if ( $stream ) {
651 $streamCallback = function ( $reply ) use ( $query ) {
652 // Support both legacy string data and new Event objects
653 if ( is_string( $reply ) ) {
654 $this->core->stream_push( [ 'type' => 'live', 'data' => $reply ], $query );
655 }
656 else {
657 $this->core->stream_push( $reply, $query );
658 }
659 };
660 if ( headers_sent( $filename, $linenum ) ) {
661 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
662 }
663 header( 'Cache-Control: no-cache' );
664 header( 'Content-Type: text/event-stream' );
665 // This is useful to disable buffering in nginx through headers.
666 header( 'X-Accel-Buffering: no' );
667 ob_implicit_flush( true );
668 if ( ob_get_level() > 0 ) {
669 ob_end_flush();
670 }
671 }
672 else if ( $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ) ) {
673 // For non-streaming debug mode, collect events
674 $streamCallback = function ( $event ) use ( &$debugEvents ) {
675 if ( is_object( $event ) && method_exists( $event, 'toArray' ) ) {
676 $debugEvents[] = $event->toArray();
677 }
678 };
679 }
680
681 // Awareness & Embeddings
682 $context = $this->core->retrieve_context( $params, $query, $streamCallback );
683 if ( !empty( $context ) ) {
684 $query->set_context( $context['content'] );
685 }
686
687 // Function Aware
688 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
689 }
690
691 // Process Query
692
693 $reply = $this->core->run_query( $query, $streamCallback, true );
694 $rawText = $reply->result;
695 $extra = [];
696 if ( $context ) {
697 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
698 }
699 // Store response ID for Responses API stateful conversations
700 // CRITICAL: Must store even when function calls are present
701 // This enables the feedback query to use previous_response_id
702 if ( !empty( $reply->id ) ) {
703 $extra['responseId'] = $reply->id;
704 $extra['responseDate'] = gmdate( 'Y-m-d H:i:s' ); // Track age for 30-day expiry
705 }
706 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
707
708 // Integrity Check: We need to store the checksum of the messages sent by the client.
709 $stored_messages = $client_messages;
710 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
711 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
712 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
713 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
714
715 // Actions
716 $actions = [];
717 if ( $reply->needClientActions ) {
718 foreach ( $reply->needClientActions as $action ) {
719 $actions[] = [
720 'type' => 'function',
721 'data' => [
722 'name' => $action['function']->name,
723 'args' => $action['arguments']
724 ]
725 ];
726 }
727 }
728
729 $restRes = [
730 'reply' => $rawText,
731 'chatId' => $this->core->fix_chat_id( $query, $params ),
732 'images' => $reply->get_type() === 'images' ? $reply->results : null,
733 'actions' => $actions,
734 'usage' => $reply->usage
735 ];
736
737 // Add debug events if collected
738 if ( !empty( $debugEvents ) ) {
739 $restRes['debugEvents'] = $debugEvents;
740 }
741
742 // Add response ID if available (for Responses API)
743 if ( !empty( $reply->id ) ) {
744 $restRes['responseId'] = $reply->id;
745 }
746
747 // Process Reply
748 if ( $stream ) {
749 $final_res = $this->build_final_res(
750 $botId,
751 $newMessage,
752 $newFileId,
753 $params,
754 $restRes['reply'],
755 $restRes['images'],
756 $restRes['actions'],
757 $restRes['usage'],
758 $restRes['responseId'] ?? null
759 );
760 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
761 die();
762 }
763 else {
764 return $restRes;
765 }
766
767 }
768 catch ( Exception $e ) {
769 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
770 if ( $stream ) {
771 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
772 die();
773 }
774 else {
775 throw $e;
776 }
777 }
778 }
779
780 public function inject_chat() {
781 $params = $this->core->get_chatbot( $this->siteWideChatId );
782 $clean_params = [];
783 if ( !empty( $params ) ) {
784 $clean_params['window'] = true;
785 $clean_params['id'] = $this->siteWideChatId;
786 echo $this->chat_shortcode( $clean_params );
787 }
788 return null;
789 }
790
791 public function build_front_params( $botId, $customId ) {
792 $frontSystem = [
793 'botId' => $customId ? null : sanitize_text_field( $botId ),
794 'customId' => sanitize_text_field( $customId ),
795 'userData' => $this->core->get_user_data(),
796 'sessionId' => $this->core->get_session_id(),
797 // IMPORTANT: REST nonce handling differs by user state:
798 // - Logged-in users: get_nonce() returns a user-specific nonce created in current session context
799 // - Logged-out users: get_nonce() returns null, they'll fetch via /start_session endpoint
800 // This prevents rest_cookie_invalid_nonce errors for logged-in users by ensuring the nonce
801 // matches their authentication context from the start.
802 'restNonce' => $this->core->get_nonce(),
803 'contextId' => get_the_ID(),
804 'pluginUrl' => MWAI_URL,
805 'restUrl' => untrailingslashit( get_rest_url() ),
806 'stream' => $this->core->get_option( 'ai_streaming' ),
807 'debugMode' => $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ),
808 'eventLogs' => $this->core->get_option( 'event_logs' ),
809 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
810 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
811 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
812 'virtual_keyboard_fix' => $this->core->get_option( 'virtual_keyboard_fix' )
813 ];
814 return $frontSystem;
815 }
816
817 public function resolveBotInfo( &$atts ) {
818 $chatbot = null;
819 $botId = $atts['id'] ?? null;
820 $customId = $atts['custom_id'] ?? null;
821 $parentBotId = null;
822
823 if ( !$botId && !$customId ) {
824 $botId = 'default';
825 }
826 if ( $botId ) {
827 $chatbot = $this->core->get_chatbot( $botId );
828 if ( !$chatbot ) {
829 $botId = $botId ?: 'N/A';
830 $safe_botId = esc_html( $botId );
831 return [
832 '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'.",
833 ];
834 }
835 }
836 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
837 if ( !empty( $customId ) ) {
838 if ( $botId !== null ) {
839 $parentBotId = $botId;
840 $botId = null;
841 }
842 }
843 unset( $atts['id'] );
844 return [
845 'chatbot' => $chatbot,
846 'botId' => $botId,
847 'customId' => $customId,
848 'parentBotId' => $parentBotId
849 ];
850 }
851
852 public function chat_shortcode( $atts ) {
853 $atts = empty( $atts ) ? [] : $atts;
854
855 foreach ( $atts as $key => $value ) {
856 $atts[ $key ] = urldecode( $value );
857 }
858
859 // Let the user override the chatbot params
860 $atts = apply_filters( 'mwai_chatbot_params', $atts );
861
862 // Resolve the bot info
863 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
864 if ( isset( $resolvedBot['error'] ) ) {
865 return $resolvedBot['error'];
866 }
867 $chatbot = $resolvedBot['chatbot'];
868 $botId = $resolvedBot['botId'];
869 $customId = $resolvedBot['customId'];
870 $parentBotId = $resolvedBot['parentBotId'];
871
872 // Rename the keys of the atts into camelCase to match the internal params system.
873 $atts = array_map( function ( $key, $value ) {
874 $key = str_replace( '_', ' ', $key );
875 $key = ucwords( $key );
876 $key = str_replace( ' ', '', $key );
877 $key = lcfirst( $key );
878 return [ $key => $value ];
879 }, array_keys( $atts ), $atts );
880 $atts = array_merge( ...$atts );
881
882 if ( !empty( $parentBotId ) ) {
883 $atts['parentBotId'] = $parentBotId;
884 }
885
886 $frontParams = [];
887 // Define text parameters that need sanitization (excluding those that support HTML)
888 $textParams = ['aiName', 'userName', 'guestName', 'textSend', 'textClear', 'textInputPlaceholder',
889 'startSentence', 'iconText', 'iconAlt', 'headerSubtitle'];
890 // Parameters that support HTML content
891 $htmlParams = ['textCompliance'];
892
893 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
894 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
895 if ( isset( $atts[$param] ) ) {
896 if ( $param === 'localMemory' ) {
897 $frontParams[$param] = $atts[$param] === 'true';
898 }
899 else if ( in_array( $param, $textParams ) ) {
900 // Sanitize text parameters to prevent XSS
901 $frontParams[$param] = sanitize_text_field( $atts[$param] );
902 }
903 else if ( in_array( $param, $htmlParams ) ) {
904 // For HTML parameters, use wp_kses_post to allow safe HTML
905 $frontParams[$param] = wp_kses_post( $atts[$param] );
906 }
907 else {
908 $frontParams[$param] = $atts[$param];
909 }
910 }
911 // If not, let's use the chatbot's default values
912 else if ( isset( $chatbot[$param] ) ) {
913 $frontParams[$param] = $chatbot[$param];
914 }
915
916 // Apply the placeholders
917 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
918 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
919 }
920 }
921
922 // Server Params
923 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
924 // we are using the default or a specific chatbot.
925 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
926 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
927 $hasFrontOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_FRONT_PARAMS ) ) > 0;
928 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasFrontOverrides );
929
930 $serverParams = [];
931 if ( $hasOverrides ) {
932 // Server parameters don't need sanitization as they're processed server-side
933 // and not rendered in HTML. They may contain code, HTML, etc. for AI context.
934 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
935 if ( isset( $atts[$param] ) ) {
936 $serverParams[$param] = $atts[$param];
937 }
938 else {
939 // For custom chatbots, don't inherit embeddingsEnvId from the default chatbot
940 if ( $param === 'embeddingsEnvId' && !empty( $customId ) ) {
941 $serverParams[$param] = '';
942 }
943 else {
944 $serverParams[$param] = $chatbot[$param] ?? null;
945 }
946 }
947 }
948 }
949
950 // Front Params
951 $frontSystem = $this->build_front_params( $botId, $customId );
952
953 // Clean Params
954 $frontParams = $this->clean_params( $frontParams );
955 $frontSystem = $this->clean_params( $frontSystem );
956 $serverParams = $this->clean_params( $serverParams );
957
958 // Server-side: Keep the System Params
959 if ( $hasOverrides ) {
960 if ( empty( $customId ) ) {
961 $customId = md5( json_encode( $serverParams ) );
962 $frontSystem['customId'] = $customId;
963 }
964 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
965 }
966
967 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
968 $filterParams = [
969 'step' => 'init',
970 'botId' => $botId,
971 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
972 ];
973 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
974 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
975 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
976 $frontSystem['actions'] = $this->sanitize_actions( $actions );
977 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
978 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
979
980 // Client-side: Prepare JSON for Front Params and System Params
981 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
982 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
983 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
984 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
985 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
986
987 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
988
989 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
990 }
991
992 public function chatbot_discussions( $atts ) {
993 $atts = empty( $atts ) ? [] : $atts;
994
995 // Resolve the bot info
996 $resolvedBot = $this->resolveBotInfo( $atts );
997 if ( isset( $resolvedBot['error'] ) ) {
998 return $resolvedBot['error'];
999 }
1000 $chatbot = $resolvedBot['chatbot'];
1001 $botId = $resolvedBot['botId'];
1002 $customId = $resolvedBot['customId'];
1003
1004 // Rename the keys of the atts into camelCase to match the internal params system.
1005 $atts = array_map( function ( $key, $value ) {
1006 $key = str_replace( '_', ' ', $key );
1007 $key = ucwords( $key );
1008 $key = str_replace( ' ', '', $key );
1009 $key = lcfirst( $key );
1010 return [ $key => $value ];
1011 }, array_keys( $atts ), $atts );
1012 $atts = array_merge( ...$atts );
1013
1014 // Front Params
1015 $frontParams = [];
1016 // All discussion params are text params that need sanitization
1017 $textParams = ['textNewChat'];
1018
1019 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
1020 if ( isset( $atts[$param] ) ) {
1021 // Sanitize text parameters
1022 $frontParams[$param] = in_array( $param, $textParams ) ? sanitize_text_field( $atts[$param] ) : $atts[$param];
1023 }
1024 else if ( isset( $chatbot[$param] ) ) {
1025 $frontParams[$param] = $chatbot[$param];
1026 }
1027 }
1028
1029 // Server Params
1030 $serverParams = [];
1031 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
1032 if ( isset( $atts[$param] ) ) {
1033 $serverParams[$param] = $atts[$param];
1034 }
1035 }
1036
1037 // Front System
1038 $frontSystem = $this->build_front_params( $botId, $customId );
1039 // Get refresh interval from settings
1040 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
1041 if ( $refresh_interval === 'Never' ) {
1042 $frontSystem['refreshInterval'] = 0;
1043 }
1044 elseif ( $refresh_interval === 'Manual' ) {
1045 $frontSystem['refreshInterval'] = -1;
1046 }
1047 elseif ( is_numeric( $refresh_interval ) ) {
1048 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
1049 }
1050 else {
1051 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
1052 }
1053 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
1054
1055 // Get paging setting
1056 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
1057 if ( $paging_option === 'None' ) {
1058 $frontSystem['paging'] = 0; // No pagination
1059 }
1060 else {
1061 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
1062 }
1063
1064 // Get metadata settings
1065 $frontSystem['metadata'] = [
1066 'enabled' => $this->core->get_option( 'chatbot_discussions_metadata_enabled' ),
1067 'startDate' => $this->core->get_option( 'chatbot_discussions_metadata_start_date' ),
1068 'lastUpdate' => $this->core->get_option( 'chatbot_discussions_metadata_last_update' ),
1069 'messageCount' => $this->core->get_option( 'chatbot_discussions_metadata_message_count' )
1070 ];
1071
1072 // Clean Params
1073 $frontParams = $this->clean_params( $frontParams );
1074 $frontSystem = $this->clean_params( $frontSystem );
1075 $serverParams = $this->clean_params( $serverParams );
1076
1077 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1078 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1079 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1080 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1081
1082 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1083 }
1084
1085 public function clean_params( &$params ) {
1086 foreach ( $params as $param => $value ) {
1087 if ( $param === 'restNonce' ) {
1088 continue;
1089 }
1090 if ( empty( $value ) || is_array( $value ) ) {
1091 continue;
1092 }
1093 $lowerCaseValue = strtolower( $value );
1094 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
1095 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
1096 }
1097 else if ( is_numeric( $value ) ) {
1098 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
1099 }
1100 }
1101 return $params;
1102 }
1103
1104 }
1105