PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.2
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / modules / chatbot.php
ai-engine / classes / modules Last commit date
advisor.php 1 year ago chatbot.php 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
1075 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 if ( $this->core->get_option( 'debug_mode' ) ) {
98 error_log( '[MWAI] Token refresh: Nonce is 12-24 hours old, providing fresh token' );
99 }
100 }
101 }
102
103 // If the nonce has changed or should be refreshed, include the new one
104 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
105 $data['new_token'] = $current_nonce;
106
107 // Log if debug mode is enabled
108 if ( $this->core->get_option( 'debug_mode' ) ) {
109 error_log( '[MWAI] Token refresh: Sending new token in response' );
110 }
111 }
112
113 return new WP_REST_Response( $data, $status );
114 }
115
116 public function rest_api_init() {
117 register_rest_route( $this->namespace, '/chats/submit', [
118 'methods' => 'POST',
119 'callback' => [ $this, 'rest_chat' ],
120 'permission_callback' => [ $this->core, 'check_rest_nonce' ]
121 ] );
122 }
123
124 public function basics_security_check( $botId, $customId, $newMessage, $newFileId ) {
125 if ( !$botId && !$customId ) {
126 Meow_MWAI_Logging::warn( 'The query was rejected - no botId nor id was specified.' );
127 return false;
128 }
129
130 if ( $newFileId ) {
131 return true;
132 }
133
134 $length = strlen( empty( $newMessage ) ? '' : $newMessage );
135 if ( $length < 1 ) {
136 Meow_MWAI_Logging::warn( 'The query was rejected - message was too short.' );
137 return false;
138 }
139 return true;
140 }
141
142 public function build_final_res( $botId, $newMessage, $newFileId, $params, $reply, $images, $actions, $usage, $responseId = null ) {
143 $filterParams = [
144 'step' => 'reply',
145 'botId' => $botId,
146 'reply' => $reply,
147 'images' => $images,
148 'newMessage' => $newMessage,
149 'newFileId' => $newFileId,
150 'params' => $params,
151 'usage' => $usage,
152 'messages' => $params['messages'] ?? [],
153 'isNewConversation' => empty( $params['messages'] ) || count( $params['messages'] ) <= 1,
154 ];
155 $actions = apply_filters( 'mwai_chatbot_actions', $actions, $filterParams );
156 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
157 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
158 $actions = $this->sanitize_actions( $actions );
159 $blocks = $this->sanitize_blocks( $blocks );
160 $shortcuts = $this->sanitize_shortcuts( $shortcuts );
161 $result = [
162 'success' => true,
163 'reply' => $reply,
164 'images' => $images,
165 'actions' => $actions,
166 'shortcuts' => $shortcuts,
167 'blocks' => $blocks,
168 'usage' => $usage
169 ];
170
171 // Add response ID if available
172 if ( !empty( $responseId ) ) {
173 $result['responseId'] = $responseId;
174 }
175
176 // Check if token needs refresh
177 $current_nonce = $this->core->get_nonce( true );
178 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
179
180 $should_refresh = false;
181 if ( $request_nonce ) {
182 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
183 if ( $verify === 2 ) {
184 // Nonce is valid but was generated 12-24 hours ago
185 $should_refresh = true;
186 }
187 }
188
189 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
190 $result['new_token'] = $current_nonce;
191 }
192
193 return $result;
194 }
195
196 public function rest_chat( $request ) {
197 $params = $request->get_json_params();
198 $botId = $params['botId'] ?? null;
199 $customId = $params['customId'] ?? null;
200 $stream = $params['stream'] ?? false;
201 $newMessage = trim( $params['newMessage'] ?? '' );
202 $newFileId = $params['newFileId'] ?? null;
203
204 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId ) ) {
205 return $this->create_rest_response( [
206 'success' => false,
207 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
208 ], 403 );
209 }
210
211 try {
212 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream );
213 $final_res = $this->build_final_res(
214 $botId,
215 $newMessage,
216 $newFileId,
217 $params,
218 $data['reply'],
219 $data['images'],
220 $data['actions'],
221 $data['usage'],
222 $data['responseId'] ?? null
223 );
224 return $this->create_rest_response( $final_res, 200 );
225 }
226 catch ( Exception $e ) {
227 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
228 return $this->create_rest_response( [
229 'success' => false,
230 'message' => $message
231 ], 500 );
232 }
233 }
234
235 private function sanitize_items( $items, $supported_types, $type_name ) {
236 if ( empty( $items ) ) {
237 return $items;
238 }
239 $sanitized_items = [];
240 foreach ( $items as $item ) {
241 if ( isset( $supported_types[$item['type']] ) ) {
242 $is_valid = true;
243 foreach ( $supported_types[$item['type']] as $param ) {
244 if ( !isset( $item['data'][$param] ) ) {
245 $is_valid = false;
246 Meow_MWAI_Logging::warn( "The query was rejected - missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
247 break;
248 }
249 }
250 if ( $is_valid ) {
251 $sanitized_items[] = $item;
252 }
253 }
254 else {
255 Meow_MWAI_Logging::warn( "The query was rejected - unsupported {$type_name} type: {$item['type']}." );
256 }
257 }
258 return $sanitized_items;
259 }
260
261 public function sanitize_actions( $actions ) {
262 $supported_action_types = [
263 'function' => ['name', 'args'],
264 'javascript' => ['snippet'],
265 ];
266 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
267 }
268
269 public function sanitize_blocks( $blocks ) {
270 $supported_block_types = [
271 'content' => ['html'],
272 ];
273 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
274 }
275
276 public function sanitize_shortcuts( $shortcuts ) {
277 $supported_shortcut_types = [
278 'message' => ['label', 'message'],
279 'callback' => ['label', 'onClick'],
280 ];
281 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
282 }
283
284 #region Messages Integrity Check
285
286 public function messages_integrity_diff( $messages1, $messages2 ) {
287 // Ensure both parameters are arrays
288 if ( !is_array( $messages1 ) ) {
289 $messages1 = [];
290 }
291 if ( !is_array( $messages2 ) ) {
292 $messages2 = [];
293 }
294
295 // Collect messages with role not 'user' from messages1
296 $messagesList1 = [];
297 foreach ( $messages1 as $msg ) {
298 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
299 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
300 if ( $role && $role != 'user' ) {
301 $messageData = [ 'role' => $role, 'content' => $content ];
302 $messagesList1[] = $messageData;
303 }
304 }
305
306 // Collect messages with role not 'user' from messages2
307 $messagesList2 = [];
308 foreach ( $messages2 as $msg ) {
309 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
310 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
311 if ( $role && $role != 'user' ) {
312 $messageData = [ 'role' => $role, 'content' => $content ];
313 $messagesList2[] = $messageData;
314 }
315 }
316
317 // Count occurrences of each message in messagesList1
318 $counts1 = [];
319 foreach ( $messagesList1 as $msg ) {
320 $key = serialize( $msg );
321 if ( isset( $counts1[ $key ] ) ) {
322 $counts1[ $key ]++;
323 }
324 else {
325 $counts1[ $key ] = 1;
326 }
327 }
328
329 // Count occurrences of each message in messagesList2
330 $counts2 = [];
331 foreach ( $messagesList2 as $msg ) {
332 $key = serialize( $msg );
333 if ( isset( $counts2[ $key ] ) ) {
334 $counts2[ $key ]++;
335 }
336 else {
337 $counts2[ $key ] = 1;
338 }
339 }
340
341 // Compare counts to find unmatched messages
342 $all_keys = array_unique( array_merge( array_keys( $counts1 ), array_keys( $counts2 ) ) );
343
344 $diffs = [];
345 foreach ( $all_keys as $key ) {
346 $count1 = isset( $counts1[ $key ] ) ? $counts1[ $key ] : 0;
347 $count2 = isset( $counts2[ $key ] ) ? $counts2[ $key ] : 0;
348 if ( $count1 != $count2 ) {
349 $message = unserialize( $key );
350 $diffs[] = [
351 'message' => $message,
352 'count_in_messages1' => $count1,
353 'count_in_messages2' => $count2
354 ];
355 }
356 }
357
358 return $diffs;
359 }
360
361 private function calculate_messages_checksum( $messages ) {
362 $messages_to_hash = [];
363 foreach ( $messages as $msg ) {
364 $role = is_array( $msg ) ? ( $msg['role'] ?? '' ) : ( is_object( $msg ) ? ( $msg->role ?? '' ) : '' );
365 $content = is_array( $msg ) ? ( $msg['content'] ?? '' ) : ( is_object( $msg ) ? ( $msg->content ?? '' ) : '' );
366 if ( in_array( $role, ['assistant', 'system'] ) ) {
367 $messages_to_hash[] = [ 'role' => $role, 'content' => $content ];
368 }
369 }
370 return md5( json_encode( $messages_to_hash ) );
371 }
372
373 #endregion
374
375 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false ) {
376 try {
377 $chatbot = null;
378 $customId = $params['customId'] ?? null;
379
380 // Custom Chatbot
381 if ( $customId ) {
382 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
383 }
384 // Registered Chatbot
385 if ( !$chatbot && $botId ) {
386 $chatbot = $this->core->get_chatbot( $botId );
387 }
388
389 if ( !$chatbot ) {
390 Meow_MWAI_Logging::warn( 'The query was rejected - no chatbot was found.' );
391 throw new Exception( 'Sorry, your query has been rejected.' );
392 }
393
394 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
395 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int) $textInputMaxLength ) {
396 Meow_MWAI_Logging::warn( 'The query was rejected - message was too long.' );
397 throw new Exception( 'Sorry, your query has been rejected.' );
398 }
399
400 // We need to check the integrity of the messages sent by the client.
401 // This is important to ensure that the messages are not tampered with.
402
403 // Messages Integrity Check with Checksums
404 $chatId = $params['chatId'] ?? 'default';
405 $checksum_key = 'mwai_chatbot_checksum_' . $chatId;
406 $stored_checksum = get_transient( $checksum_key );
407 $client_messages = $params['messages'] ?? [];
408 $client_checksum = $this->calculate_messages_checksum( $client_messages );
409 if ( $stored_checksum && $stored_checksum !== $client_checksum ) {
410 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.' );
411 }
412
413 // Messages Integrity Check with Discussions
414 if ( $this->core->get_option( 'chatbot_discussions' ) && isset( $params['chatId'] ) ) {
415 $discussion = $this->core->discussions->get_discussion( $botId ? $botId : $customId, $params['chatId'] );
416 if ( $discussion ) {
417 $messages = $discussion['messages'];
418 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
419 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
420 if ( count( $diffs ) > 0 ) {
421 Meow_MWAI_Logging::warn( "Integrity Check: It seems the messages in the discussion #{$discussion['id']} do not match the ones sent by the client." );
422 }
423
424 // Maintain conversation state for Responses API by loading previousResponseId
425 // This enables stateful conversations where only new messages are sent
426 if ( empty( $params['previousResponseId'] ) && !empty( $discussion['extra'] ) ) {
427 $extra = json_decode( $discussion['extra'], true );
428 if ( !empty( $extra['responseId'] ) ) {
429 // Response IDs expire after 30 days per OpenAI's policy
430 // Check if the stored response is still valid
431 $responseDate = !empty( $extra['responseDate'] ) ? strtotime( $extra['responseDate'] ) : 0;
432 $thirtyDaysAgo = time() - ( 30 * 24 * 60 * 60 );
433
434 if ( $responseDate > $thirtyDaysAgo ) {
435 // Use the stored response ID for stateful conversation
436 $params['previousResponseId'] = $extra['responseId'];
437 }
438 }
439 }
440 }
441 else {
442 // No discussion yet? We still need to check the startSentence.
443 $startSentence = isset( $chatbot['startSentence'] ) ? $chatbot['startSentence'] : null;
444 $messages = [];
445 if ( !empty( $startSentence ) ) {
446 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
447 }
448 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
449 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
450 if ( count( $diffs ) > 0 ) {
451 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 ) );
452 }
453 }
454 }
455
456 // Create QueryText
457 $context = null;
458 $streamCallback = null;
459 $mode = $chatbot['mode'] ?? 'chat';
460
461 if ( $mode === 'images' ) {
462 // If there's an uploaded file, use EditImage query instead
463 if ( !empty( $newFileId ) ) {
464 $query = new Meow_MWAI_Query_EditImage( $newMessage );
465
466 // Handle the uploaded image
467 $url = $this->core->files->get_url( $newFileId );
468 $mimeType = $this->core->files->get_mime_type( $newFileId );
469 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
470
471 if ( $isIMG ) {
472 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
473 $fileId = $this->core->files->get_id_from_refId( $newFileId );
474 $this->core->files->update_purpose( $fileId, 'vision' );
475 }
476 }
477 else {
478 $query = new Meow_MWAI_Query_Image( $newMessage );
479 }
480
481 // Handle Params
482 $newParams = [];
483 foreach ( $chatbot as $key => $value ) {
484 $newParams[$key] = $value;
485 }
486 if ( is_array( $params ) ) {
487 foreach ( $params as $key => $value ) {
488 $newParams[$key] = $value;
489 }
490 }
491 $params = apply_filters( 'mwai_chatbot_params', $newParams );
492 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
493
494 $query->inject_params( $params );
495 }
496 else {
497 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
498 new Meow_MWAI_Query_Text( $newMessage, 1024 );
499
500 // Handle Params
501 $newParams = [];
502 foreach ( $chatbot as $key => $value ) {
503 $newParams[$key] = $value;
504 }
505 if ( is_array( $params ) ) {
506 foreach ( $params as $key => $value ) {
507 $newParams[$key] = $value;
508 }
509 }
510 $params = apply_filters( 'mwai_chatbot_params', $newParams );
511 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
512
513 $query->inject_params( $params );
514
515 $storeId = null;
516 if ( $mode === 'assistant' ) {
517 $chatId = $params['chatId'] ?? null;
518 if ( !empty( $chatId ) ) {
519 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
520 if ( isset( $discussion['storeId'] ) ) {
521 $storeId = $discussion['storeId'];
522 $query->setStoreId( $storeId );
523 }
524 }
525 }
526
527 // Support for Uploaded Image
528 if ( !empty( $newFileId ) ) {
529
530 // Get extension and mime type
531 $isImage = $this->core->files->is_image( $newFileId );
532
533 if ( $mode === 'assistant' && !$isImage ) {
534 $url = $this->core->files->get_path( $newFileId );
535 $data = $this->core->files->get_data( $newFileId );
536 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
537 $filename = basename( $url );
538
539 // Upload the file
540 $file = $openai->upload_file( $filename, $data, 'assistants' );
541
542 // Create a store
543 if ( empty( $storeId ) ) {
544 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
545 if ( !empty( $query->chatId ) ) {
546 $chatbotName .= '_' . $query->chatId;
547 }
548 $metadata = [];
549 if ( !empty( $chatbot['assistantId'] ) ) {
550 $metadata['assistantId'] = $chatbot['assistantId'];
551 }
552 if ( !empty( $query->chatId ) ) {
553 $metadata['chatId'] = $query->chatId;
554 }
555 $expiry = $this->core->get_option( 'image_expires' );
556 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
557 $query->setStoreId( $storeId );
558 }
559
560 // Add the file to the store
561 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
562
563 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
564 $openAiRefId = $file['id'];
565 $internalFileId = $this->core->files->get_id_from_refId( $newFileId );
566 $this->core->files->update_refId( $internalFileId, $openAiRefId );
567 $this->core->files->update_envId( $internalFileId, $query->envId );
568 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
569 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
570 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
571 $newFileId = $openAiRefId;
572 $scope = $params['fileSearch'];
573 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
574 $id = $this->core->files->get_id_from_refId( $newFileId );
575 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
576 }
577 }
578 else {
579 $url = $this->core->files->get_url( $newFileId );
580 $mimeType = $this->core->files->get_mime_type( $newFileId );
581 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
582 $purposeType = $isIMG ? 'vision' : 'files';
583 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, $purposeType, $mimeType ) );
584 $fileId = $this->core->files->get_id_from_refId( $newFileId );
585 $this->core->files->update_envId( $fileId, $query->envId );
586 $this->core->files->update_purpose( $fileId, $purposeType );
587 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
588 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
589 }
590 }
591
592 // Takeover
593 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
594 if ( !empty( $takeoverAnswer ) ) {
595 $rawText = apply_filters( 'mwai_chatbot_reply', $takeoverAnswer, $query, $params, [] );
596 return [
597 'reply' => $rawText,
598 'chatId' => $this->core->fix_chat_id( $query, $params ),
599 'images' => null,
600 'actions' => [],
601 'usage' => null
602 ];
603 }
604
605 // Moderation
606 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
607 $this->core->get_option( 'shortcode_chat_moderation' );
608 if ( $moderationEnabled ) {
609 global $mwai;
610 $isFlagged = $mwai->moderationCheck( $query->get_message() );
611 if ( $isFlagged ) {
612 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
613 }
614 }
615
616 // Setup streaming if enabled (before embeddings to capture those events)
617 $streamCallback = null;
618 $debugEvents = [];
619
620 if ( $stream ) {
621 $streamCallback = function ( $reply ) use ( $query ) {
622 // Support both legacy string data and new Event objects
623 if ( is_string( $reply ) ) {
624 $this->core->stream_push( [ 'type' => 'live', 'data' => $reply ], $query );
625 }
626 else {
627 $this->core->stream_push( $reply, $query );
628 }
629 };
630 if ( headers_sent( $filename, $linenum ) ) {
631 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
632 }
633 header( 'Cache-Control: no-cache' );
634 header( 'Content-Type: text/event-stream' );
635 // This is useful to disable buffering in nginx through headers.
636 header( 'X-Accel-Buffering: no' );
637 ob_implicit_flush( true );
638 if ( ob_get_level() > 0 ) {
639 ob_end_flush();
640 }
641 }
642 else if ( $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ) ) {
643 // For non-streaming debug mode, collect events
644 $streamCallback = function ( $event ) use ( &$debugEvents ) {
645 if ( is_object( $event ) && method_exists( $event, 'toArray' ) ) {
646 $debugEvents[] = $event->toArray();
647 }
648 };
649 }
650
651 // Awareness & Embeddings
652 $context = $this->core->retrieve_context( $params, $query, $streamCallback );
653 if ( !empty( $context ) ) {
654 $query->set_context( $context['content'] );
655 }
656
657 // Function Aware
658 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
659 }
660
661 // Process Query
662
663 $reply = $this->core->run_query( $query, $streamCallback, true );
664 $rawText = $reply->result;
665 $extra = [];
666 if ( $context ) {
667 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
668 }
669 // Store response ID for Responses API stateful conversations
670 // CRITICAL: Must store even when function calls are present
671 // This enables the feedback query to use previous_response_id
672 if ( !empty( $reply->id ) ) {
673 $extra['responseId'] = $reply->id;
674 $extra['responseDate'] = gmdate( 'Y-m-d H:i:s' ); // Track age for 30-day expiry
675 }
676 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
677
678 // Integrity Check: We need to store the checksum of the messages sent by the client.
679 $stored_messages = $client_messages;
680 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
681 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
682 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
683 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
684
685 // Actions
686 $actions = [];
687 if ( $reply->needClientActions ) {
688 foreach ( $reply->needClientActions as $action ) {
689 $actions[] = [
690 'type' => 'function',
691 'data' => [
692 'name' => $action['function']->name,
693 'args' => $action['arguments']
694 ]
695 ];
696 }
697 }
698
699 $restRes = [
700 'reply' => $rawText,
701 'chatId' => $this->core->fix_chat_id( $query, $params ),
702 'images' => $reply->get_type() === 'images' ? $reply->results : null,
703 'actions' => $actions,
704 'usage' => $reply->usage
705 ];
706
707 // Add debug events if collected
708 if ( !empty( $debugEvents ) ) {
709 $restRes['debugEvents'] = $debugEvents;
710 }
711
712 // Add response ID if available (for Responses API)
713 if ( !empty( $reply->id ) ) {
714 $restRes['responseId'] = $reply->id;
715 }
716
717 // Process Reply
718 if ( $stream ) {
719 $final_res = $this->build_final_res(
720 $botId,
721 $newMessage,
722 $newFileId,
723 $params,
724 $restRes['reply'],
725 $restRes['images'],
726 $restRes['actions'],
727 $restRes['usage'],
728 $restRes['responseId'] ?? null
729 );
730 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
731 die();
732 }
733 else {
734 return $restRes;
735 }
736
737 }
738 catch ( Exception $e ) {
739 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
740 if ( $stream ) {
741 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
742 die();
743 }
744 else {
745 throw $e;
746 }
747 }
748 }
749
750 public function inject_chat() {
751 $params = $this->core->get_chatbot( $this->siteWideChatId );
752 $clean_params = [];
753 if ( !empty( $params ) ) {
754 $clean_params['window'] = true;
755 $clean_params['id'] = $this->siteWideChatId;
756 echo $this->chat_shortcode( $clean_params );
757 }
758 return null;
759 }
760
761 public function build_front_params( $botId, $customId ) {
762 $frontSystem = [
763 'botId' => $customId ? null : sanitize_text_field( $botId ),
764 'customId' => sanitize_text_field( $customId ),
765 'userData' => $this->core->get_user_data(),
766 'sessionId' => $this->core->get_session_id(),
767 // IMPORTANT: REST nonce handling differs by user state:
768 // - Logged-in users: get_nonce() returns a user-specific nonce created in current session context
769 // - Logged-out users: get_nonce() returns null, they'll fetch via /start_session endpoint
770 // This prevents rest_cookie_invalid_nonce errors for logged-in users by ensuring the nonce
771 // matches their authentication context from the start.
772 'restNonce' => $this->core->get_nonce(),
773 'contextId' => get_the_ID(),
774 'pluginUrl' => MWAI_URL,
775 'restUrl' => untrailingslashit( get_rest_url() ),
776 'stream' => $this->core->get_option( 'ai_streaming' ),
777 'debugMode' => $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ),
778 'eventLogs' => $this->core->get_option( 'event_logs' ),
779 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
780 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
781 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
782 'virtual_keyboard_fix' => $this->core->get_option( 'virtual_keyboard_fix' )
783 ];
784 return $frontSystem;
785 }
786
787 public function resolveBotInfo( &$atts ) {
788 $chatbot = null;
789 $botId = $atts['id'] ?? null;
790 $customId = $atts['custom_id'] ?? null;
791 $parentBotId = null;
792
793 if ( !$botId && !$customId ) {
794 $botId = 'default';
795 }
796 if ( $botId ) {
797 $chatbot = $this->core->get_chatbot( $botId );
798 if ( !$chatbot ) {
799 $botId = $botId ?: 'N/A';
800 $safe_botId = esc_html( $botId );
801 return [
802 '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'.",
803 ];
804 }
805 }
806 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
807 if ( !empty( $customId ) ) {
808 if ( $botId !== null ) {
809 $parentBotId = $botId;
810 $botId = null;
811 }
812 }
813 unset( $atts['id'] );
814 return [
815 'chatbot' => $chatbot,
816 'botId' => $botId,
817 'customId' => $customId,
818 'parentBotId' => $parentBotId
819 ];
820 }
821
822 public function chat_shortcode( $atts ) {
823 $atts = empty( $atts ) ? [] : $atts;
824
825 foreach ( $atts as $key => $value ) {
826 $atts[ $key ] = urldecode( $value );
827 }
828
829 // Let the user override the chatbot params
830 $atts = apply_filters( 'mwai_chatbot_params', $atts );
831
832 // Resolve the bot info
833 $resolvedBot = $this->resolveBotInfo( $atts, 'chatbot' );
834 if ( isset( $resolvedBot['error'] ) ) {
835 return $resolvedBot['error'];
836 }
837 $chatbot = $resolvedBot['chatbot'];
838 $botId = $resolvedBot['botId'];
839 $customId = $resolvedBot['customId'];
840 $parentBotId = $resolvedBot['parentBotId'];
841
842 // Rename the keys of the atts into camelCase to match the internal params system.
843 $atts = array_map( function ( $key, $value ) {
844 $key = str_replace( '_', ' ', $key );
845 $key = ucwords( $key );
846 $key = str_replace( ' ', '', $key );
847 $key = lcfirst( $key );
848 return [ $key => $value ];
849 }, array_keys( $atts ), $atts );
850 $atts = array_merge( ...$atts );
851
852 if ( !empty( $parentBotId ) ) {
853 $atts['parentBotId'] = $parentBotId;
854 }
855
856 $frontParams = [];
857 // Define text parameters that need sanitization (excluding those that support HTML)
858 $textParams = ['aiName', 'userName', 'guestName', 'textSend', 'textClear', 'textInputPlaceholder',
859 'startSentence', 'iconText', 'iconAlt', 'headerSubtitle'];
860 // Parameters that support HTML content
861 $htmlParams = ['textCompliance'];
862
863 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
864 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
865 if ( isset( $atts[$param] ) ) {
866 if ( $param === 'localMemory' ) {
867 $frontParams[$param] = $atts[$param] === 'true';
868 }
869 else if ( in_array( $param, $textParams ) ) {
870 // Sanitize text parameters to prevent XSS
871 $frontParams[$param] = sanitize_text_field( $atts[$param] );
872 }
873 else if ( in_array( $param, $htmlParams ) ) {
874 // For HTML parameters, use wp_kses_post to allow safe HTML
875 $frontParams[$param] = wp_kses_post( $atts[$param] );
876 }
877 else {
878 $frontParams[$param] = $atts[$param];
879 }
880 }
881 // If not, let's use the chatbot's default values
882 else if ( isset( $chatbot[$param] ) ) {
883 $frontParams[$param] = $chatbot[$param];
884 }
885
886 // Apply the placeholders
887 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
888 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
889 }
890 }
891
892 // Server Params
893 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
894 // we are using the default or a specific chatbot.
895 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
896 $hasServerOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
897 $hasFrontOverrides = count( array_intersect( array_keys( $atts ), MWAI_CHATBOT_FRONT_PARAMS ) ) > 0;
898 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasFrontOverrides );
899
900 $serverParams = [];
901 if ( $hasOverrides ) {
902 // Server parameters don't need sanitization as they're processed server-side
903 // and not rendered in HTML. They may contain code, HTML, etc. for AI context.
904 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
905 if ( isset( $atts[$param] ) ) {
906 $serverParams[$param] = $atts[$param];
907 }
908 else {
909 // For custom chatbots, don't inherit embeddingsEnvId from the default chatbot
910 if ( $param === 'embeddingsEnvId' && !empty( $customId ) ) {
911 $serverParams[$param] = '';
912 }
913 else {
914 $serverParams[$param] = $chatbot[$param] ?? null;
915 }
916 }
917 }
918 }
919
920 // Front Params
921 $frontSystem = $this->build_front_params( $botId, $customId );
922
923 // Clean Params
924 $frontParams = $this->clean_params( $frontParams );
925 $frontSystem = $this->clean_params( $frontSystem );
926 $serverParams = $this->clean_params( $serverParams );
927
928 // Server-side: Keep the System Params
929 if ( $hasOverrides ) {
930 if ( empty( $customId ) ) {
931 $customId = md5( json_encode( $serverParams ) );
932 $frontSystem['customId'] = $customId;
933 }
934 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
935 }
936
937 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
938 $filterParams = [
939 'step' => 'init',
940 'botId' => $botId,
941 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
942 ];
943 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
944 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
945 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
946 $frontSystem['actions'] = $this->sanitize_actions( $actions );
947 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
948 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
949
950 // Client-side: Prepare JSON for Front Params and System Params
951 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
952 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
953 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
954 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
955 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
956
957 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
958
959 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
960 }
961
962 public function chatbot_discussions( $atts ) {
963 $atts = empty( $atts ) ? [] : $atts;
964
965 // Resolve the bot info
966 $resolvedBot = $this->resolveBotInfo( $atts );
967 if ( isset( $resolvedBot['error'] ) ) {
968 return $resolvedBot['error'];
969 }
970 $chatbot = $resolvedBot['chatbot'];
971 $botId = $resolvedBot['botId'];
972 $customId = $resolvedBot['customId'];
973
974 // Rename the keys of the atts into camelCase to match the internal params system.
975 $atts = array_map( function ( $key, $value ) {
976 $key = str_replace( '_', ' ', $key );
977 $key = ucwords( $key );
978 $key = str_replace( ' ', '', $key );
979 $key = lcfirst( $key );
980 return [ $key => $value ];
981 }, array_keys( $atts ), $atts );
982 $atts = array_merge( ...$atts );
983
984 // Front Params
985 $frontParams = [];
986 // All discussion params are text params that need sanitization
987 $textParams = ['textNewChat'];
988
989 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
990 if ( isset( $atts[$param] ) ) {
991 // Sanitize text parameters
992 $frontParams[$param] = in_array( $param, $textParams ) ? sanitize_text_field( $atts[$param] ) : $atts[$param];
993 }
994 else if ( isset( $chatbot[$param] ) ) {
995 $frontParams[$param] = $chatbot[$param];
996 }
997 }
998
999 // Server Params
1000 $serverParams = [];
1001 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
1002 if ( isset( $atts[$param] ) ) {
1003 $serverParams[$param] = $atts[$param];
1004 }
1005 }
1006
1007 // Front System
1008 $frontSystem = $this->build_front_params( $botId, $customId );
1009 // Get refresh interval from settings
1010 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
1011 if ( $refresh_interval === 'Never' ) {
1012 $frontSystem['refreshInterval'] = 0;
1013 }
1014 elseif ( $refresh_interval === 'Manual' ) {
1015 $frontSystem['refreshInterval'] = -1;
1016 }
1017 elseif ( is_numeric( $refresh_interval ) ) {
1018 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
1019 }
1020 else {
1021 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
1022 }
1023 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
1024
1025 // Get paging setting
1026 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
1027 if ( $paging_option === 'None' ) {
1028 $frontSystem['paging'] = 0; // No pagination
1029 }
1030 else {
1031 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
1032 }
1033
1034 // Get metadata settings
1035 $frontSystem['metadata'] = [
1036 'enabled' => $this->core->get_option( 'chatbot_discussions_metadata_enabled' ),
1037 'startDate' => $this->core->get_option( 'chatbot_discussions_metadata_start_date' ),
1038 'lastUpdate' => $this->core->get_option( 'chatbot_discussions_metadata_last_update' ),
1039 'messageCount' => $this->core->get_option( 'chatbot_discussions_metadata_message_count' )
1040 ];
1041
1042 // Clean Params
1043 $frontParams = $this->clean_params( $frontParams );
1044 $frontSystem = $this->clean_params( $frontSystem );
1045 $serverParams = $this->clean_params( $serverParams );
1046
1047 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1048 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1049 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1050 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1051
1052 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1053 }
1054
1055 public function clean_params( &$params ) {
1056 foreach ( $params as $param => $value ) {
1057 if ( $param === 'restNonce' ) {
1058 continue;
1059 }
1060 if ( empty( $value ) || is_array( $value ) ) {
1061 continue;
1062 }
1063 $lowerCaseValue = strtolower( $value );
1064 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
1065 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
1066 }
1067 else if ( is_numeric( $value ) ) {
1068 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
1069 }
1070 }
1071 return $params;
1072 }
1073
1074 }
1075