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