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