PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.0.7
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.0.7
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 10 months ago discussions.php 10 months ago files.php 11 months ago forms-manager.php 10 months ago gdpr.php 11 months ago search.php 11 months ago security.php 11 months ago tasks.php 11 months ago wand.php 10 months ago
chatbot.php
1262 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', 'centerOpen', 'width', 'openDelay', 'iconBubble', 'windowAnimation', 'fullscreen', 'copyButton', 'headerSubtitle', 'popupTitle', 'containerType', 'headerType', 'messagesType', 'inputType', 'footerType' ] );
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', 'crossSite', 'promptId', 'promptVariables', 'reasoningEffort', 'verbosity' ] );
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
41 // Don't load chatbot scripts on the Site Editor to avoid conflicts
42 $current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
43 $is_site_editor = $current_screen && $current_screen->base === 'site-editor';
44
45 if ( ( is_admin() && !$is_site_editor ) || $hasSiteWideChat ) {
46 $themeId = null;
47 if ( $hasSiteWideChat ) {
48 $bot = $this->core->get_chatbot( $this->siteWideChatId );
49 if ( $bot && isset( $bot['themeId'] ) ) {
50 $themeId = $bot['themeId'];
51 }
52 }
53 $this->enqueue_scripts( is_admin() ? null : $themeId );
54 if ( $hasSiteWideChat ) {
55 // Chatbot Injection
56 add_action( 'wp_footer', [ $this, 'inject_chat' ] );
57 }
58 }
59 }
60
61 public function enqueue_scripts( $themeId = null ) {
62 wp_enqueue_script( 'mwai_chatbot' );
63 if ( $this->core->get_option( 'syntax_highlight' ) ) {
64 wp_enqueue_script( 'mwai_highlight' );
65 }
66 if ( $themeId ) {
67 $this->core->enqueue_theme( $themeId );
68 }
69 else {
70 $this->core->enqueue_themes();
71 }
72 }
73
74 /**
75 * Helper method to create REST responses with automatic token refresh
76 *
77 * @param array $data The response data
78 * @param int $status HTTP status code
79 * @return WP_REST_Response
80 */
81 protected function create_rest_response( $data, $status = 200 ) {
82 // Always check if we need to provide a new nonce
83 $current_nonce = $this->core->get_nonce( true );
84 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
85
86 // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
87 // We'll refresh if the nonce is older than 10 hours to be safe
88 $should_refresh = false;
89
90 if ( $request_nonce ) {
91 // Try to determine the age of the nonce
92 // WordPress uses a tick system where each tick is 12 hours
93 // If we're in the second half of the nonce's life, refresh it
94 $time = time();
95 $nonce_tick = wp_nonce_tick();
96
97 // Verify if the nonce is still valid but getting old
98 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
99 if ( $verify === 2 ) {
100 // Nonce is valid but was generated 12-24 hours ago
101 $should_refresh = true;
102 // Log will be written when token is included in response
103 }
104 }
105
106 // If the nonce has changed or should be refreshed, include the new one
107 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
108 $data['new_token'] = $current_nonce;
109
110 // Log if server debug mode is enabled
111 if ( $this->core->get_option( 'server_debug_mode' ) ) {
112 error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
113 }
114 }
115
116 return new WP_REST_Response( $data, $status );
117 }
118
119 public function rest_api_init() {
120 register_rest_route( $this->namespace, '/chats/submit', [
121 'methods' => 'POST',
122 'callback' => [ $this, 'rest_chat' ],
123 'permission_callback' => [ $this->core, 'check_rest_nonce' ]
124 ] );
125 }
126
127 public function basics_security_check( $botId, $customId, $newMessage, $newFileId ) {
128 if ( !$botId && !$customId ) {
129 Meow_MWAI_Logging::warn( 'The query was rejected - no botId nor id was specified.' );
130 return false;
131 }
132
133 if ( $newFileId ) {
134 return true;
135 }
136
137 $length = strlen( empty( $newMessage ) ? '' : $newMessage );
138 if ( $length < 1 ) {
139 Meow_MWAI_Logging::warn( 'The query was rejected - message was too short.' );
140 return false;
141 }
142 return true;
143 }
144
145 public function build_final_res( $botId, $newMessage, $newFileId, $params, $reply, $images, $actions, $usage, $responseId = null ) {
146 $filterParams = [
147 'step' => 'reply',
148 'botId' => $botId,
149 'reply' => $reply,
150 'images' => $images,
151 'newMessage' => $newMessage,
152 'newFileId' => $newFileId,
153 'params' => $params,
154 'usage' => $usage,
155 'messages' => $params['messages'] ?? [],
156 'isNewConversation' => empty( $params['messages'] ) || count( $params['messages'] ) <= 1,
157 ];
158 $actions = apply_filters( 'mwai_chatbot_actions', $actions, $filterParams );
159 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
160 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
161 $actions = $this->sanitize_actions( $actions );
162 $blocks = $this->sanitize_blocks( $blocks );
163 $shortcuts = $this->sanitize_shortcuts( $shortcuts );
164 $result = [
165 'success' => true,
166 'reply' => $reply,
167 'images' => $images,
168 'actions' => $actions,
169 'shortcuts' => $shortcuts,
170 'blocks' => $blocks,
171 'usage' => $usage
172 ];
173
174 // Add response ID if available
175 if ( !empty( $responseId ) ) {
176 $result['responseId'] = $responseId;
177 }
178
179 // Check if token needs refresh
180 $current_nonce = $this->core->get_nonce( true );
181 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
182
183 $should_refresh = false;
184 if ( $request_nonce ) {
185 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
186 if ( $verify === 2 ) {
187 // Nonce is valid but was generated 12-24 hours ago
188 $should_refresh = true;
189 }
190 }
191
192 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
193 $result['new_token'] = $current_nonce;
194 }
195
196 return $result;
197 }
198
199 public function rest_chat( $request ) {
200 $params = $request->get_json_params();
201 $botId = $params['botId'] ?? null;
202 $customId = $params['customId'] ?? null;
203 $stream = $params['stream'] ?? false;
204 $newMessage = trim( $params['newMessage'] ?? '' );
205 $newFileId = $params['newFileId'] ?? null;
206 $newFileIds = $params['newFileIds'] ?? [];
207 $crossSite = $params['crossSite'] ?? false;
208
209 if ( !$this->basics_security_check( $botId, $customId, $newMessage, $newFileId ) ) {
210 return $this->create_rest_response( [
211 'success' => false,
212 'message' => apply_filters( 'mwai_ai_exception', 'Sorry, your query has been rejected.' )
213 ], 403 );
214 }
215
216 try {
217 $data = $this->chat_submit( $botId, $newMessage, $newFileId, $params, $stream, $newFileIds );
218 $final_res = $this->build_final_res(
219 $botId,
220 $newMessage,
221 $newFileId,
222 $params,
223 $data['reply'],
224 $data['images'],
225 $data['actions'],
226 $data['usage'],
227 $data['responseId'] ?? null
228 );
229 return $this->create_rest_response( $final_res, 200 );
230 }
231 catch ( Exception $e ) {
232 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
233 return $this->create_rest_response( [
234 'success' => false,
235 'message' => $message
236 ], 500 );
237 }
238 }
239
240 private function sanitize_items( $items, $supported_types, $type_name ) {
241 if ( empty( $items ) ) {
242 return $items;
243 }
244 $sanitized_items = [];
245 foreach ( $items as $item ) {
246 if ( isset( $supported_types[$item['type']] ) ) {
247 $is_valid = true;
248 foreach ( $supported_types[$item['type']] as $param ) {
249 if ( !isset( $item['data'][$param] ) ) {
250 $is_valid = false;
251 Meow_MWAI_Logging::warn( "The query was rejected - missing required parameter '{$param}' for {$type_name} type: {$item['type']}." );
252 break;
253 }
254 }
255 if ( $is_valid ) {
256 $sanitized_items[] = $item;
257 }
258 }
259 else {
260 Meow_MWAI_Logging::warn( "The query was rejected - unsupported {$type_name} type: {$item['type']}." );
261 }
262 }
263 return $sanitized_items;
264 }
265
266 public function sanitize_actions( $actions ) {
267 $supported_action_types = [
268 'function' => ['name', 'args'],
269 'javascript' => ['snippet'],
270 ];
271 return $this->sanitize_items( $actions, $supported_action_types, 'action' );
272 }
273
274 public function sanitize_blocks( $blocks ) {
275 $supported_block_types = [
276 'content' => ['html'],
277 ];
278 return $this->sanitize_items( $blocks, $supported_block_types, 'block' );
279 }
280
281 public function sanitize_shortcuts( $shortcuts ) {
282 $supported_shortcut_types = [
283 'message' => ['label', 'message'],
284 'action' => ['label', 'message', 'action'],
285 'callback' => ['label', 'onClick'],
286 ];
287 return $this->sanitize_items( $shortcuts, $supported_shortcut_types, 'shortcut' );
288 }
289
290 #region Messages Integrity Check
291
292 public function messages_integrity_diff( $messages1, $messages2 ) {
293 // Ensure both parameters are arrays
294 if ( !is_array( $messages1 ) ) {
295 $messages1 = [];
296 }
297 if ( !is_array( $messages2 ) ) {
298 $messages2 = [];
299 }
300
301 // Collect messages with role not 'user' from messages1
302 $messagesList1 = [];
303 foreach ( $messages1 as $msg ) {
304 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
305 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
306 if ( $role && $role != 'user' ) {
307 $messageData = [ 'role' => $role, 'content' => $content ];
308 $messagesList1[] = $messageData;
309 }
310 }
311
312 // Collect messages with role not 'user' from messages2
313 $messagesList2 = [];
314 foreach ( $messages2 as $msg ) {
315 $role = isset( $msg->role ) ? $msg->role : ( isset( $msg['role'] ) ? $msg['role'] : null );
316 $content = isset( $msg->content ) ? $msg->content : ( isset( $msg['content'] ) ? $msg['content'] : null );
317 if ( $role && $role != 'user' ) {
318 $messageData = [ 'role' => $role, 'content' => $content ];
319 $messagesList2[] = $messageData;
320 }
321 }
322
323 // Count occurrences of each message in messagesList1
324 $counts1 = [];
325 foreach ( $messagesList1 as $msg ) {
326 $key = serialize( $msg );
327 if ( isset( $counts1[ $key ] ) ) {
328 $counts1[ $key ]++;
329 }
330 else {
331 $counts1[ $key ] = 1;
332 }
333 }
334
335 // Count occurrences of each message in messagesList2
336 $counts2 = [];
337 foreach ( $messagesList2 as $msg ) {
338 $key = serialize( $msg );
339 if ( isset( $counts2[ $key ] ) ) {
340 $counts2[ $key ]++;
341 }
342 else {
343 $counts2[ $key ] = 1;
344 }
345 }
346
347 // Compare counts to find unmatched messages
348 $all_keys = array_unique( array_merge( array_keys( $counts1 ), array_keys( $counts2 ) ) );
349
350 $diffs = [];
351 foreach ( $all_keys as $key ) {
352 $count1 = isset( $counts1[ $key ] ) ? $counts1[ $key ] : 0;
353 $count2 = isset( $counts2[ $key ] ) ? $counts2[ $key ] : 0;
354 if ( $count1 != $count2 ) {
355 $message = unserialize( $key );
356 $diffs[] = [
357 'message' => $message,
358 'count_in_messages1' => $count1,
359 'count_in_messages2' => $count2
360 ];
361 }
362 }
363
364 return $diffs;
365 }
366
367 private function calculate_messages_checksum( $messages ) {
368 $messages_to_hash = [];
369 foreach ( $messages as $msg ) {
370 $role = is_array( $msg ) ? ( $msg['role'] ?? '' ) : ( is_object( $msg ) ? ( $msg->role ?? '' ) : '' );
371 $content = is_array( $msg ) ? ( $msg['content'] ?? '' ) : ( is_object( $msg ) ? ( $msg->content ?? '' ) : '' );
372 if ( in_array( $role, ['assistant', 'system'] ) ) {
373 $messages_to_hash[] = [ 'role' => $role, 'content' => $content ];
374 }
375 }
376 return md5( json_encode( $messages_to_hash ) );
377 }
378
379 #endregion
380
381 public function chat_submit( $botId, $newMessage, $newFileId = null, $params = [], $stream = false, $newFileIds = [] ) {
382 $query = null; // Initialize query variable to avoid undefined variable errors
383 try {
384 $chatbot = null;
385 $customId = $params['customId'] ?? null;
386
387 // Custom Chatbot
388 if ( $customId ) {
389 $chatbot = get_transient( 'mwai_custom_chatbot_' . $customId );
390 }
391 // Registered Chatbot
392 if ( !$chatbot && $botId ) {
393 $chatbot = $this->core->get_chatbot( $botId );
394 }
395 // Fall back to default chatbot if no chatbot found yet
396 if ( !$chatbot ) {
397 $chatbot = $this->core->get_chatbot( 'default' );
398 }
399
400 if ( !$chatbot ) {
401 Meow_MWAI_Logging::warn( 'The query was rejected - no chatbot was found.' );
402 throw new Exception( 'Sorry, your query has been rejected.' );
403 }
404
405 $textInputMaxLength = $chatbot['textInputMaxLength'] ?? null;
406 if ( $textInputMaxLength && $this->core->safe_strlen( $newMessage ) > (int) $textInputMaxLength ) {
407 Meow_MWAI_Logging::warn( 'The query was rejected - message was too long.' );
408 throw new Exception( 'Sorry, your query has been rejected.' );
409 }
410
411 // We need to check the integrity of the messages sent by the client.
412 // This is important to ensure that the messages are not tampered with.
413
414 // Messages Integrity Check with Checksums
415 $chatId = $params['chatId'] ?? 'default';
416 $checksum_key = 'mwai_chatbot_checksum_' . $chatId;
417 $stored_checksum = get_transient( $checksum_key );
418 $client_messages = $params['messages'] ?? [];
419 $client_checksum = $this->calculate_messages_checksum( $client_messages );
420 if ( $stored_checksum && $stored_checksum !== $client_checksum ) {
421 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.' );
422 }
423
424 // Messages Integrity Check with Discussions
425 if ( $this->core->get_option( 'chatbot_discussions' ) && $this->core->discussions && isset( $params['chatId'] ) ) {
426 $discussion = $this->core->discussions->get_discussion( $botId ? $botId : $customId, $params['chatId'] );
427 if ( $discussion ) {
428 $messages = $discussion['messages'];
429 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
430 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
431 if ( count( $diffs ) > 0 ) {
432 Meow_MWAI_Logging::warn( "Integrity Check: It seems the messages in the discussion #{$discussion['id']} do not match the ones sent by the client." );
433 }
434
435 // Maintain conversation state for Responses API by loading previousResponseId
436 // This enables stateful conversations where only new messages are sent
437 if ( empty( $params['previousResponseId'] ) && !empty( $discussion['extra'] ) ) {
438 $extra = json_decode( $discussion['extra'], true );
439 if ( !empty( $extra['responseId'] ) ) {
440 // Response IDs expire after 30 days per OpenAI's policy
441 // Check if the stored response is still valid
442 $responseDate = !empty( $extra['responseDate'] ) ? strtotime( $extra['responseDate'] ) : 0;
443 $thirtyDaysAgo = time() - ( 30 * 24 * 60 * 60 );
444
445 if ( $responseDate > $thirtyDaysAgo ) {
446 // Use the stored response ID for stateful conversation
447 $params['previousResponseId'] = $extra['responseId'];
448 }
449 }
450 }
451 }
452 else {
453 // No discussion yet? We still need to check the startSentence.
454 $startSentence = isset( $chatbot['startSentence'] ) ? $chatbot['startSentence'] : null;
455 $messages = [];
456 if ( !empty( $startSentence ) ) {
457 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
458 }
459 $clientMessages = isset( $params['messages'] ) ? $params['messages'] : [];
460 $diffs = $this->messages_integrity_diff( $messages, $clientMessages );
461 if ( count( $diffs ) > 0 ) {
462 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 ) );
463 }
464 }
465 }
466
467 // Create QueryText
468 $context = null;
469 $streamCallback = null;
470 $mode = $chatbot['mode'] ?? 'chat';
471
472 if ( $mode === 'images' ) {
473 // Check for uploaded files
474 $fileForImage = null;
475 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
476 $fileForImage = $newFileIds[0];
477 }
478 elseif ( !empty( $newFileId ) ) {
479 $fileForImage = $newFileId;
480 }
481
482 // If there's an uploaded file, use EditImage query instead
483 if ( !empty( $fileForImage ) ) {
484 $query = new Meow_MWAI_Query_EditImage( $newMessage );
485
486 // Handle the uploaded image
487 $url = $this->core->files->get_url( $fileForImage );
488 $mimeType = $this->core->files->get_mime_type( $fileForImage );
489 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
490
491 if ( $isIMG ) {
492 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision', $mimeType ) );
493 $fileId = $this->core->files->get_id_from_refId( $fileForImage );
494 $this->core->files->update_purpose( $fileId, 'vision' );
495 }
496 }
497 else {
498 $query = new Meow_MWAI_Query_Image( $newMessage );
499 }
500
501 // Handle Params
502 $newParams = [];
503 foreach ( $chatbot as $key => $value ) {
504 $newParams[$key] = $value;
505 }
506 if ( is_array( $params ) ) {
507 foreach ( $params as $key => $value ) {
508 $newParams[$key] = $value;
509 }
510 }
511
512 // Map 'environment' field to 'envId' for compatibility
513 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
514 $newParams['envId'] = $newParams['environment'];
515 }
516
517 $params = apply_filters( 'mwai_chatbot_params', $newParams );
518 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
519
520 // Debug log for embeddings
521 if ( !empty( $params['embeddingsEnvId'] ) ) {
522 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
523 }
524 else {
525 // Log all params to debug
526 $paramKeys = array_keys( $params );
527 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
528 }
529
530 $query->inject_params( $params );
531 }
532 else {
533 $query = $mode === 'assistant' ? new Meow_MWAI_Query_Assistant( $newMessage ) :
534 new Meow_MWAI_Query_Text( $newMessage, 4096 );
535
536 // Handle Params
537 $newParams = [];
538 foreach ( $chatbot as $key => $value ) {
539 $newParams[$key] = $value;
540 }
541 if ( is_array( $params ) ) {
542 foreach ( $params as $key => $value ) {
543 $newParams[$key] = $value;
544 }
545 }
546
547 // Map 'environment' field to 'envId' for compatibility
548 if ( isset( $newParams['environment'] ) && !isset( $newParams['envId'] ) ) {
549 $newParams['envId'] = $newParams['environment'];
550 }
551
552 $params = apply_filters( 'mwai_chatbot_params', $newParams );
553 $params['scope'] = empty( $params['scope'] ) ? 'chatbot' : $params['scope'];
554
555 // Debug log for embeddings
556 if ( !empty( $params['embeddingsEnvId'] ) ) {
557 Meow_MWAI_Logging::log( 'Chatbot: Setting embeddingsEnvId on query: ' . $params['embeddingsEnvId'] );
558 }
559 else {
560 // Log all params to debug
561 $paramKeys = array_keys( $params );
562 Meow_MWAI_Logging::log( 'Chatbot: No embeddingsEnvId found. Available params: ' . implode( ', ', $paramKeys ) );
563 }
564
565 // In Prompt mode, clear out features that are not supported before injecting params
566 if ( $mode === 'prompt' ) {
567 // Clear embeddings/context settings
568 unset( $params['embeddingsEnvId'] );
569 unset( $params['embeddingsIndex'] );
570 unset( $params['embeddingsNamespace'] );
571 unset( $params['contentAware'] );
572 unset( $params['context'] );
573
574 // Clear function calling and MCP servers
575 unset( $params['functions'] );
576 unset( $params['mcpServers'] );
577
578 // Clear tools
579 unset( $params['tools'] );
580
581 // Clear temperature, reasoning, verbosity as they're configured in the prompt
582 unset( $params['temperature'] );
583 unset( $params['reasoningEffort'] );
584 unset( $params['verbosity'] );
585 unset( $params['maxTokens'] );
586 }
587
588 $query->inject_params( $params );
589
590 // Handle Prompt mode specifics
591 if ( $mode === 'prompt' && !empty( $params['promptId'] ) ) {
592 $promptData = [
593 'id' => $params['promptId']
594 ];
595
596 // TODO: Prompt Variables support - might be added later
597 // Add prompt version if provided
598 // if ( !empty( $params['promptVersion'] ) ) {
599 // $promptData['version'] = $params['promptVersion'];
600 // }
601
602 // Add prompt variables if provided
603 // if ( !empty( $params['promptVariables'] ) ) {
604 // try {
605 // $variables = is_string( $params['promptVariables'] ) ?
606 // json_decode( $params['promptVariables'], true ) :
607 // $params['promptVariables'];
608 // if ( $variables ) {
609 // $promptData['variables'] = $variables;
610 // }
611 // } catch ( Exception $e ) {
612 // // Invalid JSON, skip variables
613 // }
614 // }
615
616 $query->setExtraParam( 'prompt', $promptData );
617 }
618
619 $storeId = null;
620 if ( $mode === 'assistant' ) {
621 $chatId = $params['chatId'] ?? null;
622 if ( !empty( $chatId ) && $this->core->discussions ) {
623 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
624 if ( isset( $discussion['storeId'] ) ) {
625 $storeId = $discussion['storeId'];
626 $query->setStoreId( $storeId );
627 }
628 }
629 }
630
631 // Support for Multiple Uploaded Files
632 $filesToProcess = [];
633 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
634 $filesToProcess = $newFileIds;
635 }
636 elseif ( !empty( $newFileId ) ) {
637 $filesToProcess[] = $newFileId;
638 }
639
640 // Support for Uploaded Image/Files
641 if ( !empty( $filesToProcess ) ) {
642 // For now, we only process the first file to maintain backward compatibility
643 // TODO: In the future, we could support multiple files in the query
644 $fileToProcess = $filesToProcess[0];
645
646 // Get extension and mime type
647 $isImage = $this->core->files->is_image( $fileToProcess );
648
649 if ( $mode === 'assistant' && !$isImage ) {
650 $url = $this->core->files->get_path( $fileToProcess );
651 $data = $this->core->files->get_data( $fileToProcess );
652 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
653 $filename = basename( $url );
654
655 // Upload the file
656 $file = $openai->upload_file( $filename, $data, 'assistants' );
657
658 // Create a store
659 if ( empty( $storeId ) ) {
660 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
661 if ( !empty( $query->chatId ) ) {
662 $chatbotName .= '_' . $query->chatId;
663 }
664 $metadata = [];
665 if ( !empty( $chatbot['assistantId'] ) ) {
666 $metadata['assistantId'] = $chatbot['assistantId'];
667 }
668 if ( !empty( $query->chatId ) ) {
669 $metadata['chatId'] = $query->chatId;
670 }
671 $expiry = $this->core->get_option( 'image_expires' );
672 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
673 $query->setStoreId( $storeId );
674 }
675
676 // Add the file to the store - wait a moment for store to be ready
677 sleep( 1 );
678 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
679
680 if ( empty( $storeFileId ) ) {
681 throw new Exception( 'Failed to add file to vector store.' );
682 }
683
684 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
685 $openAiRefId = $file['id'];
686 $internalFileId = $this->core->files->get_id_from_refId( $fileToProcess );
687 $this->core->files->update_refId( $internalFileId, $openAiRefId );
688 $this->core->files->update_envId( $internalFileId, $query->envId );
689 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
690 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
691 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
692 $fileToProcess = $openAiRefId;
693 $scope = $params['fileSearch'];
694 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
695 $id = $this->core->files->get_id_from_refId( $fileToProcess );
696 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
697 }
698 }
699 else {
700 $url = $this->core->files->get_url( $fileToProcess );
701 $mimeType = $this->core->files->get_mime_type( $fileToProcess );
702 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
703 $purposeType = $isIMG ? 'vision' : 'files';
704 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, $purposeType, $mimeType ) );
705 $fileId = $this->core->files->get_id_from_refId( $fileToProcess );
706 $this->core->files->update_envId( $fileId, $query->envId );
707 $this->core->files->update_purpose( $fileId, $purposeType );
708 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
709 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
710 }
711 }
712
713 // Takeover
714 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
715 if ( !empty( $takeoverAnswer ) ) {
716 $rawText = apply_filters( 'mwai_chatbot_reply', $takeoverAnswer, $query, $params, [] );
717 return [
718 'reply' => $rawText,
719 'chatId' => $this->core->fix_chat_id( $query, $params ),
720 'images' => null,
721 'actions' => [],
722 'usage' => null
723 ];
724 }
725
726 // Moderation
727 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
728 $this->core->get_option( 'shortcode_chat_moderation' );
729 if ( $moderationEnabled ) {
730 global $mwai;
731 $isFlagged = $mwai->moderationCheck( $query->get_message() );
732 if ( $isFlagged ) {
733 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
734 }
735 }
736
737 // Setup streaming if enabled (before embeddings to capture those events)
738 $streamCallback = null;
739 $debugEvents = [];
740
741 if ( $stream ) {
742 $streamCallback = function ( $reply ) use ( $query ) {
743 // Support both legacy string data and new Event objects
744 if ( is_string( $reply ) ) {
745 $this->core->stream_push( [ 'type' => 'live', 'data' => $reply ], $query );
746 }
747 else {
748 $this->core->stream_push( $reply, $query );
749 }
750 };
751 if ( headers_sent( $filename, $linenum ) ) {
752 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
753 }
754 header( 'Cache-Control: no-cache' );
755 header( 'Content-Type: text/event-stream' );
756 // This is useful to disable buffering in nginx through headers.
757 header( 'X-Accel-Buffering: no' );
758 ob_implicit_flush( true );
759 if ( ob_get_level() > 0 ) {
760 ob_end_flush();
761 }
762 }
763 else if ( $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ) ) {
764 // For non-streaming debug mode, collect events
765 $streamCallback = function ( $event ) use ( &$debugEvents ) {
766 if ( is_object( $event ) && method_exists( $event, 'toArray' ) ) {
767 $debugEvents[] = $event->toArray();
768 }
769 };
770 }
771
772 // Awareness & Embeddings
773 $context = $this->core->retrieve_context( $params, $query, $streamCallback );
774 if ( !empty( $context ) ) {
775 $query->set_context( $context['content'] );
776 }
777
778 // Function Aware
779 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
780 }
781
782 // Process Query
783
784 $reply = $this->core->run_query( $query, $streamCallback, true );
785 $rawText = $reply->result;
786 $extra = [];
787 if ( $context ) {
788 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
789 }
790 // Store response ID for Responses API stateful conversations
791 // CRITICAL: Must store even when function calls are present
792 // This enables the feedback query to use previous_response_id
793 if ( !empty( $reply->id ) ) {
794 $extra['responseId'] = $reply->id;
795 $extra['responseDate'] = gmdate( 'Y-m-d H:i:s' ); // Track age for 30-day expiry
796 }
797 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
798
799 // Integrity Check: We need to store the checksum of the messages sent by the client.
800 $stored_messages = $client_messages;
801 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
802 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
803 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
804 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
805
806 // Actions
807 $actions = [];
808 if ( $reply->needClientActions ) {
809 foreach ( $reply->needClientActions as $action ) {
810 $actions[] = [
811 'type' => 'function',
812 'data' => [
813 'name' => $action['function']->name,
814 'args' => $action['arguments']
815 ]
816 ];
817 }
818 }
819
820 $restRes = [
821 'reply' => $rawText,
822 'chatId' => $this->core->fix_chat_id( $query, $params ),
823 'images' => $reply->get_type() === 'images' ? $reply->results : null,
824 'actions' => $actions,
825 'usage' => $reply->usage
826 ];
827
828 // Add debug events if collected
829 if ( !empty( $debugEvents ) ) {
830 $restRes['debugEvents'] = $debugEvents;
831 }
832
833 // Add response ID if available (for Responses API)
834 if ( !empty( $reply->id ) ) {
835 $restRes['responseId'] = $reply->id;
836 }
837
838 // Process Reply
839 if ( $stream ) {
840 $final_res = $this->build_final_res(
841 $botId,
842 $newMessage,
843 $newFileId,
844 $params,
845 $restRes['reply'],
846 $restRes['images'],
847 $restRes['actions'],
848 $restRes['usage'],
849 $restRes['responseId'] ?? null
850 );
851 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
852 die();
853 }
854 else {
855 return $restRes;
856 }
857
858 }
859 catch ( Exception $e ) {
860 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
861 if ( $stream ) {
862 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
863 die();
864 }
865 else {
866 throw $e;
867 }
868 }
869 }
870
871 public function inject_chat() {
872 $params = $this->core->get_chatbot( $this->siteWideChatId );
873 $clean_params = [];
874 if ( !empty( $params ) ) {
875 $clean_params['window'] = true;
876 $clean_params['id'] = $this->siteWideChatId;
877 echo $this->chat_shortcode( $clean_params );
878 }
879 return null;
880 }
881
882 public function build_front_params( $botId, $customId, $crossSite = false ) {
883 $frontSystem = [
884 'botId' => ( $customId && $customId !== '' ) ? null : sanitize_text_field( $botId ),
885 'customId' => ( $customId && $customId !== '' ) ? sanitize_text_field( $customId ) : null,
886 'userData' => $this->core->get_user_data(),
887 'sessionId' => $this->core->get_session_id(),
888 // IMPORTANT: REST nonce handling differs by user state:
889 // - Logged-in users: get_nonce() returns a user-specific nonce created in current session context
890 // - Logged-out users: get_nonce() returns null, they'll fetch via /start_session endpoint
891 // This prevents rest_cookie_invalid_nonce errors for logged-in users by ensuring the nonce
892 // matches their authentication context from the start.
893 'restNonce' => $crossSite ? null : $this->core->get_nonce(),
894 'contextId' => get_the_ID(),
895 'pluginUrl' => MWAI_URL,
896 'restUrl' => untrailingslashit( get_rest_url() ),
897 'stream' => $this->core->get_option( 'ai_streaming' ),
898 'debugMode' => $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ),
899 'eventLogs' => $this->core->get_option( 'event_logs' ),
900 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
901 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
902 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
903 'crossSite' => $crossSite
904 ];
905 return $frontSystem;
906 }
907
908 public function resolveBotInfo( &$atts ) {
909 $chatbot = null;
910 $botId = $atts['id'] ?? null;
911 $customId = $atts['custom_id'] ?? null;
912 $parentBotId = null;
913
914 if ( !$botId && !$customId ) {
915 $botId = 'default';
916 }
917 if ( $botId ) {
918 $chatbot = $this->core->get_chatbot( $botId );
919 if ( !$chatbot ) {
920 $botId = $botId ?: 'N/A';
921 $safe_botId = esc_html( $botId );
922 return [
923 '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'.",
924 ];
925 }
926 }
927 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
928
929 if ( !empty( $customId ) ) {
930 if ( $botId !== null ) {
931 $parentBotId = $botId;
932 $botId = null;
933 }
934 }
935
936 unset( $atts['id'] );
937 return [
938 'chatbot' => $chatbot,
939 'botId' => $botId,
940 'customId' => $customId,
941 'parentBotId' => $parentBotId
942 ];
943 }
944
945 public function chat_shortcode( $atts ) {
946 $atts = empty( $atts ) ? [] : $atts;
947
948 foreach ( $atts as $key => $value ) {
949 $atts[ $key ] = urldecode( $value );
950 }
951
952 // Let the user override the chatbot params
953 $atts = apply_filters( 'mwai_chatbot_params', $atts );
954
955 // Resolve the bot info
956 $resolvedBot = $this->resolveBotInfo( $atts );
957 if ( isset( $resolvedBot['error'] ) ) {
958 return $resolvedBot['error'];
959 }
960 $chatbot = $resolvedBot['chatbot'];
961 $botId = $resolvedBot['botId'];
962 $customId = $resolvedBot['customId'];
963 $parentBotId = $resolvedBot['parentBotId'];
964
965 // Rename the keys of the atts into camelCase to match the internal params system.
966 $atts = array_map( function ( $key, $value ) {
967 $key = str_replace( '_', ' ', $key );
968 $key = ucwords( $key );
969 $key = str_replace( ' ', '', $key );
970 $key = lcfirst( $key );
971 return [ $key => $value ];
972 }, array_keys( $atts ), $atts );
973 $atts = array_merge( ...$atts );
974
975 if ( !empty( $parentBotId ) ) {
976 $atts['parentBotId'] = $parentBotId;
977 }
978
979 $frontParams = [];
980 // Define text parameters that need sanitization (excluding those that support HTML)
981 $textParams = ['aiName', 'userName', 'guestName', 'textSend', 'textClear', 'textInputPlaceholder',
982 'startSentence', 'iconText', 'iconAlt', 'headerSubtitle', 'popupTitle'];
983 // Parameters that support HTML content
984 $htmlParams = ['textCompliance'];
985 // Boolean parameters that need special handling
986 $booleanParams = ['window', 'copyButton', 'fullscreen', 'localMemory', 'iconBubble', 'centerOpen',
987 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch'];
988
989 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
990 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
991 if ( isset( $atts[$param] ) ) {
992 if ( $param === 'localMemory' ) {
993 $frontParams[$param] = $atts[$param] === 'true';
994 }
995 else if ( in_array( $param, $textParams ) ) {
996 // Sanitize text parameters to prevent XSS
997 $frontParams[$param] = sanitize_text_field( $atts[$param] );
998 }
999 else if ( in_array( $param, $htmlParams ) ) {
1000 // For HTML parameters, use wp_kses_post to allow safe HTML
1001 $frontParams[$param] = wp_kses_post( $atts[$param] );
1002 }
1003 else if ( in_array( $param, $booleanParams ) ) {
1004 // Convert to proper boolean
1005 // Handle various boolean representations from shortcode attributes
1006 $value = $atts[$param];
1007 if ( is_bool( $value ) ) {
1008 $frontParams[$param] = $value;
1009 } else if ( is_string( $value ) ) {
1010 $frontParams[$param] = !empty( $value ) && $value !== 'false' && $value !== '0' && $value !== 'no';
1011 } else {
1012 $frontParams[$param] = !empty( $value );
1013 }
1014 }
1015 else {
1016 $frontParams[$param] = $atts[$param];
1017 }
1018 }
1019 // If not, let's use the chatbot's default values
1020 else if ( isset( $chatbot[$param] ) ) {
1021 if ( in_array( $param, $booleanParams ) ) {
1022 // Convert to proper boolean for chatbot defaults too
1023 // Handle various boolean representations
1024 $value = $chatbot[$param];
1025
1026 if ( is_bool( $value ) ) {
1027 $frontParams[$param] = $value;
1028 } else if ( is_string( $value ) ) {
1029 $frontParams[$param] = !empty( $value ) && $value !== 'false' && $value !== '0';
1030 } else {
1031 $frontParams[$param] = !empty( $value );
1032 }
1033 }
1034 else {
1035 $frontParams[$param] = $chatbot[$param];
1036 }
1037 }
1038
1039 // Apply the placeholders
1040 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
1041 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
1042 }
1043 }
1044
1045 // Server Params
1046 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
1047 // we are using the default or a specific chatbot.
1048 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
1049
1050 // Parameters that are purely visual/UI and shouldn't trigger custom ID
1051 $visualOnlyParams = [
1052 // Bot selectors
1053 'id', 'custom_id',
1054 // System-added params
1055 'crossSite',
1056 // Visual/UI parameters that don't affect AI behavior
1057 'aiName', 'userName', 'guestName', // Display names
1058 'aiAvatar', 'userAvatar', 'guestAvatar', 'aiAvatarUrl', 'userAvatarUrl', 'guestAvatarUrl', // Avatars
1059 'textSend', 'textClear', 'textInputPlaceholder', 'textCompliance', // UI text labels
1060 'textInputMaxLength', // Input constraint (visual)
1061 'themeId', // Theme selection
1062 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', // Window/icon settings
1063 'centerOpen', 'width', 'openDelay', 'iconBubble', 'windowAnimation', 'fullscreen', // Window behavior
1064 'copyButton', 'headerSubtitle', 'popupTitle', // UI features
1065 'containerType', 'headerType', 'messagesType', 'inputType', 'footerType' // UI style variants
1066 ];
1067
1068 // Remove visual-only params from override detection
1069 $attsForOverrideCheck = array_diff_key( $atts, array_flip( $visualOnlyParams ) );
1070
1071 // Only these front params affect behavior and should trigger custom ID:
1072 // - mode: chat vs. prompt mode
1073 // - startSentence: initial AI message
1074 // - localMemory: affects data persistence
1075 // - imageUpload, fileUpload, multiUpload, fileSearch: affect capabilities
1076 $behavioralFrontParams = ['mode', 'startSentence', 'localMemory', 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch'];
1077
1078 $hasServerOverrides = count( array_intersect( array_keys( $attsForOverrideCheck ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
1079 $hasBehavioralFrontOverrides = count( array_intersect( array_keys( $attsForOverrideCheck ), $behavioralFrontParams ) ) > 0;
1080 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasBehavioralFrontOverrides );
1081
1082 $serverParams = [];
1083 if ( $hasOverrides ) {
1084 // Server parameters don't need sanitization as they're processed server-side
1085 // and not rendered in HTML. They may contain code, HTML, etc. for AI context.
1086 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
1087 if ( isset( $atts[$param] ) ) {
1088 $serverParams[$param] = $atts[$param];
1089 }
1090 else {
1091 // For custom chatbots, don't inherit embeddingsEnvId from the default chatbot
1092 if ( $param === 'embeddingsEnvId' && !empty( $customId ) ) {
1093 $serverParams[$param] = '';
1094 }
1095 else {
1096 $serverParams[$param] = $chatbot[$param] ?? null;
1097 }
1098 }
1099 }
1100 }
1101
1102 // Front Params
1103 $frontSystem = $this->build_front_params( $botId, $customId );
1104
1105 // Clean Params
1106 $frontParams = $this->clean_params( $frontParams );
1107 $frontSystem = $this->clean_params( $frontSystem );
1108 $serverParams = $this->clean_params( $serverParams );
1109
1110 // Server-side: Keep the System Params
1111 if ( $hasOverrides ) {
1112 if ( empty( $customId ) ) {
1113 $customId = md5( json_encode( $serverParams ) );
1114 $frontSystem['customId'] = $customId;
1115 }
1116 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
1117 }
1118
1119 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
1120 $filterParams = [
1121 'step' => 'init',
1122 'botId' => $botId,
1123 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
1124 ];
1125 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
1126 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
1127 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
1128 $frontSystem['actions'] = $this->sanitize_actions( $actions );
1129 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
1130 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
1131
1132 // Client-side: Prepare JSON for Front Params and System Params
1133 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1134 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1135 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1136 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1137 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
1138
1139 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
1140
1141 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1142 }
1143
1144 public function chatbot_discussions( $atts ) {
1145 $atts = empty( $atts ) ? [] : $atts;
1146
1147 // Resolve the bot info
1148 $resolvedBot = $this->resolveBotInfo( $atts );
1149 if ( isset( $resolvedBot['error'] ) ) {
1150 return $resolvedBot['error'];
1151 }
1152 $chatbot = $resolvedBot['chatbot'];
1153 $botId = $resolvedBot['botId'];
1154 $customId = $resolvedBot['customId'];
1155
1156 // Rename the keys of the atts into camelCase to match the internal params system.
1157 $atts = array_map( function ( $key, $value ) {
1158 $key = str_replace( '_', ' ', $key );
1159 $key = ucwords( $key );
1160 $key = str_replace( ' ', '', $key );
1161 $key = lcfirst( $key );
1162 return [ $key => $value ];
1163 }, array_keys( $atts ), $atts );
1164 $atts = array_merge( ...$atts );
1165
1166 // Front Params
1167 $frontParams = [];
1168 // All discussion params are text params that need sanitization
1169 $textParams = ['textNewChat'];
1170
1171 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
1172 if ( isset( $atts[$param] ) ) {
1173 // Sanitize text parameters
1174 $frontParams[$param] = in_array( $param, $textParams ) ? sanitize_text_field( $atts[$param] ) : $atts[$param];
1175 }
1176 else if ( isset( $chatbot[$param] ) ) {
1177 $frontParams[$param] = $chatbot[$param];
1178 }
1179 }
1180
1181 // Server Params
1182 $serverParams = [];
1183 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
1184 if ( isset( $atts[$param] ) ) {
1185 $serverParams[$param] = $atts[$param];
1186 }
1187 }
1188
1189 // Front System
1190 $frontSystem = $this->build_front_params( $botId, $customId );
1191 // Get refresh interval from settings
1192 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
1193 if ( $refresh_interval === 'Never' ) {
1194 $frontSystem['refreshInterval'] = 0;
1195 }
1196 elseif ( $refresh_interval === 'Manual' ) {
1197 $frontSystem['refreshInterval'] = -1;
1198 }
1199 elseif ( is_numeric( $refresh_interval ) ) {
1200 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
1201 }
1202 else {
1203 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
1204 }
1205 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
1206
1207 // Get paging setting
1208 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
1209 if ( $paging_option === 'None' ) {
1210 $frontSystem['paging'] = 0; // No pagination
1211 }
1212 else {
1213 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
1214 }
1215
1216 // Get metadata settings
1217 $frontSystem['metadata'] = [
1218 'enabled' => $this->core->get_option( 'chatbot_discussions_metadata_enabled' ),
1219 'startDate' => $this->core->get_option( 'chatbot_discussions_metadata_start_date' ),
1220 'lastUpdate' => $this->core->get_option( 'chatbot_discussions_metadata_last_update' ),
1221 'messageCount' => $this->core->get_option( 'chatbot_discussions_metadata_message_count' )
1222 ];
1223
1224 // Clean Params
1225 $frontParams = $this->clean_params( $frontParams );
1226 $frontSystem = $this->clean_params( $frontSystem );
1227 $serverParams = $this->clean_params( $serverParams );
1228
1229 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1230 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1231 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1232 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1233
1234 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1235 }
1236
1237 public function clean_params( &$params ) {
1238 foreach ( $params as $param => $value ) {
1239 if ( $param === 'restNonce' ) {
1240 continue;
1241 }
1242 // Skip only if value is null or an array - but not if it's false or 0
1243 if ( is_null( $value ) || is_array( $value ) ) {
1244 continue;
1245 }
1246 // Handle empty strings
1247 if ( $value === '' ) {
1248 continue;
1249 }
1250 $lowerCaseValue = is_string( $value ) ? strtolower( $value ) : '';
1251 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
1252 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
1253 }
1254 else if ( is_numeric( $value ) ) {
1255 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
1256 }
1257 }
1258 return $params;
1259 }
1260
1261 }
1262