PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.0.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.0.4
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 10 months ago discussions.php 11 months ago files.php 11 months ago forms-manager.php 10 months ago gdpr.php 11 months ago search.php 1 year ago security.php 1 year ago tasks.php 1 year ago wand.php 10 months ago
chatbot.php
1210 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' ] );
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 $query->inject_params( $params );
566
567 $storeId = null;
568 if ( $mode === 'assistant' ) {
569 $chatId = $params['chatId'] ?? null;
570 if ( !empty( $chatId ) && $this->core->discussions ) {
571 $discussion = $this->core->discussions->get_discussion( $query->botId, $chatId );
572 if ( isset( $discussion['storeId'] ) ) {
573 $storeId = $discussion['storeId'];
574 $query->setStoreId( $storeId );
575 }
576 }
577 }
578
579 // Support for Multiple Uploaded Files
580 $filesToProcess = [];
581 if ( !empty( $newFileIds ) && is_array( $newFileIds ) ) {
582 $filesToProcess = $newFileIds;
583 }
584 elseif ( !empty( $newFileId ) ) {
585 $filesToProcess[] = $newFileId;
586 }
587
588 // Support for Uploaded Image/Files
589 if ( !empty( $filesToProcess ) ) {
590 // For now, we only process the first file to maintain backward compatibility
591 // TODO: In the future, we could support multiple files in the query
592 $fileToProcess = $filesToProcess[0];
593
594 // Get extension and mime type
595 $isImage = $this->core->files->is_image( $fileToProcess );
596
597 if ( $mode === 'assistant' && !$isImage ) {
598 $url = $this->core->files->get_path( $fileToProcess );
599 $data = $this->core->files->get_data( $fileToProcess );
600 $openai = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
601 $filename = basename( $url );
602
603 // Upload the file
604 $file = $openai->upload_file( $filename, $data, 'assistants' );
605
606 // Create a store
607 if ( empty( $storeId ) ) {
608 $chatbotName = 'mwai_' . strtolower( !empty( $chatbot['name'] ) ? $chatbot['name'] : 'default' );
609 if ( !empty( $query->chatId ) ) {
610 $chatbotName .= '_' . $query->chatId;
611 }
612 $metadata = [];
613 if ( !empty( $chatbot['assistantId'] ) ) {
614 $metadata['assistantId'] = $chatbot['assistantId'];
615 }
616 if ( !empty( $query->chatId ) ) {
617 $metadata['chatId'] = $query->chatId;
618 }
619 $expiry = $this->core->get_option( 'image_expires' );
620 $storeId = $openai->create_vector_store( $chatbotName, $expiry, $metadata );
621 $query->setStoreId( $storeId );
622 }
623
624 // Add the file to the store - wait a moment for store to be ready
625 sleep( 1 );
626 $storeFileId = $openai->add_vector_store_file( $storeId, $file['id'] );
627
628 if ( empty( $storeFileId ) ) {
629 throw new Exception( 'Failed to add file to vector store.' );
630 }
631
632 // Update the local file with the OpenAI RefId, StoreId and StoreFileId
633 $openAiRefId = $file['id'];
634 $internalFileId = $this->core->files->get_id_from_refId( $fileToProcess );
635 $this->core->files->update_refId( $internalFileId, $openAiRefId );
636 $this->core->files->update_envId( $internalFileId, $query->envId );
637 $this->core->files->update_purpose( $internalFileId, 'assistant-in' );
638 $this->core->files->add_metadata( $internalFileId, 'assistant_storeId', $storeId );
639 $this->core->files->add_metadata( $internalFileId, 'assistant_storeFileId', $storeFileId );
640 $fileToProcess = $openAiRefId;
641 $scope = $params['fileSearch'];
642 if ( $scope === 'discussion' || $scope === 'user' || $scope === 'assistant' ) {
643 $id = $this->core->files->get_id_from_refId( $fileToProcess );
644 $this->core->files->add_metadata( $id, 'assistant_scope', $scope );
645 }
646 }
647 else {
648 $url = $this->core->files->get_url( $fileToProcess );
649 $mimeType = $this->core->files->get_mime_type( $fileToProcess );
650 $isIMG = in_array( $mimeType, [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ] );
651 $purposeType = $isIMG ? 'vision' : 'files';
652 $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, $purposeType, $mimeType ) );
653 $fileId = $this->core->files->get_id_from_refId( $fileToProcess );
654 $this->core->files->update_envId( $fileId, $query->envId );
655 $this->core->files->update_purpose( $fileId, $purposeType );
656 $this->core->files->add_metadata( $fileId, 'query_envId', $query->envId );
657 $this->core->files->add_metadata( $fileId, 'query_session', $query->session );
658 }
659 }
660
661 // Takeover
662 $takeoverAnswer = apply_filters( 'mwai_chatbot_takeover', null, $query, $params );
663 if ( !empty( $takeoverAnswer ) ) {
664 $rawText = apply_filters( 'mwai_chatbot_reply', $takeoverAnswer, $query, $params, [] );
665 return [
666 'reply' => $rawText,
667 'chatId' => $this->core->fix_chat_id( $query, $params ),
668 'images' => null,
669 'actions' => [],
670 'usage' => null
671 ];
672 }
673
674 // Moderation
675 $moderationEnabled = $this->core->get_option( 'module_moderation' ) &&
676 $this->core->get_option( 'shortcode_chat_moderation' );
677 if ( $moderationEnabled ) {
678 global $mwai;
679 $isFlagged = $mwai->moderationCheck( $query->get_message() );
680 if ( $isFlagged ) {
681 throw new Exception( 'Sorry, your message has been rejected by moderation.' );
682 }
683 }
684
685 // Setup streaming if enabled (before embeddings to capture those events)
686 $streamCallback = null;
687 $debugEvents = [];
688
689 if ( $stream ) {
690 $streamCallback = function ( $reply ) use ( $query ) {
691 // Support both legacy string data and new Event objects
692 if ( is_string( $reply ) ) {
693 $this->core->stream_push( [ 'type' => 'live', 'data' => $reply ], $query );
694 }
695 else {
696 $this->core->stream_push( $reply, $query );
697 }
698 };
699 if ( headers_sent( $filename, $linenum ) ) {
700 throw new Exception( "Headers already sent in $filename on line $linenum. Cannot start streaming." );
701 }
702 header( 'Cache-Control: no-cache' );
703 header( 'Content-Type: text/event-stream' );
704 // This is useful to disable buffering in nginx through headers.
705 header( 'X-Accel-Buffering: no' );
706 ob_implicit_flush( true );
707 if ( ob_get_level() > 0 ) {
708 ob_end_flush();
709 }
710 }
711 else if ( $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ) ) {
712 // For non-streaming debug mode, collect events
713 $streamCallback = function ( $event ) use ( &$debugEvents ) {
714 if ( is_object( $event ) && method_exists( $event, 'toArray' ) ) {
715 $debugEvents[] = $event->toArray();
716 }
717 };
718 }
719
720 // Awareness & Embeddings
721 $context = $this->core->retrieve_context( $params, $query, $streamCallback );
722 if ( !empty( $context ) ) {
723 $query->set_context( $context['content'] );
724 }
725
726 // Function Aware
727 $query = apply_filters( 'mwai_chatbot_query', $query, $params );
728 }
729
730 // Process Query
731
732 $reply = $this->core->run_query( $query, $streamCallback, true );
733 $rawText = $reply->result;
734 $extra = [];
735 if ( $context ) {
736 $extra = [ 'embeddings' => isset( $context['embeddings'] ) ? $context['embeddings'] : null ];
737 }
738 // Store response ID for Responses API stateful conversations
739 // CRITICAL: Must store even when function calls are present
740 // This enables the feedback query to use previous_response_id
741 if ( !empty( $reply->id ) ) {
742 $extra['responseId'] = $reply->id;
743 $extra['responseDate'] = gmdate( 'Y-m-d H:i:s' ); // Track age for 30-day expiry
744 }
745 $rawText = apply_filters( 'mwai_chatbot_reply', $rawText, $query, $params, $extra );
746
747 // Integrity Check: We need to store the checksum of the messages sent by the client.
748 $stored_messages = $client_messages;
749 $stored_messages[] = [ 'role' => 'user', 'content' => $newMessage ];
750 $stored_messages[] = [ 'role' => 'assistant', 'content' => $rawText ];
751 $stored_checksum = $this->calculate_messages_checksum( $stored_messages );
752 set_transient( $checksum_key, $stored_checksum, 60 * 60 * 24 * 30 );
753
754 // Actions
755 $actions = [];
756 if ( $reply->needClientActions ) {
757 foreach ( $reply->needClientActions as $action ) {
758 $actions[] = [
759 'type' => 'function',
760 'data' => [
761 'name' => $action['function']->name,
762 'args' => $action['arguments']
763 ]
764 ];
765 }
766 }
767
768 $restRes = [
769 'reply' => $rawText,
770 'chatId' => $this->core->fix_chat_id( $query, $params ),
771 'images' => $reply->get_type() === 'images' ? $reply->results : null,
772 'actions' => $actions,
773 'usage' => $reply->usage
774 ];
775
776 // Add debug events if collected
777 if ( !empty( $debugEvents ) ) {
778 $restRes['debugEvents'] = $debugEvents;
779 }
780
781 // Add response ID if available (for Responses API)
782 if ( !empty( $reply->id ) ) {
783 $restRes['responseId'] = $reply->id;
784 }
785
786 // Process Reply
787 if ( $stream ) {
788 $final_res = $this->build_final_res(
789 $botId,
790 $newMessage,
791 $newFileId,
792 $params,
793 $restRes['reply'],
794 $restRes['images'],
795 $restRes['actions'],
796 $restRes['usage'],
797 $restRes['responseId'] ?? null
798 );
799 $this->core->stream_push( [ 'type' => 'end', 'data' => json_encode( $final_res ) ], $query );
800 die();
801 }
802 else {
803 return $restRes;
804 }
805
806 }
807 catch ( Exception $e ) {
808 $message = apply_filters( 'mwai_ai_exception', $e->getMessage() );
809 if ( $stream ) {
810 $this->core->stream_push( [ 'type' => 'error', 'data' => $message ], $query );
811 die();
812 }
813 else {
814 throw $e;
815 }
816 }
817 }
818
819 public function inject_chat() {
820 $params = $this->core->get_chatbot( $this->siteWideChatId );
821 $clean_params = [];
822 if ( !empty( $params ) ) {
823 $clean_params['window'] = true;
824 $clean_params['id'] = $this->siteWideChatId;
825 echo $this->chat_shortcode( $clean_params );
826 }
827 return null;
828 }
829
830 public function build_front_params( $botId, $customId, $crossSite = false ) {
831 $frontSystem = [
832 'botId' => ( $customId && $customId !== '' ) ? null : sanitize_text_field( $botId ),
833 'customId' => ( $customId && $customId !== '' ) ? sanitize_text_field( $customId ) : null,
834 'userData' => $this->core->get_user_data(),
835 'sessionId' => $this->core->get_session_id(),
836 // IMPORTANT: REST nonce handling differs by user state:
837 // - Logged-in users: get_nonce() returns a user-specific nonce created in current session context
838 // - Logged-out users: get_nonce() returns null, they'll fetch via /start_session endpoint
839 // This prevents rest_cookie_invalid_nonce errors for logged-in users by ensuring the nonce
840 // matches their authentication context from the start.
841 'restNonce' => $crossSite ? null : $this->core->get_nonce(),
842 'contextId' => get_the_ID(),
843 'pluginUrl' => MWAI_URL,
844 'restUrl' => untrailingslashit( get_rest_url() ),
845 'stream' => $this->core->get_option( 'ai_streaming' ),
846 'debugMode' => $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' ),
847 'eventLogs' => $this->core->get_option( 'event_logs' ),
848 'speech_recognition' => $this->core->get_option( 'speech_recognition' ),
849 'speech_synthesis' => $this->core->get_option( 'speech_synthesis' ),
850 'typewriter' => $this->core->get_option( 'chatbot_typewriter' ),
851 'crossSite' => $crossSite
852 ];
853 return $frontSystem;
854 }
855
856 public function resolveBotInfo( &$atts ) {
857 $chatbot = null;
858 $botId = $atts['id'] ?? null;
859 $customId = $atts['custom_id'] ?? null;
860 $parentBotId = null;
861
862 if ( !$botId && !$customId ) {
863 $botId = 'default';
864 }
865 if ( $botId ) {
866 $chatbot = $this->core->get_chatbot( $botId );
867 if ( !$chatbot ) {
868 $botId = $botId ?: 'N/A';
869 $safe_botId = esc_html( $botId );
870 return [
871 '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'.",
872 ];
873 }
874 }
875 $chatbot = $chatbot ?: $this->core->get_chatbot( 'default' );
876
877 if ( !empty( $customId ) ) {
878 if ( $botId !== null ) {
879 $parentBotId = $botId;
880 $botId = null;
881 }
882 }
883
884 unset( $atts['id'] );
885 return [
886 'chatbot' => $chatbot,
887 'botId' => $botId,
888 'customId' => $customId,
889 'parentBotId' => $parentBotId
890 ];
891 }
892
893 public function chat_shortcode( $atts ) {
894 $atts = empty( $atts ) ? [] : $atts;
895
896 foreach ( $atts as $key => $value ) {
897 $atts[ $key ] = urldecode( $value );
898 }
899
900 // Let the user override the chatbot params
901 $atts = apply_filters( 'mwai_chatbot_params', $atts );
902
903 // Resolve the bot info
904 $resolvedBot = $this->resolveBotInfo( $atts );
905 if ( isset( $resolvedBot['error'] ) ) {
906 return $resolvedBot['error'];
907 }
908 $chatbot = $resolvedBot['chatbot'];
909 $botId = $resolvedBot['botId'];
910 $customId = $resolvedBot['customId'];
911 $parentBotId = $resolvedBot['parentBotId'];
912
913 // Rename the keys of the atts into camelCase to match the internal params system.
914 $atts = array_map( function ( $key, $value ) {
915 $key = str_replace( '_', ' ', $key );
916 $key = ucwords( $key );
917 $key = str_replace( ' ', '', $key );
918 $key = lcfirst( $key );
919 return [ $key => $value ];
920 }, array_keys( $atts ), $atts );
921 $atts = array_merge( ...$atts );
922
923 if ( !empty( $parentBotId ) ) {
924 $atts['parentBotId'] = $parentBotId;
925 }
926
927 $frontParams = [];
928 // Define text parameters that need sanitization (excluding those that support HTML)
929 $textParams = ['aiName', 'userName', 'guestName', 'textSend', 'textClear', 'textInputPlaceholder',
930 'startSentence', 'iconText', 'iconAlt', 'headerSubtitle', 'popupTitle'];
931 // Parameters that support HTML content
932 $htmlParams = ['textCompliance'];
933 // Boolean parameters that need special handling
934 $booleanParams = ['window', 'copyButton', 'fullscreen', 'localMemory', 'iconBubble', 'centerOpen',
935 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch'];
936
937 foreach ( MWAI_CHATBOT_FRONT_PARAMS as $param ) {
938 // Let's go through the overriden or custom params first (the ones passed in the shortcode)
939 if ( isset( $atts[$param] ) ) {
940 if ( $param === 'localMemory' ) {
941 $frontParams[$param] = $atts[$param] === 'true';
942 }
943 else if ( in_array( $param, $textParams ) ) {
944 // Sanitize text parameters to prevent XSS
945 $frontParams[$param] = sanitize_text_field( $atts[$param] );
946 }
947 else if ( in_array( $param, $htmlParams ) ) {
948 // For HTML parameters, use wp_kses_post to allow safe HTML
949 $frontParams[$param] = wp_kses_post( $atts[$param] );
950 }
951 else if ( in_array( $param, $booleanParams ) ) {
952 // Convert to proper boolean
953 // Handle various boolean representations from shortcode attributes
954 $value = $atts[$param];
955 if ( is_bool( $value ) ) {
956 $frontParams[$param] = $value;
957 } else if ( is_string( $value ) ) {
958 $frontParams[$param] = !empty( $value ) && $value !== 'false' && $value !== '0' && $value !== 'no';
959 } else {
960 $frontParams[$param] = !empty( $value );
961 }
962 }
963 else {
964 $frontParams[$param] = $atts[$param];
965 }
966 }
967 // If not, let's use the chatbot's default values
968 else if ( isset( $chatbot[$param] ) ) {
969 if ( in_array( $param, $booleanParams ) ) {
970 // Convert to proper boolean for chatbot defaults too
971 // Handle various boolean representations
972 $value = $chatbot[$param];
973
974 if ( is_bool( $value ) ) {
975 $frontParams[$param] = $value;
976 } else if ( is_string( $value ) ) {
977 $frontParams[$param] = !empty( $value ) && $value !== 'false' && $value !== '0';
978 } else {
979 $frontParams[$param] = !empty( $value );
980 }
981 }
982 else {
983 $frontParams[$param] = $chatbot[$param];
984 }
985 }
986
987 // Apply the placeholders
988 if ( in_array( $param, ['startSentence', 'iconText'] ) ) {
989 $frontParams[$param] = $this->core->do_placeholders( $frontParams[$param] );
990 }
991 }
992
993 // Server Params
994 // NOTE: We don't need the server params for the chatbot if there are no overrides, it means
995 // we are using the default or a specific chatbot.
996 $isSiteWide = $this->siteWideChatId && $botId === $this->siteWideChatId;
997
998 // Parameters that are purely visual/UI and shouldn't trigger custom ID
999 $visualOnlyParams = [
1000 // Bot selectors
1001 'id', 'custom_id',
1002 // System-added params
1003 'crossSite',
1004 // Visual/UI parameters that don't affect AI behavior
1005 'aiName', 'userName', 'guestName', // Display names
1006 'aiAvatar', 'userAvatar', 'guestAvatar', 'aiAvatarUrl', 'userAvatarUrl', 'guestAvatarUrl', // Avatars
1007 'textSend', 'textClear', 'textInputPlaceholder', 'textCompliance', // UI text labels
1008 'textInputMaxLength', // Input constraint (visual)
1009 'themeId', // Theme selection
1010 'window', 'icon', 'iconText', 'iconTextDelay', 'iconAlt', 'iconPosition', // Window/icon settings
1011 'centerOpen', 'width', 'openDelay', 'iconBubble', 'windowAnimation', 'fullscreen', // Window behavior
1012 'copyButton', 'headerSubtitle', 'popupTitle', // UI features
1013 'containerType', 'headerType', 'messagesType', 'inputType', 'footerType' // UI style variants
1014 ];
1015
1016 // Remove visual-only params from override detection
1017 $attsForOverrideCheck = array_diff_key( $atts, array_flip( $visualOnlyParams ) );
1018
1019 // Only these front params affect behavior and should trigger custom ID:
1020 // - mode: chat vs. prompt mode
1021 // - startSentence: initial AI message
1022 // - localMemory: affects data persistence
1023 // - imageUpload, fileUpload, multiUpload, fileSearch: affect capabilities
1024 $behavioralFrontParams = ['mode', 'startSentence', 'localMemory', 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch'];
1025
1026 $hasServerOverrides = count( array_intersect( array_keys( $attsForOverrideCheck ), MWAI_CHATBOT_SERVER_PARAMS ) ) > 0;
1027 $hasBehavioralFrontOverrides = count( array_intersect( array_keys( $attsForOverrideCheck ), $behavioralFrontParams ) ) > 0;
1028 $hasOverrides = !$isSiteWide && ( $hasServerOverrides || $hasBehavioralFrontOverrides );
1029
1030 $serverParams = [];
1031 if ( $hasOverrides ) {
1032 // Server parameters don't need sanitization as they're processed server-side
1033 // and not rendered in HTML. They may contain code, HTML, etc. for AI context.
1034 foreach ( MWAI_CHATBOT_SERVER_PARAMS as $param ) {
1035 if ( isset( $atts[$param] ) ) {
1036 $serverParams[$param] = $atts[$param];
1037 }
1038 else {
1039 // For custom chatbots, don't inherit embeddingsEnvId from the default chatbot
1040 if ( $param === 'embeddingsEnvId' && !empty( $customId ) ) {
1041 $serverParams[$param] = '';
1042 }
1043 else {
1044 $serverParams[$param] = $chatbot[$param] ?? null;
1045 }
1046 }
1047 }
1048 }
1049
1050 // Front Params
1051 $frontSystem = $this->build_front_params( $botId, $customId );
1052
1053 // Clean Params
1054 $frontParams = $this->clean_params( $frontParams );
1055 $frontSystem = $this->clean_params( $frontSystem );
1056 $serverParams = $this->clean_params( $serverParams );
1057
1058 // Server-side: Keep the System Params
1059 if ( $hasOverrides ) {
1060 if ( empty( $customId ) ) {
1061 $customId = md5( json_encode( $serverParams ) );
1062 $frontSystem['customId'] = $customId;
1063 }
1064 set_transient( 'mwai_custom_chatbot_' . $customId, $serverParams, 60 * 60 * 24 );
1065 }
1066
1067 // Retrieve the actions, shortcuts, and blocks we want to inject at the beginning
1068 $filterParams = [
1069 'step' => 'init',
1070 'botId' => $botId,
1071 'params' => array_merge( $frontParams, $frontSystem, $serverParams )
1072 ];
1073 $actions = apply_filters( 'mwai_chatbot_actions', [], $filterParams );
1074 $blocks = apply_filters( 'mwai_chatbot_blocks', [], $filterParams );
1075 $shortcuts = apply_filters( 'mwai_chatbot_shortcuts', [], $filterParams );
1076 $frontSystem['actions'] = $this->sanitize_actions( $actions );
1077 $frontSystem['blocks'] = $this->sanitize_blocks( $blocks );
1078 $frontSystem['shortcuts'] = $this->sanitize_shortcuts( $shortcuts );
1079
1080 // Client-side: Prepare JSON for Front Params and System Params
1081 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1082 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1083 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1084 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1085 //$jsonAttributes = htmlspecialchars(json_encode($atts), ENT_QUOTES, 'UTF-8');
1086
1087 $this->enqueue_scripts( $frontParams['themeId'] ?? null );
1088
1089 return "<div class='mwai-chatbot-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1090 }
1091
1092 public function chatbot_discussions( $atts ) {
1093 $atts = empty( $atts ) ? [] : $atts;
1094
1095 // Resolve the bot info
1096 $resolvedBot = $this->resolveBotInfo( $atts );
1097 if ( isset( $resolvedBot['error'] ) ) {
1098 return $resolvedBot['error'];
1099 }
1100 $chatbot = $resolvedBot['chatbot'];
1101 $botId = $resolvedBot['botId'];
1102 $customId = $resolvedBot['customId'];
1103
1104 // Rename the keys of the atts into camelCase to match the internal params system.
1105 $atts = array_map( function ( $key, $value ) {
1106 $key = str_replace( '_', ' ', $key );
1107 $key = ucwords( $key );
1108 $key = str_replace( ' ', '', $key );
1109 $key = lcfirst( $key );
1110 return [ $key => $value ];
1111 }, array_keys( $atts ), $atts );
1112 $atts = array_merge( ...$atts );
1113
1114 // Front Params
1115 $frontParams = [];
1116 // All discussion params are text params that need sanitization
1117 $textParams = ['textNewChat'];
1118
1119 foreach ( MWAI_DISCUSSIONS_FRONT_PARAMS as $param ) {
1120 if ( isset( $atts[$param] ) ) {
1121 // Sanitize text parameters
1122 $frontParams[$param] = in_array( $param, $textParams ) ? sanitize_text_field( $atts[$param] ) : $atts[$param];
1123 }
1124 else if ( isset( $chatbot[$param] ) ) {
1125 $frontParams[$param] = $chatbot[$param];
1126 }
1127 }
1128
1129 // Server Params
1130 $serverParams = [];
1131 foreach ( MWAI_DISCUSSIONS_SERVER_PARAMS as $param ) {
1132 if ( isset( $atts[$param] ) ) {
1133 $serverParams[$param] = $atts[$param];
1134 }
1135 }
1136
1137 // Front System
1138 $frontSystem = $this->build_front_params( $botId, $customId );
1139 // Get refresh interval from settings
1140 $refresh_interval = $this->core->get_option( 'chatbot_discussions_refresh_interval' );
1141 if ( $refresh_interval === 'Never' ) {
1142 $frontSystem['refreshInterval'] = 0;
1143 }
1144 elseif ( $refresh_interval === 'Manual' ) {
1145 $frontSystem['refreshInterval'] = -1;
1146 }
1147 elseif ( is_numeric( $refresh_interval ) ) {
1148 $frontSystem['refreshInterval'] = intval( $refresh_interval ) * 1000; // Convert to milliseconds
1149 }
1150 else {
1151 $frontSystem['refreshInterval'] = 5000; // Default to 5 seconds
1152 }
1153 $frontSystem['refreshInterval'] = apply_filters( 'mwai_discussions_refresh_interval', $frontSystem['refreshInterval'] );
1154
1155 // Get paging setting
1156 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
1157 if ( $paging_option === 'None' ) {
1158 $frontSystem['paging'] = 0; // No pagination
1159 }
1160 else {
1161 $frontSystem['paging'] = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Default to 10
1162 }
1163
1164 // Get metadata settings
1165 $frontSystem['metadata'] = [
1166 'enabled' => $this->core->get_option( 'chatbot_discussions_metadata_enabled' ),
1167 'startDate' => $this->core->get_option( 'chatbot_discussions_metadata_start_date' ),
1168 'lastUpdate' => $this->core->get_option( 'chatbot_discussions_metadata_last_update' ),
1169 'messageCount' => $this->core->get_option( 'chatbot_discussions_metadata_message_count' )
1170 ];
1171
1172 // Clean Params
1173 $frontParams = $this->clean_params( $frontParams );
1174 $frontSystem = $this->clean_params( $frontSystem );
1175 $serverParams = $this->clean_params( $serverParams );
1176
1177 $theme = isset( $frontParams['themeId'] ) ? $this->core->get_theme( $frontParams['themeId'] ) : null;
1178 $jsonFrontParams = htmlspecialchars( json_encode( $frontParams ), ENT_QUOTES, 'UTF-8' );
1179 $jsonFrontSystem = htmlspecialchars( json_encode( $frontSystem ), ENT_QUOTES, 'UTF-8' );
1180 $jsonFrontTheme = htmlspecialchars( json_encode( $theme ), ENT_QUOTES, 'UTF-8' );
1181
1182 return "<div class='mwai-discussions-container' data-params='{$jsonFrontParams}' data-system='{$jsonFrontSystem}' data-theme='{$jsonFrontTheme}'></div>";
1183 }
1184
1185 public function clean_params( &$params ) {
1186 foreach ( $params as $param => $value ) {
1187 if ( $param === 'restNonce' ) {
1188 continue;
1189 }
1190 // Skip only if value is null or an array - but not if it's false or 0
1191 if ( is_null( $value ) || is_array( $value ) ) {
1192 continue;
1193 }
1194 // Handle empty strings
1195 if ( $value === '' ) {
1196 continue;
1197 }
1198 $lowerCaseValue = is_string( $value ) ? strtolower( $value ) : '';
1199 if ( $lowerCaseValue === 'true' || $lowerCaseValue === 'false' || is_bool( $value ) ) {
1200 $params[$param] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
1201 }
1202 else if ( is_numeric( $value ) ) {
1203 $params[$param] = filter_var( $value, FILTER_VALIDATE_FLOAT );
1204 }
1205 }
1206 return $params;
1207 }
1208
1209 }
1210