PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.2.9
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.2.9
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 / core.php
ai-engine / classes Last commit date
data 1 year ago engines 6 months ago exceptions 1 year ago modules 6 months ago query 6 months ago rest 8 months ago services 6 months ago admin.php 7 months ago api.php 6 months ago core.php 6 months ago discussion.php 1 year ago event.php 1 year ago init.php 7 months ago logging.php 1 year ago reply.php 6 months ago rest.php 6 months ago
core.php
1599 lines
1 <?php
2
3 require_once( MWAI_PATH . '/vendor/autoload.php' );
4 require_once( MWAI_PATH . '/constants/init.php' );
5
6 define( 'MWAI_IMG_WAND', MWAI_URL . '/images/wand.png' );
7 define( 'MWAI_IMG_WAND_HTML', "<img style='height: 22px; margin-bottom: -5px; margin-right: 8px;'
8 src='" . MWAI_IMG_WAND . "' alt='AI Wand' />" );
9 define( 'MWAI_IMG_WAND_HTML_XS', "<img style='height: 16px; margin-bottom: -2px;'
10 src='" . MWAI_IMG_WAND . "' alt='AI Wand' />" );
11
12 class Meow_MWAI_Core {
13 public $admin = null;
14 public $is_rest = false;
15 public $is_cli = false;
16 public $site_url = null;
17 public $files = null;
18 public $tasks = null;
19 public $magicWand = null;
20 private $options = null;
21 private $option_name = 'mwai_options';
22 private $themes_option_name = 'mwai_themes';
23 private $chatbots_option_name = 'mwai_chatbots';
24 private $nonce = null;
25
26 public $chatbot = null;
27 public $discussions = null;
28 public $search = null;
29 public $advisor = null;
30
31 // Service instances for improved architecture
32 public $responseIdManager = null;
33 public $messageBuilder = null;
34 public $sessionService = null;
35 public $imageService = null;
36 public $usageStatsService = null;
37 public $modelEnvironmentService = null;
38
39 public function __construct() {
40 Meow_MWAI_Logging::init( 'mwai_options', 'AI Engine' );
41 $this->site_url = get_site_url();
42 $this->is_rest = MeowKit_MWAI_Helpers::is_rest();
43 $this->is_cli = defined( 'WP_CLI' );
44 $this->files = new Meow_MWAI_Modules_Files( $this );
45 $this->tasks = new Meow_MWAI_Modules_Tasks( $this );
46 $this->advisor = new Meow_MWAI_Modules_Advisor( $this );
47
48 // Load task examples in Dev Mode
49 if ( $this->get_option( 'dev_mode' ) ) {
50 new Meow_MWAI_Modules_Tasks_Examples( $this );
51 }
52
53 add_action( 'plugins_loaded', [ $this, 'init' ] );
54 add_action( 'wp_register_script', [ $this, 'register_scripts' ] );
55 add_action( 'wp_enqueue_scripts', [ $this, 'register_scripts' ] );
56 add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ] );
57 }
58
59 #region Init & Scripts
60 public function init() {
61 global $mwai;
62 $this->chatbot = null;
63 $this->discussions = null;
64
65 // Initialize services here after autoloader is ready
66 $this->responseIdManager = new Meow_MWAI_Services_ResponseIdManager( $this );
67 $this->messageBuilder = new Meow_MWAI_Services_MessageBuilder( $this );
68 $this->sessionService = new Meow_MWAI_Services_Session( $this );
69 $this->imageService = new Meow_MWAI_Services_Image( $this );
70 $this->usageStatsService = new Meow_MWAI_Services_UsageStats( $this );
71 $this->modelEnvironmentService = new Meow_MWAI_Services_ModelEnvironment( $this );
72
73 // Start session early if needed for REST requests
74 if ( $this->is_rest && $this->sessionService->can_start_session() ) {
75 session_start();
76 }
77
78 new Meow_MWAI_Modules_Security( $this );
79
80 // REST API
81 if ( $this->is_rest ) {
82 new Meow_MWAI_Rest( $this );
83 }
84
85 // WP Admin
86 if ( is_admin() ) {
87 new Meow_MWAI_Admin( $this );
88 }
89
90 // GDPR Module
91 if ( $this->get_option( 'chatbot_gdpr_consent' ) ) {
92 new Meow_MWAI_Modules_GDPR( $this );
93 }
94
95 // Suggestions Module
96 if ( $this->get_option( 'module_suggestions' ) && ( is_admin() || $this->is_rest ) ) {
97 $this->magicWand = new Meow_MWAI_Modules_Wand( $this );
98 }
99
100 // Chatbots & Discussions
101 if ( $this->get_option( 'module_chatbots' ) ) {
102 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
103 // Only instantiate discussions if the feature is enabled
104 if ( $this->get_option( 'chatbot_discussions' ) ) {
105 $this->discussions = new Meow_MWAI_Modules_Discussions();
106 }
107 }
108
109 // Search
110 if ( $this->get_option( 'module_search' ) ) {
111 $this->search = new Meow_MWAI_Modules_Search( $this );
112 }
113
114 // Forms Manager (standalone Forms UI + shortcode renderer)
115 if ( $this->get_option( 'module_forms' ) ) {
116 new Meow_MWAI_Modules_Forms_Manager( $this );
117 }
118
119 // Advanced Core
120 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
121 new MeowPro_MWAI_Core( $this );
122 }
123
124 // Simple API
125 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions ?? null );
126
127 // MCP
128 if ( $this->get_option( 'module_mcp' ) ) {
129 new Meow_MWAI_Labs_MCP( $this );
130
131 // Core - Core WordPress MCP tools
132 if ( $this->get_option( 'mcp_core' ) ) {
133 new Meow_MWAI_Labs_MCP_Core( $this );
134 }
135
136 // Dynamic REST - WordPress REST API MCP tools
137 if ( $this->get_option( 'mcp_dynamic_rest' ) ) {
138 require_once MWAI_PATH . '/labs/mcp-rest.php';
139 new Meow_MWAI_Labs_MCP_Rest();
140 }
141
142 // Themes - Pro theme management MCP tools
143 if ( $this->get_option( 'mcp_themes' ) && class_exists( 'MeowPro_MWAI_MCP_Theme' ) ) {
144 new MeowPro_MWAI_MCP_Theme( $this );
145 }
146
147 // Plugins - Pro plugin management MCP tools
148 if ( $this->get_option( 'mcp_plugins' ) && class_exists( 'MeowPro_MWAI_MCP_Plugin' ) ) {
149 new MeowPro_MWAI_MCP_Plugin( $this );
150 }
151 }
152 }
153
154 public function register_scripts() {
155 // Register Highlight.js
156 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
157 // Register CSS for the themes
158 $themes = $this->get_themes();
159 foreach ( $themes as $theme ) {
160 if ( $theme['type'] === 'internal' ) {
161 $themeId = $theme['themeId'];
162 $filename = $themeId . '.css';
163 $physical_file = trailingslashit( MWAI_PATH ) . 'themes/' . $filename;
164 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
165 wp_register_style( 'mwai_chatbot_theme_' . $themeId, trailingslashit( MWAI_URL )
166 . 'themes/' . $filename, [], $cache_buster );
167 }
168 }
169 }
170
171 public function enqueue_theme( $themeId ) {
172 if ( empty( $themeId ) ) {
173 return;
174 }
175 wp_enqueue_style( "mwai_chatbot_theme_$themeId" );
176 }
177
178 public function enqueue_themes() {
179 $themes = $this->get_themes();
180 foreach ( $themes as $theme ) {
181 if ( $theme['type'] === 'internal' ) {
182 $this->enqueue_theme( $theme['themeId'] );
183 }
184 }
185 }
186
187 #endregion
188
189 #region Roles & Capabilities
190 public function can_start_session() {
191 return $this->sessionService->can_start_session();
192 }
193
194 public function can_access_settings() {
195 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
196 }
197
198 public function can_access_features() {
199 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
200 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
201 }
202
203 public function can_access_public_api( $feature, $extra ) {
204 $logged_in = is_user_logged_in();
205 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
206 }
207 #endregion
208
209 #region AI-Related Helpers
210 public function run_query( $query, $streamCallback = null, $markdown = false ) {
211
212 // Allow to modify the query before it is sent.
213 // Different query types have specific filters for type-safe modifications.
214 if ( $query instanceof Meow_MWAI_Query_Embed ) {
215 $query = apply_filters( 'mwai_ai_embeddings_query', $query );
216 }
217 else if ( $query instanceof Meow_MWAI_Query_Feedback ) {
218 $query = apply_filters( 'mwai_ai_feedback_query', $query );
219 }
220 else {
221 $query = apply_filters( 'mwai_ai_query', $query );
222 }
223
224 // Ensure the query is still valid after filtering
225 if ( !$query || !is_object( $query ) ) {
226 throw new Exception( __( 'Invalid query object after filtering. The mwai_ai_query filter must return a valid query object.', 'ai-engine' ) );
227 }
228
229 // Validate that embeddings queries have a non-empty message
230 if ( $query instanceof Meow_MWAI_Query_Embed && empty( $query->get_message() ) ) {
231 throw new Exception( __( 'Embeddings query cannot have an empty message. Please check that the conversation context is properly extracted.', 'ai-engine' ) );
232 }
233
234 // Let's check the default environment and model.
235 $this->validate_env_model( $query );
236
237 // Create the engine based on the query's environment
238 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
239
240 // Let's run the query.
241 $reply = $engine->run( $query, $streamCallback );
242
243 // Let's allow to modify the reply before it is sent.
244 if ( $markdown ) {
245 if ( $query instanceof Meow_MWAI_Query_Image || $query instanceof Meow_MWAI_Query_EditImage ) {
246 $reply->result = '';
247 foreach ( $reply->results as $result ) {
248 $reply->result .= "![Image]($result)\n";
249 }
250 }
251 }
252
253 return $reply;
254 }
255
256 public function validate_env_model( $query ) {
257 return $this->modelEnvironmentService->validate_env_model( $query );
258 }
259
260 #endregion
261
262 #region Text-Related Helpers
263
264 // Clean the text perfectly, resolve shortcodes, etc, etc.
265 public function clean_text( $rawText = '' ) {
266 $text = html_entity_decode( $rawText );
267 $text = wp_strip_all_tags( $text );
268 $text = preg_replace( '/[\r\n]+/', "\n", $text );
269 $text = preg_replace( '/\n+/', "\n", $text );
270 $text = preg_replace( '/\t+/', "\t", $text );
271 return $text . ' ';
272 }
273
274 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
275 public function clean_sentences( $text, $maxLength = null ) {
276 // Step 1: Identify URLs and replace them with a placeholder.
277 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
278 preg_match_all( $urlPattern, $text, $urls );
279 $urlPlaceholders = [];
280 foreach ( $urls[0] as $index => $url ) {
281 $placeholder = '{urlPlaceholder' . $index . '}';
282 $text = str_replace( $url, $placeholder, $text );
283 $urlPlaceholders[$placeholder] = $url;
284 }
285
286 $maxLength = (int) ( $maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ) );
287 $sentences = preg_split( '/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY );
288 $hashes = [];
289 $uniqueSentences = [];
290 $total = 0;
291
292 foreach ( $sentences as $sentence ) {
293 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
294 $hash = md5( $sentence );
295 if ( !in_array( $hash, $hashes ) ) {
296 $length = mb_strlen( $sentence, 'UTF-8' );
297 if ( $total + $length > $maxLength ) {
298 continue;
299 }
300 $hashes[] = $hash;
301 $uniqueSentences[] = $sentence;
302 $total += $length;
303 }
304 }
305
306 $freshText = implode( ' ', $uniqueSentences );
307
308 // Step 3: Restore URLs in the final text.
309 foreach ( $urlPlaceholders as $placeholder => $url ) {
310 $freshText = str_replace( $placeholder, $url, $freshText );
311 }
312
313 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
314 return $freshText;
315 }
316
317 public function get_post_content( $postId ) {
318 // Ensure we get fresh post data by clearing cache
319 clean_post_cache( $postId );
320 $post = get_post( $postId );
321 if ( !$post ) {
322 return false;
323 }
324 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
325 $pattern = '/\[mwai_.*?\]/';
326 $text = preg_replace( $pattern, '', $text );
327 if ( $this->get_option( 'resolve_shortcodes' ) ) {
328 $text = apply_filters( 'the_content', $text );
329 }
330 else {
331 $pattern = "/\[[^\]]+\]/";
332 $text = preg_replace( $pattern, '', $text );
333 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
334 $text = preg_replace( $pattern, '', $text );
335 }
336 $text = $this->clean_text( $text );
337 $text = $this->clean_sentences( $text );
338 $text = apply_filters( 'mwai_post_content', $text, $postId );
339 return $text;
340 }
341
342 public function markdown_to_html( $content ) {
343 $Parsedown = new Parsedown();
344 $content = $Parsedown->text( $content );
345 return $content;
346 }
347
348 public function get_post_language( $postId ) {
349 $locale = get_locale();
350 $code = strtolower( substr( $locale, 0, 2 ) );
351 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
352 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
353 if ( !empty( $lang ) ) {
354 $locale = $lang['locale'];
355 $humanLanguage = $lang['display_name'];
356 }
357 return strtolower( "$locale ($humanLanguage)" );
358 }
359
360 public function do_placeholders( $text ) {
361 $defaultPlaceholders = [
362 // Date and time in a clear, AI-friendly format (e.g., "December 11, 2025 at 3:45 PM")
363 'DATE_TIME' => date_i18n( 'F j, Y \a\t g:i A' ),
364 ];
365 $dataPlaceholders = $this->get_user_data();
366 if ( !empty( $dataPlaceholders ) ) {
367 $defaultPlaceholders = array_merge( $defaultPlaceholders, $dataPlaceholders );
368 }
369 $placeholders = apply_filters( 'mwai_placeholders', $defaultPlaceholders );
370 foreach ( $placeholders as $key => $value ) {
371 $text = str_replace( '{' . $key . '}', $value ?? '', $text );
372 }
373 // Replace any remaining unmatched placeholders with a clear indicator
374 $text = preg_replace( '/\{[A-Z_]+\}/', '[N/A]', $text );
375 return $text;
376 }
377 #endregion
378
379 #region Security Helpers
380 /**
381 * Sanitize file path to prevent PHAR deserialization attacks.
382 * Strips dangerous stream wrappers that could trigger object injection.
383 *
384 * @param string $path The file path to sanitize.
385 * @return string The sanitized file path.
386 * @throws Exception If a dangerous stream wrapper is detected.
387 */
388 public static function sanitize_file_path( $path ) {
389 if ( empty( $path ) || !is_string( $path ) ) {
390 return $path;
391 }
392
393 // List of dangerous stream wrappers that could trigger deserialization
394 $dangerous_wrappers = [
395 'phar://',
396 'php://',
397 'zip://',
398 'zlib://',
399 'data://',
400 'glob://',
401 'rar://',
402 'ogg://',
403 'expect://'
404 ];
405
406 // Check if the path contains any dangerous wrappers
407 $lower_path = strtolower( $path );
408 foreach ( $dangerous_wrappers as $wrapper ) {
409 if ( strpos( $lower_path, $wrapper ) === 0 ) {
410 throw new Exception( 'Invalid file path: Stream wrappers are not allowed for security reasons.' );
411 }
412 }
413
414 return $path;
415 }
416 #endregion
417
418 #region Image-Related Helpers
419 public static function is_image( $file ) {
420 global $mwai_core;
421 if ( $mwai_core && $mwai_core->imageService ) {
422 return $mwai_core->imageService->is_image( $file );
423 }
424 // Fallback to original implementation if service not available
425 $mimeType = self::get_mime_type( $file );
426 if ( strpos( $mimeType, 'image' ) !== false ) {
427 return true;
428 }
429 return false;
430 }
431
432 public static function get_image_resolution( $url ) {
433 global $mwai_core;
434 if ( $mwai_core && $mwai_core->imageService ) {
435 return $mwai_core->imageService->get_image_resolution( $url );
436 }
437 // Fallback to original implementation if service not available
438 if ( empty( $url ) ) {
439 return null;
440 }
441 $headers = get_headers( $url, 1 );
442 if ( strpos( $headers[0], '200' ) === false ) {
443 return null;
444 }
445 $image_info = getimagesize( $url );
446 if ( $image_info === false ) {
447 return null;
448 }
449 return [
450 'width' => $image_info[0],
451 'height' => $image_info[1]
452 ];
453 }
454
455 public static function get_mime_type( $file ) {
456 global $mwai_core;
457 if ( $mwai_core && $mwai_core->imageService ) {
458 return $mwai_core->imageService->get_mime_type( $file );
459 }
460
461 // Fallback implementation - this should rarely be used as imageService is initialized early
462 Meow_MWAI_Logging::warn( 'get_mime_type called before imageService is available' );
463
464 // Basic extension-based detection only
465 $extension = pathinfo( $file, PATHINFO_EXTENSION );
466 $extension = strtolower( $extension );
467 $mimeTypes = [
468 'jpg' => 'image/jpeg',
469 'jpeg' => 'image/jpeg',
470 'png' => 'image/png',
471 'gif' => 'image/gif',
472 'webp' => 'image/webp',
473 'bmp' => 'image/bmp',
474 'tiff' => 'image/tiff',
475 'tif' => 'image/tiff',
476 'svg' => 'image/svg+xml',
477 'ico' => 'image/x-icon',
478 'pdf' => 'application/pdf',
479 ];
480 return isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
481 }
482
483 public function download_image( $url ) {
484 return $this->imageService->download_image( $url );
485 }
486
487 /**
488 * Add an image from a URL to the Media Library.
489 * @param string $url The URL of the image to be downloaded.
490 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
491 * @param string $title The title of the image.
492 * @param string $description The description of the image.
493 * @param string $caption The caption of the image.
494 * @param string $alt The alt text of the image.
495 * @return int The attachment ID of the image.
496 */
497 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null, $attachedPost = null, $post_status = 'inherit', $post_type = 'attachment', $ai_metadata = [] ) {
498 return $this->imageService->add_image_from_url( $url, $filename, $title, $description, $caption, $alt, $attachedPost, $post_status, $post_type, $ai_metadata );
499 }
500 #endregion
501
502 #region Context-Related Helpers
503 public function retrieve_context( $params, $query, $streamCallback = null ) {
504 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
505 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
506
507 $context = apply_filters( 'mwai_context_search', [], $query, [
508 'embeddingsEnvId' => $embeddingsEnvId,
509 'streamCallback' => $streamCallback
510 ] );
511
512 // Emit embeddings event if streaming and context was found
513 if ( $streamCallback && !empty( $context ) ) {
514 $count = 0;
515 if ( isset( $context['embeddings'] ) && is_array( $context['embeddings'] ) ) {
516 $count = count( $context['embeddings'] );
517 }
518 else if ( isset( $context['content'] ) ) {
519 $count = 1;
520 }
521 if ( $count > 0 ) {
522 $event = Meow_MWAI_Event::embeddings( $count );
523 $streamCallback( $event );
524 }
525 }
526
527 if ( empty( $context ) ) {
528 return null;
529 }
530 else if ( !isset( $context['content'] ) ) {
531 Meow_MWAI_Logging::warn( 'A context without content was returned.' );
532 return null;
533 }
534 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
535 $context['length'] = strlen( $context['content'] );
536 return $context;
537 }
538 #endregion
539
540 #region Users/Sessions Helpers
541
542 public function get_nonce( $force = false ) {
543 return $this->sessionService->get_nonce( $force );
544 }
545
546 // This is a bit hacky, but chatId needs to be retrieved or generated.
547 // Maybe we can clean this up later.
548 public function fix_chat_id( $query, $params ) {
549 return $this->sessionService->fix_chat_id( $query, $params );
550 }
551
552 public function get_session_id() {
553 return $this->sessionService->get_session_id();
554 }
555
556 /**
557 * Get the Response ID Manager service
558 */
559 public function get_response_id_manager() {
560 return $this->responseIdManager;
561 }
562
563 /**
564 * Get the Message Builder service
565 */
566 public function get_message_builder() {
567 return $this->messageBuilder;
568 }
569
570 // Get the UserID from the data, or from the current user
571 public function get_user_id( $data = null ) {
572 return $this->sessionService->get_user_id( $data );
573 }
574
575 public function get_session_user_id() {
576 return $this->sessionService->get_session_user_id();
577 }
578
579 public function get_admin_user() {
580 return $this->sessionService->get_admin_user();
581 }
582
583 public function get_user_data() {
584 return $this->sessionService->get_user_data();
585 }
586
587 public function get_ip_address( $force = false ) {
588 return $this->sessionService->get_ip_address( $force );
589 }
590
591 #endregion
592
593 #region Sanitization
594 public function sanitize_sort(
595 &$sort,
596 $default_accessor = 'created',
597 $default_order = 'DESC',
598 $allowed_columns = [ 'created', 'updated', 'name', 'id', 'time', 'units', 'price' ]
599 ) {
600
601 // Ensure $sort is an array
602 if ( !is_array( $sort ) ) {
603 $sort = [ 'accessor' => $default_accessor, 'by' => $default_order ];
604 }
605 // Extract and sanitize the accessor
606 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
607 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
608 Meow_MWAI_Logging::error( "This sort accessor is not allowed ($sort_accessor)." );
609 $sort_accessor = $default_accessor;
610 }
611 // Extract and sanitize the sort order
612 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
613 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
614 Meow_MWAI_Logging::error( "This sort order is not allowed ($sort_by)." );
615 $sort_by = $default_order;
616 }
617 // Update the sort array with sanitized values
618 $sort['accessor'] = $sort_accessor;
619 $sort['by'] = $sort_by;
620 }
621 #endregion
622
623 #region Other Helpers
624 public function safe_strlen( $string, $encoding = 'UTF-8' ) {
625 if ( function_exists( 'mb_strlen' ) ) {
626 return mb_strlen( $string, $encoding );
627 }
628 else {
629 // Fallback implementation for environments without mbstring extension
630 return preg_match_all( '/./u', $string, $matches );
631 }
632 }
633
634 public function check_rest_nonce( $request ) {
635 // REST NONCE VERIFICATION:
636 // Validates nonce from X-WP-Nonce header using WordPress nonce system.
637 // Returns: false (invalid), 1 (0-12 hours old), or 2 (12-24 hours old)
638 // WordPress REST permission callbacks accept any truthy value as success.
639 // The filter allows custom authorization logic if needed.
640 $nonce = $request->get_header( 'X-WP-Nonce' );
641 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
642 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
643 }
644
645 public function get_random_id( $length = 8, $excludeIds = [] ) {
646 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
647 $charactersLength = strlen( $characters );
648 $randomId = '';
649 for ( $i = 0; $i < $length; $i++ ) {
650 $randomId .= $characters[ mt_rand( 0, $charactersLength - 1 ) ];
651 }
652 if ( in_array( $randomId, $excludeIds ) ) {
653 return $this->get_random_id( $length, $excludeIds );
654 }
655 return $randomId;
656 }
657
658 public function is_url( $url ) {
659 return strpos( $url, 'http' ) === 0 ? true : false;
660 }
661
662 public function get_post_types() {
663 $excluded = [ 'attachment', 'revision', 'nav_menu_item' ];
664 $post_types = [];
665 $types = get_post_types( [], 'objects' );
666
667 // Let's get the Post Types that are enabled for Embeddings Sync
668 $embeddingsSettings = $this->get_option( 'embeddings' );
669 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
670
671 foreach ( $types as $type ) {
672 $forced = in_array( $type->name, $syncPostTypes );
673 // Should not be excluded.
674 if ( !$forced && in_array( $type->name, $excluded ) ) {
675 continue;
676 }
677 // Should be public.
678 if ( !$forced && !$type->public ) {
679 continue;
680 }
681 $post_types[] = [
682 'name' => $type->labels->name,
683 'type' => $type->name,
684 ];
685 }
686
687 // Let's get the Post Types that are enabled for Embeddings Sync
688 $embeddingsSettings = $this->get_option( 'embeddings' );
689 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
690
691 return $post_types;
692 }
693
694 public function get_post( $post ) {
695 if ( is_numeric( $post ) ) {
696 // Force fresh retrieval to avoid cache issues
697 clean_post_cache( $post );
698 $post = get_post( $post );
699 }
700 if ( is_object( $post ) ) {
701 $post = (array) $post;
702 }
703 if ( !is_array( $post ) ) {
704 return null;
705 }
706 $language = $this->get_post_language( $post['ID'] );
707 $content = $this->get_post_content( $post['ID'] );
708 $title = $post['post_title'];
709 $excerpt = $post['post_excerpt'];
710 $url = get_permalink( $post['ID'] );
711 $checksum = wp_hash( $content . $title . $url );
712
713 return [
714 'postId' => (int) $post['ID'],
715 'title' => $title,
716 'content' => $content,
717 'excerpt' => $excerpt,
718 'url' => $url,
719 'language' => $language ?? 'english',
720 'checksum' => $checksum,
721 ];
722 }
723
724 /**
725 * Format a date/time string into a human-readable format
726 * @param string $date_string The date string to format
727 * @return string Formatted date (e.g., "Just now", "5m ago", "2h ago", "3d ago", "Jan 20th")
728 */
729 public function format_discussion_date( $date_string ) {
730 $date = strtotime( $date_string );
731 $now = time();
732 $diff = $now - $date;
733
734 // Less than a minute
735 if ( $diff < 60 ) {
736 return 'Just now';
737 }
738
739 // Less than an hour
740 if ( $diff < 3600 ) {
741 $minutes = floor( $diff / 60 );
742 return $minutes . 'm ago';
743 }
744
745 // Less than a day
746 if ( $diff < 86400 ) {
747 $hours = floor( $diff / 3600 );
748 return $hours . 'h ago';
749 }
750
751 // Less than a week
752 if ( $diff < 604800 ) {
753 $days = floor( $diff / 86400 );
754 return $days . 'd ago';
755 }
756
757 // Format as date
758 $is_current_year = date( 'Y', $date ) === date( 'Y', $now );
759 if ( $is_current_year ) {
760 return date( 'M jS', $date );
761 }
762 else {
763 return date( 'M jS, Y', $date );
764 }
765 }
766 #endregion
767
768 #region Usage & Costs
769
770 // Quick and dirty token estimation
771 // Let's keep this synchronized with Helpers in JS
772 public static function estimate_tokens( ...$args ): int {
773 global $mwai_core;
774 if ( $mwai_core && $mwai_core->usageStatsService ) {
775 return $mwai_core->usageStatsService->estimate_tokens( ...$args );
776 }
777 // Fallback to original implementation if service not available
778 $text = '';
779 foreach ( $args as $arg ) {
780 if ( is_array( $arg ) ) {
781 foreach ( $arg as $message ) {
782 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : '';
783 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : '';
784 }
785 }
786 else if ( is_string( $arg ) ) {
787 $text .= $arg;
788 }
789 }
790 $averageTokenLength = 4;
791 $words = preg_split( '/\s+/', trim( $text ) );
792 $tokenCount = 0;
793 foreach ( $words as $word ) {
794 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
795 }
796 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
797 }
798
799 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
800 return $this->usageStatsService->record_tokens_usage( $model, $in_tokens, $out_tokens, $returned_price );
801 }
802
803 public function record_audio_usage( $model, $seconds ) {
804 return $this->usageStatsService->record_audio_usage( $model, $seconds );
805 }
806
807 public function record_images_usage( $model, $resolution, $images ) {
808 return $this->usageStatsService->record_images_usage( $model, $resolution, $images );
809 }
810
811 public function record_videos_usage( $model, $resolution, $seconds ) {
812 return $this->usageStatsService->record_videos_usage( $model, $resolution, $seconds );
813 }
814
815 #endregion
816
817 #region Streaming
818 public function stream_push( $data, $query = null ) {
819 try {
820 // Handle new Event objects
821 if ( is_object( $data ) && method_exists( $data, 'to_array' ) ) {
822 $data = $data->to_array();
823 }
824
825 $data = apply_filters( 'mwai_stream_push', $data, $query );
826 $out = 'data: ' . json_encode( $data );
827 echo $out;
828 echo "\n\n";
829 if ( ob_get_level() > 0 ) {
830 ob_end_flush();
831 }
832 flush();
833 }
834 catch ( Exception $e ) {
835 // Send error as proper SSE error event
836 $errorMessage = 'Oops! Something went wrong on the server. Please try again, and if you are the site developer, check the PHP Error Logs for details.';
837 error_log( '[AI Engine Stream Error] ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
838
839 $errorData = [
840 'type' => 'error',
841 'data' => $errorMessage
842 ];
843 $out = 'data: ' . json_encode( $errorData );
844 echo $out;
845 echo "\n\n";
846 if ( ob_get_level() > 0 ) {
847 ob_end_flush();
848 }
849 flush();
850
851 // Stop execution after sending error
852 die();
853 }
854 }
855 #endregion
856
857 #region Options
858 public function get_themes() {
859 $themes = get_option( $this->themes_option_name, [] );
860 $themes = empty( $themes ) ? [] : $themes;
861
862 $internalThemes = [
863 'chatgpt' => [
864 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
865 'settings' => [], 'style' => ''
866 ],
867 'messages' => [
868 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
869 'settings' => [], 'style' => ''
870 ],
871 'timeless' => [
872 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
873 'settings' => [], 'style' => ''
874 ],
875 ];
876 $customThemes = [];
877 foreach ( $themes as $theme ) {
878 if ( isset( $internalThemes[$theme['themeId']] ) ) {
879 $internalThemes[$theme['themeId']] = $theme;
880 continue;
881 }
882 $customThemes[] = $theme;
883 }
884 return array_merge( array_values( $internalThemes ), $customThemes );
885 }
886
887 public function update_themes( $themes ) {
888 update_option( $this->themes_option_name, $themes );
889 return $themes;
890 }
891
892 public function get_chatbots() {
893 $chatbots = get_option( $this->chatbots_option_name, [] );
894 $hasChanges = false;
895 if ( empty( $chatbots ) ) {
896 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
897 }
898 $hasDefault = false;
899 foreach ( $chatbots as &$chatbot ) {
900 if ( $chatbot['botId'] === 'default' ) {
901 $hasDefault = true;
902 }
903 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
904 // Use default value if not set.
905 if ( !isset( $chatbot[$key] ) ) {
906 $chatbot[$key] = $value;
907 }
908 }
909
910 /*
911 This is the best section to rename fields.
912 We did this in 2024 for context to instructions, and fileUpload to fileSearch. fileSearch is for assistant file search, and fileUpload is now for chatbot file upload (similar to vision, but for files instead of images).
913 */
914
915 // Migrate old file upload params to new unified system
916 if ( !isset( $chatbot['fileUpload'] ) ) {
917 // Set fileUpload based on old params (imageUpload, fileUploads, or vision checkbox)
918 $chatbot['fileUpload'] = !empty( $chatbot['imageUpload'] ) || ( isset( $chatbot['fileUploads'] ) && $chatbot['fileUploads'] > 0 );
919 $hasChanges = true;
920 }
921
922 // Ensure maxUploads is set
923 if ( !isset( $chatbot['maxUploads'] ) ) {
924 if ( isset( $chatbot['fileUploads'] ) && $chatbot['fileUploads'] > 0 ) {
925 $chatbot['maxUploads'] = $chatbot['fileUploads'];
926 }
927 else {
928 $chatbot['maxUploads'] = 1; // Default to 1 file
929 }
930 $hasChanges = true;
931 }
932
933 // Sync fileUploads with fileUpload and maxUploads for consistency
934 if ( isset( $chatbot['fileUpload'] ) ) {
935 $chatbot['fileUploads'] = $chatbot['fileUpload'] ? $chatbot['maxUploads'] : 0;
936 $chatbot['multiUpload'] = $chatbot['fileUpload'] && $chatbot['maxUploads'] > 1;
937 $chatbot['imageUpload'] = $chatbot['fileUpload']; // Keep imageUpload in sync
938 }
939
940 // if ( isset( $chatbot['context'] ) ) {
941 // $chatbot['instructions'] = $chatbot['context'];
942 // unset( $chatbot['context'] );
943 // $hasChanges = true;
944 // }
945 }
946 if ( !$hasDefault ) {
947 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
948 array_unshift( $chatbots, $defaultBot );
949 $hasChanges = true;
950 }
951 if ( $hasChanges ) {
952 update_option( $this->chatbots_option_name, $chatbots );
953 }
954 return $chatbots;
955 }
956
957 public function get_chatbot( $botId ) {
958 $chatbots = $this->get_chatbots();
959 foreach ( $chatbots as $chatbot ) {
960 if ( $chatbot['botId'] === (string) $botId ) {
961 return $chatbot;
962 }
963 }
964 return null;
965 }
966
967 public function get_embeddings_env( $envId ) {
968 return $this->modelEnvironmentService->get_embeddings_env( $envId );
969 }
970
971 public function get_ai_env( $envId ) {
972 return $this->modelEnvironmentService->get_ai_env( $envId );
973 }
974
975 public function get_assistant( $envId, $assistantId ) {
976 return $this->modelEnvironmentService->get_assistant( $envId, $assistantId );
977 }
978
979 public function get_theme( $themeId ) {
980 $themes = $this->get_themes();
981 foreach ( $themes as $theme ) {
982 if ( $theme['themeId'] === $themeId ) {
983 // Append custom CSS to theme data for frontend rendering (check for non-empty trimmed string)
984 if ( isset( $theme['settings']['customCSS'] ) && trim( $theme['settings']['customCSS'] ) !== '' ) {
985 $customCSS = $theme['settings']['customCSS'];
986
987 // Add theme class prefix to all CSS rules for proper scoping
988 $themeClass = '.mwai-' . $themeId . '-theme';
989 $lines = explode( "\n", $customCSS );
990 $processedCSS = '';
991 $inRule = false;
992
993 foreach ( $lines as $line ) {
994 $trimmedLine = trim( $line );
995
996 // Skip empty lines and comments
997 if ( empty( $trimmedLine ) || strpos( $trimmedLine, '/*' ) === 0 ) {
998 $processedCSS .= $line . "\n";
999 continue;
1000 }
1001
1002 // If line contains a selector (has { but not })
1003 if ( strpos( $line, '{' ) !== false && strpos( $line, '}' ) === false ) {
1004 // Extract selector and the rest
1005 $parts = explode( '{', $line, 2 );
1006 $selector = trim( $parts[0] );
1007
1008 // Add theme class prefix if not already present
1009 if ( strpos( $selector, $themeClass ) !== 0 ) {
1010 // Handle multiple selectors separated by comma
1011 $selectors = explode( ',', $selector );
1012 $prefixedSelectors = array_map( function ( $sel ) use ( $themeClass ) {
1013 $sel = trim( $sel );
1014 // Don't prefix if it's a keyframe or similar
1015 if ( strpos( $sel, '@' ) === 0 || strpos( $sel, 'from' ) === 0 || strpos( $sel, 'to' ) === 0 || preg_match( '/^\d+%/', $sel ) ) {
1016 return $sel;
1017 }
1018 return $themeClass . ' ' . $sel;
1019 }, $selectors );
1020 $selector = implode( ', ', $prefixedSelectors );
1021 }
1022
1023 $processedCSS .= $selector . ' {' . ( isset( $parts[1] ) ? $parts[1] : '' ) . "\n";
1024 $inRule = true;
1025 }
1026 else {
1027 $processedCSS .= $line . "\n";
1028 }
1029 }
1030
1031 $customCSS = $processedCSS;
1032
1033 // For custom themes (type: 'css'), append to style property
1034 if ( $theme['type'] === 'css' ) {
1035 $theme['style'] = ( $theme['style'] ?? '' ) . "\n\n/* Custom CSS */\n" . $customCSS;
1036 }
1037 // For internal themes, add customCSS as a separate property
1038 else {
1039 $theme['customCSS'] = $customCSS;
1040 }
1041 }
1042
1043 // Add CSS URL for cross-site and dynamic loading support
1044 // Internal themes can use physical file, custom themes use REST endpoint
1045 $theme_type = $theme['type'] ?? 'internal';
1046 if ( $theme_type === 'internal' ) {
1047 $theme['cssUrl'] = MWAI_URL . 'themes/' . $themeId . '.css';
1048 }
1049 else {
1050 // Custom themes use REST endpoint (requires Cross-Site module to be enabled)
1051 $theme['cssUrl'] = get_rest_url( null, 'mwai-ui/v1/cross-site/theme-css' ) . '?themeId=' . $themeId;
1052 }
1053
1054 return $theme;
1055 }
1056 }
1057 return null;
1058 }
1059
1060 public function update_chatbots( $chatbots ) {
1061 // TODO: Remove after January 2026 - Legacy chatbot fields cleanup
1062 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
1063 // TODO: I think some HTML fields are missing, guestName, maybe others.
1064 $htmlFields = [ 'instructions', 'textCompliance', 'aiName', 'userName', 'startSentence' ];
1065 $keepLineReturnsFields = [ 'instructions' ];
1066 $whiteSpacedFields = [ 'context' ];
1067 // Boolean fields that need proper conversion
1068 $booleanFields = [ 'window', 'copyButton', 'fullscreen', 'localMemory', 'iconBubble', 'centerOpen',
1069 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch', 'contentAware', 'aiAvatar', 'userAvatar', 'guestAvatar' ];
1070 foreach ( $chatbots as &$chatbot ) {
1071 foreach ( $chatbot as $key => &$value ) {
1072 if ( in_array( $key, $deprecatedFields ) ) {
1073 unset( $chatbot[$key] );
1074 continue;
1075 }
1076 if ( in_array( $key, $htmlFields ) ) {
1077 $value = wp_kses_post( $value );
1078 }
1079 else if ( in_array( $key, $whiteSpacedFields ) ) {
1080 $value = sanitize_textarea_field( $value );
1081 }
1082 else if ( in_array( $key, $booleanFields ) ) {
1083 // Convert various representations to boolean
1084 if ( is_bool( $value ) ) {
1085 // Already boolean, keep as is
1086 }
1087 else if ( $value === 1 || $value === '1' || $value === true || $value === 'true' || $value === 'yes' ) {
1088 // These are true values
1089 $value = true;
1090 }
1091 else if ( $value === 0 || $value === '0' || $value === false || $value === 'false' || $value === 'no' || $value === '' || $value === null ) {
1092 // These are false values
1093 $value = false;
1094 }
1095 else {
1096 // Default to checking if not empty
1097 $value = !empty( $value );
1098 }
1099 }
1100 else if ( $key === 'functions' ) {
1101 $functions = [];
1102 foreach ( $value as $function ) {
1103 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
1104 $functions[] = [
1105 'id' => sanitize_text_field( $function['id'] ),
1106 'type' => sanitize_text_field( $function['type'] ),
1107 ];
1108 }
1109 }
1110 $value = $functions;
1111 }
1112 else if ( $key === 'mcpServers' ) {
1113 $mcpServers = [];
1114 foreach ( $value as $server ) {
1115 if ( isset( $server['id'] ) ) {
1116 $mcpServers[] = [
1117 'id' => sanitize_text_field( $server['id'] ),
1118 ];
1119 }
1120 }
1121 $value = $mcpServers;
1122 }
1123 else if ( $key === 'tools' ) {
1124 // Sanitize tools array (web_search, image_generation, thinking, etc)
1125 $tools = [];
1126 if ( is_array( $value ) ) {
1127 foreach ( $value as $tool ) {
1128 $sanitized_tool = sanitize_text_field( $tool );
1129 if ( in_array( $sanitized_tool, ['web_search', 'image_generation', 'thinking', 'code_interpreter'] ) ) {
1130 $tools[] = $sanitized_tool;
1131 }
1132 }
1133 }
1134 $value = $tools;
1135 }
1136 else if ( $key === 'crossSite' ) {
1137 // Handle crossSite object
1138 $crossSite = [
1139 'enabled' => isset( $value['enabled'] ) ? (bool) $value['enabled'] : false,
1140 'allowedDomains' => []
1141 ];
1142 if ( isset( $value['allowedDomains'] ) && is_array( $value['allowedDomains'] ) ) {
1143 foreach ( $value['allowedDomains'] as $domain ) {
1144 $sanitized_domain = sanitize_text_field( $domain );
1145 if ( !empty( $sanitized_domain ) ) {
1146 $crossSite['allowedDomains'][] = $sanitized_domain;
1147 }
1148 }
1149 }
1150 $value = $crossSite;
1151 }
1152 else {
1153 if ( in_array( $key, $keepLineReturnsFields ) ) {
1154 $value = preg_replace( '/\r\n/', '[==LINE_RETURN==]', $value );
1155 $value = preg_replace( '/\n/', '[==LINE_RETURN==]', $value );
1156 }
1157 $value = sanitize_text_field( $value );
1158 if ( in_array( $key, $keepLineReturnsFields ) ) {
1159 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
1160 }
1161 }
1162 }
1163
1164 // Sync upload params to ensure consistency
1165 // fileUpload is the master control - respect its value
1166 $fileUploadEnabled = !empty( $chatbot['fileUpload'] ) || !empty( $chatbot['imageUpload'] );
1167 $maxFiles = isset( $chatbot['maxUploads'] ) && $chatbot['maxUploads'] > 0 ? (int) $chatbot['maxUploads'] : 1;
1168
1169 $chatbot['imageUpload'] = $fileUploadEnabled;
1170 $chatbot['fileUploads'] = $fileUploadEnabled ? $maxFiles : 0;
1171 $chatbot['multiUpload'] = $fileUploadEnabled && $maxFiles > 1;
1172 }
1173 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
1174 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
1175 $chatbots = get_option( $this->chatbots_option_name, [] );
1176 return $chatbots;
1177 }
1178 return $chatbots;
1179 }
1180
1181 public function populate_dynamic_options( $options ) {
1182 static $populating = false;
1183
1184 // Prevent infinite recursion
1185 if ( $populating ) {
1186 return $options;
1187 }
1188
1189 $populating = true;
1190
1191 // Languages - use custom languages as the complete list
1192 $custom_languages = isset( $options['custom_languages'] ) && !empty( $options['custom_languages'] )
1193 ? $options['custom_languages']
1194 : [];
1195
1196 // If no custom languages defined, fall back to defaults
1197 if ( empty( $custom_languages ) ) {
1198 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
1199 }
1200 else {
1201 // Process custom languages
1202 $processed_languages = [];
1203 foreach ( $custom_languages as $custom_lang ) {
1204 // Support formats like "Russian (ru)" or just "Russian"
1205 $custom_lang = trim( $custom_lang );
1206 if ( !empty( $custom_lang ) ) {
1207 // Check if language code is provided in parentheses
1208 if ( preg_match( '/^(.+)\s*\(([a-z]{2,3})\)$/i', $custom_lang, $matches ) ) {
1209 $lang_name = trim( $matches[1] );
1210 $lang_code = strtolower( trim( $matches[2] ) );
1211 $processed_languages[$lang_code] = $lang_name;
1212 }
1213 else {
1214 // No code provided, add as-is
1215 $processed_languages[] = $custom_lang;
1216 }
1217 }
1218 }
1219
1220 $options['languages'] = apply_filters( 'mwai_languages', $processed_languages );
1221 }
1222
1223 // Consolidate the Engines and their Models
1224 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
1225 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
1226 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
1227 foreach ( $options['ai_engines'] as &$engine ) {
1228 if ( $engine['type'] === 'openai' ) {
1229 $engine['models'] = apply_filters(
1230 'mwai_openai_models',
1231 Meow_MWAI_Engines_OpenAI::get_models_static()
1232 );
1233 }
1234 else if ( $engine['type'] === 'anthropic' ) {
1235 $engine['models'] = apply_filters(
1236 'mwai_anthropic_models',
1237 Meow_MWAI_Engines_Anthropic::get_models_static()
1238 );
1239 }
1240 else if ( $engine['type'] === 'perplexity' ) {
1241 $engine['models'] = apply_filters(
1242 'mwai_perplexity_models',
1243 Meow_MWAI_Engines_Perplexity::get_models_static()
1244 );
1245 }
1246 else if ( $engine['type'] === 'mistral' ) {
1247 $engine['models'] = apply_filters(
1248 'mwai_mistral_models',
1249 Meow_MWAI_Engines_Mistral::get_models_static()
1250 );
1251 }
1252 else {
1253 $engine['models'] = [];
1254 foreach ( $options['ai_models'] as $model ) {
1255 if ( $model['type'] === $engine['type'] ) {
1256 $engine['models'][] = $model;
1257 }
1258 }
1259 }
1260 }
1261
1262 // Functions via Code Engine (or custom code)
1263 $json = [];
1264 $functions = apply_filters( 'mwai_functions_list', [] );
1265 foreach ( $functions as $function ) {
1266 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1267 }
1268 $options['functions'] = $json;
1269
1270 // Addons
1271 $options['addons'] = apply_filters( 'mwai_addons', [
1272 [
1273 'slug' => 'mwai-notifications',
1274 'name' => 'Notifications',
1275 'description' => 'Get real-time alerts for new discussions in your chatbot, so you never miss a chance to engage.',
1276 'install_url' => 'https://meowapps.com/products/mwai-notifications/',
1277 'settings_url' => null,
1278 'stars' => 4,
1279 'enabled' => false
1280 ],
1281 [
1282 'slug' => 'mwai-ollama',
1283 'name' => 'Ollama',
1284 'description' => 'Leverage local LLM integration through Ollama; refresh and use your own models for a flexible, cost-free approach.',
1285 'install_url' => 'https://meowapps.com/products/mwai-ollama/',
1286 'settings_url' => null,
1287 'stars' => 3,
1288 'enabled' => false
1289 ],
1290 [
1291 'slug' => 'mwai-deepseek',
1292 'name' => 'DeepSeek',
1293 'description' => 'Support for DeepSeek, a Chinese AI company that provides extremely powerful LLM models.',
1294 'install_url' => 'https://meowapps.com/products/deepseek/',
1295 'settings_url' => null,
1296 'stars' => 3,
1297 'enabled' => false
1298 ],
1299 [
1300 'slug' => 'mwai-websearch',
1301 'name' => 'Web Search',
1302 'description' => 'Enhance chatbot responses by pulling context from Google and Tavily, delivering more accurate answers.',
1303 'install_url' => 'https://meowapps.com/products/mwai-websearch/',
1304 'settings_url' => null,
1305 'stars' => 5,
1306 'enabled' => false
1307 ],
1308 [
1309 'slug' => 'mwai-better-links',
1310 'name' => 'Better Links',
1311 'description' => 'Validate internal and external links and map specific terms to custom URLs, ensuring smoother navigation and references.',
1312 'install_url' => 'https://meowapps.com/products/mwai-better-links/',
1313 'settings_url' => null,
1314 'stars' => 3,
1315 'enabled' => false
1316 ],
1317 [
1318 'slug' => 'mwai-woo-basics',
1319 'name' => 'Woo Basics',
1320 'description' => 'Access essential WooCommerce data so your chatbot can understand products, orders, and more for a richer shopping experience.',
1321 'install_url' => 'https://meowapps.com/products/mwai-woo-basics/',
1322 'settings_url' => null,
1323 'stars' => 2,
1324 'enabled' => false
1325 ],
1326 [
1327 'slug' => 'mwai-quick-actions',
1328 'name' => 'Quick Actions',
1329 'description' => 'Enable dynamic quick actions at chat start or during events, helping users find what they need faster.',
1330 'install_url' => 'https://meowapps.com/products/mwai-quick-actions/',
1331 'settings_url' => null,
1332 'stars' => 3,
1333 'enabled' => false
1334 ],
1335 [
1336 'slug' => 'mwai-content-parser',
1337 'name' => 'Content Parser',
1338 'description' => 'Parse complex website content, including ACF fields and page builders, for more precise embeddings and knowledge retrieval.',
1339 'install_url' => 'https://meowapps.com/products/mwai-content-parser/',
1340 'settings_url' => null,
1341 'stars' => 2,
1342 'enabled' => false
1343 ],
1344 [
1345 'slug' => 'mwai-visitor-form',
1346 'name' => 'Visitor Form',
1347 'description' => 'Add a customizable form triggered by specific events in your chatbot to collect key visitor information seamlessly.',
1348 'install_url' => 'https://meowapps.com/products/mwai-visitor-form/',
1349 'settings_url' => null,
1350 'stars' => 2,
1351 'enabled' => false
1352 ],
1353 [
1354 'slug' => 'mwai-dynamic-keys',
1355 'name' => 'Dynamic Keys',
1356 'description' => 'Rotate multiple API keys dynamically for any environment, balancing usage and ensuring smooth performance.',
1357 'install_url' => 'https://meowapps.com/products/mwai-dynamic-keys/',
1358 'settings_url' => null,
1359 'stars' => 1,
1360 'enabled' => false
1361 ],
1362 ] );
1363
1364 // Populate usage data from ai_usage to ai_models_usage for the frontend
1365 $ai_usage = $this->get_option( 'ai_usage', [] );
1366 $options['ai_models_usage'] = $ai_usage;
1367
1368 // Also include daily usage data
1369 $ai_usage_daily = $this->get_option( 'ai_usage_daily', [] );
1370 $options['ai_models_usage_daily'] = $ai_usage_daily;
1371
1372 $populating = false;
1373 return $options;
1374 }
1375
1376 public function get_all_options( $force = false, $sanitize = false ) {
1377 if ( $force || is_null( $this->options ) ) {
1378 $options = get_option( $this->option_name, [] );
1379 $init_mode = empty( $options );
1380 foreach ( MWAI_OPTIONS as $key => $value ) {
1381 if ( !isset( $options[$key] ) ) {
1382 $options[$key] = $value;
1383 }
1384 }
1385 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1386 $options['default_limits'] = MWAI_LIMITS;
1387
1388 // Force sanitization if custom_languages is not set (migration)
1389 $needs_language_migration = !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] );
1390
1391 if ( $sanitize || $init_mode || $needs_language_migration ) {
1392 $options = $this->sanitize_options( $options );
1393 }
1394 $this->options = $options;
1395 }
1396 $options = $this->populate_dynamic_options( $this->options );
1397 return $options;
1398 }
1399
1400 // Sanitize options when we update the plugin or perform some updates
1401 // if we change the structure of the options.
1402 public function sanitize_options( $options ) {
1403 $needs_update = false;
1404
1405 // Removing old options of options renaming should be done here, as it was done before.
1406 // Check version 2.6.8 for an example.
1407
1408 // Avoid the logs_path to be a PHP file.
1409 if ( isset( $options['logs_path'] ) ) {
1410 $logs_path = $options['logs_path'];
1411 if ( substr( $logs_path, -4 ) !== '.log' ) {
1412 $options['logs_path'] = '';
1413 $needs_update = true;
1414 }
1415 }
1416
1417 // The IDs for the embeddings environments are generated here.
1418 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1419 $embeddings_default_exists = false;
1420 if ( isset( $options['embeddings_envs'] ) ) {
1421 foreach ( $options['embeddings_envs'] as &$env ) {
1422 if ( !isset( $env['id'] ) ) {
1423 $env['id'] = $this->get_random_id();
1424 $needs_update = true;
1425 }
1426 if ( $env['id'] === $options['embeddings_default_env'] ) {
1427 $embeddings_default_exists = true;
1428 }
1429 }
1430 }
1431 if ( !$embeddings_default_exists ) {
1432 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1433 $needs_update = true;
1434 }
1435
1436 // The IDs for the AI environments are generated here.
1437 $allEnvIds = [];
1438 $ai_default_exists = false;
1439 // Allow empty string as valid "None" selection
1440 $ai_default_is_none = isset( $options['ai_default_env'] ) && $options['ai_default_env'] === '';
1441 if ( isset( $options['ai_envs'] ) ) {
1442 foreach ( $options['ai_envs'] as &$env ) {
1443 if ( !isset( $env['id'] ) ) {
1444 $env['id'] = $this->get_random_id();
1445 $needs_update = true;
1446 }
1447 if ( $env['id'] === $options['ai_default_env'] ) {
1448 $ai_default_exists = true;
1449 }
1450 $allEnvIds[] = $env['id'];
1451 }
1452 }
1453 if ( !$ai_default_exists && !$ai_default_is_none ) {
1454 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1455 $needs_update = true;
1456 }
1457
1458 // The IDs for the MCP environments are generated here.
1459 if ( isset( $options['mcp_envs'] ) ) {
1460 foreach ( $options['mcp_envs'] as &$env ) {
1461 if ( !isset( $env['id'] ) ) {
1462 $env['id'] = $this->get_random_id();
1463 $needs_update = true;
1464 }
1465 }
1466 }
1467
1468 // All the models with an envId that does not exist anymore are removed.
1469 if ( isset( $options['ai_models'] ) ) {
1470 $options['ai_models'] = array_values( array_filter(
1471 $options['ai_models'],
1472 function ( $model ) use ( $allEnvIds, &$needs_update ) {
1473 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1474 $needs_update = true;
1475 return false;
1476 }
1477 return true;
1478 }
1479 ) );
1480 }
1481
1482 // Migration: Populate custom_languages if empty for existing installations
1483 if ( !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] ) ) {
1484 $options['custom_languages'] = [
1485 'English (en)',
1486 'German (de)',
1487 'French (fr)',
1488 'Spanish (es)',
1489 'Italian (it)',
1490 'Chinese (zh)',
1491 'Japanese (ja)',
1492 'Portuguese (pt)'
1493 ];
1494 $needs_update = true;
1495 }
1496
1497 if ( $needs_update ) {
1498 ksort( $options );
1499 update_option( $this->option_name, $options, false );
1500 }
1501
1502 return $options;
1503 }
1504
1505 public function update_options( $options ) {
1506 // update_option returns false both when update fails AND when value is unchanged
1507 // We just attempt the update and always return the current options
1508 // The frontend will see if the values actually changed
1509 update_option( $this->option_name, $options, false );
1510 $options = $this->get_all_options( true, true );
1511 return $options;
1512 }
1513
1514 public function update_option( $option, $value ) {
1515 $options = $this->get_all_options( true );
1516 $options[$option] = $value;
1517 return $this->update_options( $options );
1518 }
1519
1520 public function get_option( $option, $default = null ) {
1521 $options = $this->get_all_options();
1522 return $options[$option] ?? $default;
1523 }
1524
1525 public function update_ai_env( $env_id, $option, $value ) {
1526 $options = $this->get_all_options( true );
1527 foreach ( $options['ai_envs'] as &$env ) {
1528 if ( $env['id'] === $env_id ) {
1529 $env[$option] = $value;
1530 break;
1531 }
1532 }
1533 return $this->update_options( $options );
1534 }
1535
1536 public function get_engine_models( $engineType ) {
1537 // This method is called by engines with just a string type
1538 // We need to get the models differently
1539 $options = $this->get_all_options();
1540 $engines = $options['ai_envs'];
1541 $models = [];
1542
1543 // Find all models for this engine type
1544 foreach ( $engines as $engine ) {
1545 if ( $engine['type'] === $engineType ) {
1546 if ( isset( $engine['models'] ) ) {
1547 foreach ( $engine['models'] as $model ) {
1548 $models[] = $model;
1549 }
1550 }
1551 }
1552 }
1553
1554 // Also check custom models
1555 if ( isset( $options['ai_models'] ) ) {
1556 foreach ( $options['ai_models'] as $model ) {
1557 if ( $model['type'] === $engineType ) {
1558 $models[] = $model;
1559 }
1560 }
1561 }
1562
1563 return $models;
1564 }
1565
1566 public function reset_options() {
1567 delete_option( $this->themes_option_name );
1568 delete_option( $this->chatbots_option_name );
1569 delete_option( $this->option_name );
1570 return $this->get_all_options( true );
1571 }
1572 #endregion
1573
1574 #region Cron Tracking
1575 public function track_cron_start( $hook ) {
1576 // Set running transient (expires in 5 minutes as a safety measure)
1577 set_transient( 'mwai_cron_running_' . $hook, true, 300 );
1578 }
1579
1580 public function track_cron_end( $hook, $status = 'success', $error_message = '' ) {
1581 // Remove running transient
1582 delete_transient( 'mwai_cron_running_' . $hook );
1583
1584 // Get existing data
1585 $cron_data = get_transient( 'mwai_cron_last_run' ) ?: [];
1586
1587 // Update this cron's data - use time() for consistency
1588 $cron_data[$hook] = [
1589 'time' => time(),
1590 'status' => $status,
1591 'error' => $error_message
1592 ];
1593
1594 // Store for 7 days
1595 set_transient( 'mwai_cron_last_run', $cron_data, 7 * DAY_IN_SECONDS );
1596 }
1597 #endregion
1598 }
1599