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