PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.1.8
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.1.8
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / core.php
ai-engine / classes Last commit date
data 1 year ago engines 8 months ago exceptions 1 year ago modules 7 months ago query 8 months ago rest 8 months ago services 8 months ago admin.php 7 months ago api.php 8 months ago core.php 7 months ago discussion.php 1 year ago event.php 1 year ago init.php 8 months ago logging.php 1 year ago reply.php 8 months ago rest.php 7 months ago
core.php
1543 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 = MeowCommon_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
101 // Chatbots & Discussions
102 if ( $this->get_option( 'module_chatbots' ) ) {
103 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
104 // Only instantiate discussions if the feature is enabled
105 if ( $this->get_option( 'chatbot_discussions' ) ) {
106 $this->discussions = new Meow_MWAI_Modules_Discussions();
107 }
108 }
109
110 // Search
111 if ( $this->get_option( 'module_search' ) ) {
112 $this->search = new Meow_MWAI_Modules_Search( $this );
113 }
114
115 // Forms Manager (standalone Forms UI + shortcode renderer)
116 if ( $this->get_option( 'module_forms' ) ) {
117 new Meow_MWAI_Modules_Forms_Manager( $this );
118 }
119
120 // Advanced Core
121 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
122 new MeowPro_MWAI_Core( $this );
123 }
124
125 // Simple API
126 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions ?? null );
127
128 // MCP
129 if ( $this->get_option( 'module_mcp' ) ) {
130 new Meow_MWAI_Labs_MCP( $this );
131
132 // Core - Core WordPress MCP tools
133 if ( $this->get_option( 'mcp_core' ) ) {
134 new Meow_MWAI_Labs_MCP_Core( $this );
135 }
136
137 // Dynamic REST - WordPress REST API MCP tools
138 if ( $this->get_option( 'mcp_dynamic_rest' ) ) {
139 require_once MWAI_PATH . '/labs/mcp-rest.php';
140 new Meow_MWAI_Labs_MCP_Rest();
141 }
142
143 // Themes - Pro theme management MCP tools
144 if ( $this->get_option( 'mcp_themes' ) && class_exists( 'MeowPro_MWAI_MCP_Theme' ) ) {
145 new MeowPro_MWAI_MCP_Theme( $this );
146 }
147
148 // Plugins - Pro plugin management MCP tools
149 if ( $this->get_option( 'mcp_plugins' ) && class_exists( 'MeowPro_MWAI_MCP_Plugin' ) ) {
150 new MeowPro_MWAI_MCP_Plugin( $this );
151 }
152 }
153 }
154
155 public function register_scripts() {
156 // Register Highlight.js
157 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
158 // Register CSS for the themes
159 $themes = $this->get_themes();
160 foreach ( $themes as $theme ) {
161 if ( $theme['type'] === 'internal' ) {
162 $themeId = $theme['themeId'];
163 $filename = $themeId . '.css';
164 $physical_file = trailingslashit( MWAI_PATH ) . 'themes/' . $filename;
165 $cache_buster = file_exists( $physical_file ) ? filemtime( $physical_file ) : MWAI_VERSION;
166 wp_register_style( 'mwai_chatbot_theme_' . $themeId, trailingslashit( MWAI_URL )
167 . 'themes/' . $filename, [], $cache_buster );
168 }
169 }
170 }
171
172 public function enqueue_theme( $themeId ) {
173 if ( empty( $themeId ) ) {
174 return;
175 }
176 wp_enqueue_style( "mwai_chatbot_theme_$themeId" );
177 }
178
179 public function enqueue_themes() {
180 $themes = $this->get_themes();
181 foreach ( $themes as $theme ) {
182 if ( $theme['type'] === 'internal' ) {
183 $this->enqueue_theme( $theme['themeId'] );
184 }
185 }
186 }
187
188 #endregion
189
190 #region Roles & Capabilities
191 public function can_start_session() {
192 return $this->sessionService->can_start_session();
193 }
194
195 public function can_access_settings() {
196 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
197 }
198
199 public function can_access_features() {
200 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
201 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
202 }
203
204 public function can_access_public_api( $feature, $extra ) {
205 $logged_in = is_user_logged_in();
206 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
207 }
208 #endregion
209
210 #region AI-Related Helpers
211 public function run_query( $query, $streamCallback = null, $markdown = false ) {
212
213 // Allow to modify the query before it is sent.
214 // Different query types have specific filters for type-safe modifications.
215 if ( $query instanceof Meow_MWAI_Query_Embed ) {
216 $query = apply_filters( 'mwai_ai_embeddings_query', $query );
217 } else if ( $query instanceof Meow_MWAI_Query_Feedback ) {
218 $query = apply_filters( 'mwai_ai_feedback_query', $query );
219 } else {
220 $query = apply_filters( 'mwai_ai_query', $query );
221 }
222
223 // Ensure the query is still valid after filtering
224 if ( !$query || !is_object( $query ) ) {
225 throw new Exception( __( 'Invalid query object after filtering. The mwai_ai_query filter must return a valid query object.', 'ai-engine' ) );
226 }
227
228 // Validate that embeddings queries have a non-empty message
229 if ( $query instanceof Meow_MWAI_Query_Embed && empty( $query->get_message() ) ) {
230 throw new Exception( __( 'Embeddings query cannot have an empty message. Please check that the conversation context is properly extracted.', 'ai-engine' ) );
231 }
232
233 // Let's check the default environment and model.
234 $this->validate_env_model( $query );
235
236 // Create the engine based on the query's environment
237 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
238
239 // Let's run the query.
240 $reply = $engine->run( $query, $streamCallback );
241
242 // Let's allow to modify the reply before it is sent.
243 if ( $markdown ) {
244 if ( $query instanceof Meow_MWAI_Query_Image || $query instanceof Meow_MWAI_Query_EditImage ) {
245 $reply->result = '';
246 foreach ( $reply->results as $result ) {
247 $reply->result .= "![Image]($result)\n";
248 }
249 }
250 }
251
252 return $reply;
253 }
254
255 public function validate_env_model( $query ) {
256 return $this->modelEnvironmentService->validate_env_model( $query );
257 }
258
259 #endregion
260
261 #region Text-Related Helpers
262
263 // Clean the text perfectly, resolve shortcodes, etc, etc.
264 public function clean_text( $rawText = '' ) {
265 $text = html_entity_decode( $rawText );
266 $text = wp_strip_all_tags( $text );
267 $text = preg_replace( '/[\r\n]+/', "\n", $text );
268 $text = preg_replace( '/\n+/', "\n", $text );
269 $text = preg_replace( '/\t+/', "\t", $text );
270 return $text . ' ';
271 }
272
273 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
274 public function clean_sentences( $text, $maxLength = null ) {
275 // Step 1: Identify URLs and replace them with a placeholder.
276 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
277 preg_match_all( $urlPattern, $text, $urls );
278 $urlPlaceholders = [];
279 foreach ( $urls[0] as $index => $url ) {
280 $placeholder = '{urlPlaceholder' . $index . '}';
281 $text = str_replace( $url, $placeholder, $text );
282 $urlPlaceholders[$placeholder] = $url;
283 }
284
285 $maxLength = (int) ( $maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ) );
286 $sentences = preg_split( '/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY );
287 $hashes = [];
288 $uniqueSentences = [];
289 $total = 0;
290
291 foreach ( $sentences as $sentence ) {
292 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
293 $hash = md5( $sentence );
294 if ( !in_array( $hash, $hashes ) ) {
295 $length = mb_strlen( $sentence, 'UTF-8' );
296 if ( $total + $length > $maxLength ) {
297 continue;
298 }
299 $hashes[] = $hash;
300 $uniqueSentences[] = $sentence;
301 $total += $length;
302 }
303 }
304
305 $freshText = implode( ' ', $uniqueSentences );
306
307 // Step 3: Restore URLs in the final text.
308 foreach ( $urlPlaceholders as $placeholder => $url ) {
309 $freshText = str_replace( $placeholder, $url, $freshText );
310 }
311
312 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
313 return $freshText;
314 }
315
316 public function get_post_content( $postId ) {
317 // Ensure we get fresh post data by clearing cache
318 clean_post_cache( $postId );
319 $post = get_post( $postId );
320 if ( !$post ) {
321 return false;
322 }
323 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
324 $pattern = '/\[mwai_.*?\]/';
325 $text = preg_replace( $pattern, '', $text );
326 if ( $this->get_option( 'resolve_shortcodes' ) ) {
327 $text = apply_filters( 'the_content', $text );
328 }
329 else {
330 $pattern = "/\[[^\]]+\]/";
331 $text = preg_replace( $pattern, '', $text );
332 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
333 $text = preg_replace( $pattern, '', $text );
334 }
335 $text = $this->clean_text( $text );
336 $text = $this->clean_sentences( $text );
337 $text = apply_filters( 'mwai_post_content', $text, $postId );
338 return $text;
339 }
340
341 public function markdown_to_html( $content ) {
342 $Parsedown = new Parsedown();
343 $content = $Parsedown->text( $content );
344 return $content;
345 }
346
347 public function get_post_language( $postId ) {
348 $locale = get_locale();
349 $code = strtolower( substr( $locale, 0, 2 ) );
350 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
351 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
352 if ( !empty( $lang ) ) {
353 $locale = $lang['locale'];
354 $humanLanguage = $lang['display_name'];
355 }
356 return strtolower( "$locale ($humanLanguage)" );
357 }
358
359 public function do_placeholders( $text ) {
360 $defaultPlaceholders = [];
361 $dataPlaceholders = $this->get_user_data();
362 if ( !empty( $dataPlaceholders ) ) {
363 $defaultPlaceholders = array_merge( $defaultPlaceholders, $dataPlaceholders );
364 }
365 $placeholders = apply_filters( 'mwai_placeholders', $defaultPlaceholders );
366 foreach ( $placeholders as $key => $value ) {
367 $text = str_replace( '{' . $key . '}', $value, $text );
368 }
369 return $text;
370 }
371 #endregion
372
373 #region Image-Related Helpers
374 public static function is_image( $file ) {
375 global $mwai_core;
376 if ( $mwai_core && $mwai_core->imageService ) {
377 return $mwai_core->imageService->is_image( $file );
378 }
379 // Fallback to original implementation if service not available
380 $mimeType = self::get_mime_type( $file );
381 if ( strpos( $mimeType, 'image' ) !== false ) {
382 return true;
383 }
384 return false;
385 }
386
387 public static function get_image_resolution( $url ) {
388 global $mwai_core;
389 if ( $mwai_core && $mwai_core->imageService ) {
390 return $mwai_core->imageService->get_image_resolution( $url );
391 }
392 // Fallback to original implementation if service not available
393 if ( empty( $url ) ) {
394 return null;
395 }
396 $headers = get_headers( $url, 1 );
397 if ( strpos( $headers[0], '200' ) === false ) {
398 return null;
399 }
400 $image_info = getimagesize( $url );
401 if ( $image_info === false ) {
402 return null;
403 }
404 return [
405 'width' => $image_info[0],
406 'height' => $image_info[1]
407 ];
408 }
409
410 public static function get_mime_type( $file ) {
411 global $mwai_core;
412 if ( $mwai_core && $mwai_core->imageService ) {
413 return $mwai_core->imageService->get_mime_type( $file );
414 }
415
416 // Fallback implementation - this should rarely be used as imageService is initialized early
417 Meow_MWAI_Logging::warn( 'get_mime_type called before imageService is available' );
418
419 // Basic extension-based detection only
420 $extension = pathinfo( $file, PATHINFO_EXTENSION );
421 $extension = strtolower( $extension );
422 $mimeTypes = [
423 'jpg' => 'image/jpeg',
424 'jpeg' => 'image/jpeg',
425 'png' => 'image/png',
426 'gif' => 'image/gif',
427 'webp' => 'image/webp',
428 'bmp' => 'image/bmp',
429 'tiff' => 'image/tiff',
430 'tif' => 'image/tiff',
431 'svg' => 'image/svg+xml',
432 'ico' => 'image/x-icon',
433 'pdf' => 'application/pdf',
434 ];
435 return isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
436 }
437
438 public function download_image( $url ) {
439 return $this->imageService->download_image( $url );
440 }
441
442 /**
443 * Add an image from a URL to the Media Library.
444 * @param string $url The URL of the image to be downloaded.
445 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
446 * @param string $title The title of the image.
447 * @param string $description The description of the image.
448 * @param string $caption The caption of the image.
449 * @param string $alt The alt text of the image.
450 * @return int The attachment ID of the image.
451 */
452 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 = [] ) {
453 return $this->imageService->add_image_from_url( $url, $filename, $title, $description, $caption, $alt, $attachedPost, $post_status, $post_type, $ai_metadata );
454 }
455 #endregion
456
457 #region Context-Related Helpers
458 public function retrieve_context( $params, $query, $streamCallback = null ) {
459 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
460 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
461
462 $context = apply_filters( 'mwai_context_search', [], $query, [
463 'embeddingsEnvId' => $embeddingsEnvId,
464 'streamCallback' => $streamCallback
465 ] );
466
467 // Emit embeddings event if streaming and context was found
468 if ( $streamCallback && !empty( $context ) ) {
469 $count = 0;
470 if ( isset( $context['embeddings'] ) && is_array( $context['embeddings'] ) ) {
471 $count = count( $context['embeddings'] );
472 }
473 else if ( isset( $context['content'] ) ) {
474 $count = 1;
475 }
476 if ( $count > 0 ) {
477 $event = Meow_MWAI_Event::embeddings( $count );
478 $streamCallback( $event );
479 }
480 }
481
482 if ( empty( $context ) ) {
483 return null;
484 }
485 else if ( !isset( $context['content'] ) ) {
486 Meow_MWAI_Logging::warn( 'A context without content was returned.' );
487 return null;
488 }
489 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
490 $context['length'] = strlen( $context['content'] );
491 return $context;
492 }
493 #endregion
494
495 #region Users/Sessions Helpers
496
497 public function get_nonce( $force = false ) {
498 return $this->sessionService->get_nonce( $force );
499 }
500
501 // This is a bit hacky, but chatId needs to be retrieved or generated.
502 // Maybe we can clean this up later.
503 public function fix_chat_id( $query, $params ) {
504 return $this->sessionService->fix_chat_id( $query, $params );
505 }
506
507 public function get_session_id() {
508 return $this->sessionService->get_session_id();
509 }
510
511 /**
512 * Get the Response ID Manager service
513 */
514 public function get_response_id_manager() {
515 return $this->responseIdManager;
516 }
517
518 /**
519 * Get the Message Builder service
520 */
521 public function get_message_builder() {
522 return $this->messageBuilder;
523 }
524
525 // Get the UserID from the data, or from the current user
526 public function get_user_id( $data = null ) {
527 return $this->sessionService->get_user_id( $data );
528 }
529
530 public function get_session_user_id() {
531 return $this->sessionService->get_session_user_id();
532 }
533
534 public function get_admin_user() {
535 return $this->sessionService->get_admin_user();
536 }
537
538 public function get_user_data() {
539 return $this->sessionService->get_user_data();
540 }
541
542 public function get_ip_address( $force = false ) {
543 return $this->sessionService->get_ip_address( $force );
544 }
545
546 #endregion
547
548 #region Sanitization
549 public function sanitize_sort(
550 &$sort,
551 $default_accessor = 'created',
552 $default_order = 'DESC',
553 $allowed_columns = [ 'created', 'updated', 'name', 'id', 'time', 'units', 'price' ]
554 ) {
555
556 // Ensure $sort is an array
557 if ( !is_array( $sort ) ) {
558 $sort = [ 'accessor' => $default_accessor, 'by' => $default_order ];
559 }
560 // Extract and sanitize the accessor
561 $sort_accessor = isset( $sort['accessor'] ) ? $sort['accessor'] : $default_accessor;
562 if ( !in_array( $sort_accessor, $allowed_columns ) ) {
563 Meow_MWAI_Logging::error( "This sort accessor is not allowed ($sort_accessor)." );
564 $sort_accessor = $default_accessor;
565 }
566 // Extract and sanitize the sort order
567 $sort_by = isset( $sort['by'] ) ? strtoupper( $sort['by'] ) : $default_order;
568 if ( $sort_by !== 'ASC' && $sort_by !== 'DESC' ) {
569 Meow_MWAI_Logging::error( "This sort order is not allowed ($sort_by)." );
570 $sort_by = $default_order;
571 }
572 // Update the sort array with sanitized values
573 $sort['accessor'] = $sort_accessor;
574 $sort['by'] = $sort_by;
575 }
576 #endregion
577
578 #region Other Helpers
579 public function safe_strlen( $string, $encoding = 'UTF-8' ) {
580 if ( function_exists( 'mb_strlen' ) ) {
581 return mb_strlen( $string, $encoding );
582 }
583 else {
584 // Fallback implementation for environments without mbstring extension
585 return preg_match_all( '/./u', $string, $matches );
586 }
587 }
588
589 public function check_rest_nonce( $request ) {
590 // REST NONCE VERIFICATION:
591 // Validates nonce from X-WP-Nonce header using WordPress nonce system.
592 // Returns: false (invalid), 1 (0-12 hours old), or 2 (12-24 hours old)
593 // WordPress REST permission callbacks accept any truthy value as success.
594 // The filter allows custom authorization logic if needed.
595 $nonce = $request->get_header( 'X-WP-Nonce' );
596 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
597 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
598 }
599
600 public function get_random_id( $length = 8, $excludeIds = [] ) {
601 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
602 $charactersLength = strlen( $characters );
603 $randomId = '';
604 for ( $i = 0; $i < $length; $i++ ) {
605 $randomId .= $characters[ mt_rand( 0, $charactersLength - 1 ) ];
606 }
607 if ( in_array( $randomId, $excludeIds ) ) {
608 return $this->get_random_id( $length, $excludeIds );
609 }
610 return $randomId;
611 }
612
613 public function is_url( $url ) {
614 return strpos( $url, 'http' ) === 0 ? true : false;
615 }
616
617 public function get_post_types() {
618 $excluded = [ 'attachment', 'revision', 'nav_menu_item' ];
619 $post_types = [];
620 $types = get_post_types( [], 'objects' );
621
622 // Let's get the Post Types that are enabled for Embeddings Sync
623 $embeddingsSettings = $this->get_option( 'embeddings' );
624 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
625
626 foreach ( $types as $type ) {
627 $forced = in_array( $type->name, $syncPostTypes );
628 // Should not be excluded.
629 if ( !$forced && in_array( $type->name, $excluded ) ) {
630 continue;
631 }
632 // Should be public.
633 if ( !$forced && !$type->public ) {
634 continue;
635 }
636 $post_types[] = [
637 'name' => $type->labels->name,
638 'type' => $type->name,
639 ];
640 }
641
642 // Let's get the Post Types that are enabled for Embeddings Sync
643 $embeddingsSettings = $this->get_option( 'embeddings' );
644 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
645
646 return $post_types;
647 }
648
649 public function get_post( $post ) {
650 if ( is_numeric( $post ) ) {
651 // Force fresh retrieval to avoid cache issues
652 clean_post_cache( $post );
653 $post = get_post( $post );
654 }
655 if ( is_object( $post ) ) {
656 $post = (array) $post;
657 }
658 if ( !is_array( $post ) ) {
659 return null;
660 }
661 $language = $this->get_post_language( $post['ID'] );
662 $content = $this->get_post_content( $post['ID'] );
663 $title = $post['post_title'];
664 $excerpt = $post['post_excerpt'];
665 $url = get_permalink( $post['ID'] );
666 $checksum = wp_hash( $content . $title . $url );
667
668
669 return [
670 'postId' => (int) $post['ID'],
671 'title' => $title,
672 'content' => $content,
673 'excerpt' => $excerpt,
674 'url' => $url,
675 'language' => $language ?? 'english',
676 'checksum' => $checksum,
677 ];
678 }
679
680 /**
681 * Format a date/time string into a human-readable format
682 * @param string $date_string The date string to format
683 * @return string Formatted date (e.g., "Just now", "5m ago", "2h ago", "3d ago", "Jan 20th")
684 */
685 public function format_discussion_date( $date_string ) {
686 $date = strtotime( $date_string );
687 $now = time();
688 $diff = $now - $date;
689
690 // Less than a minute
691 if ( $diff < 60 ) {
692 return 'Just now';
693 }
694
695 // Less than an hour
696 if ( $diff < 3600 ) {
697 $minutes = floor( $diff / 60 );
698 return $minutes . 'm ago';
699 }
700
701 // Less than a day
702 if ( $diff < 86400 ) {
703 $hours = floor( $diff / 3600 );
704 return $hours . 'h ago';
705 }
706
707 // Less than a week
708 if ( $diff < 604800 ) {
709 $days = floor( $diff / 86400 );
710 return $days . 'd ago';
711 }
712
713 // Format as date
714 $is_current_year = date( 'Y', $date ) === date( 'Y', $now );
715 if ( $is_current_year ) {
716 return date( 'M jS', $date );
717 } else {
718 return date( 'M jS, Y', $date );
719 }
720 }
721 #endregion
722
723 #region Usage & Costs
724
725 // Quick and dirty token estimation
726 // Let's keep this synchronized with Helpers in JS
727 public static function estimate_tokens( ...$args ): int {
728 global $mwai_core;
729 if ( $mwai_core && $mwai_core->usageStatsService ) {
730 return $mwai_core->usageStatsService->estimate_tokens( ...$args );
731 }
732 // Fallback to original implementation if service not available
733 $text = '';
734 foreach ( $args as $arg ) {
735 if ( is_array( $arg ) ) {
736 foreach ( $arg as $message ) {
737 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : '';
738 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : '';
739 }
740 }
741 else if ( is_string( $arg ) ) {
742 $text .= $arg;
743 }
744 }
745 $averageTokenLength = 4;
746 $words = preg_split( '/\s+/', trim( $text ) );
747 $tokenCount = 0;
748 foreach ( $words as $word ) {
749 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
750 }
751 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
752 }
753
754 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
755 return $this->usageStatsService->record_tokens_usage( $model, $in_tokens, $out_tokens, $returned_price );
756 }
757
758 public function record_audio_usage( $model, $seconds ) {
759 return $this->usageStatsService->record_audio_usage( $model, $seconds );
760 }
761
762 public function record_images_usage( $model, $resolution, $images ) {
763 return $this->usageStatsService->record_images_usage( $model, $resolution, $images );
764 }
765
766 public function record_videos_usage( $model, $resolution, $seconds ) {
767 return $this->usageStatsService->record_videos_usage( $model, $resolution, $seconds );
768 }
769
770 #endregion
771
772 #region Streaming
773 public function stream_push( $data, $query = null ) {
774 try {
775 // Handle new Event objects
776 if ( is_object( $data ) && method_exists( $data, 'to_array' ) ) {
777 $data = $data->to_array();
778 }
779
780 $data = apply_filters( 'mwai_stream_push', $data, $query );
781 $out = 'data: ' . json_encode( $data );
782 echo $out;
783 echo "\n\n";
784 if ( ob_get_level() > 0 ) {
785 ob_end_flush();
786 }
787 flush();
788 }
789 catch ( Exception $e ) {
790 // Send error as proper SSE error event
791 $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.';
792 error_log( '[AI Engine Stream Error] ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
793
794 $errorData = [
795 'type' => 'error',
796 'data' => $errorMessage
797 ];
798 $out = 'data: ' . json_encode( $errorData );
799 echo $out;
800 echo "\n\n";
801 if ( ob_get_level() > 0 ) {
802 ob_end_flush();
803 }
804 flush();
805
806 // Stop execution after sending error
807 die();
808 }
809 }
810 #endregion
811
812 #region Options
813 public function get_themes() {
814 $themes = get_option( $this->themes_option_name, [] );
815 $themes = empty( $themes ) ? [] : $themes;
816
817 $internalThemes = [
818 'chatgpt' => [
819 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
820 'settings' => [], 'style' => ''
821 ],
822 'messages' => [
823 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
824 'settings' => [], 'style' => ''
825 ],
826 'timeless' => [
827 'type' => 'internal', 'name' => 'Timeless', 'themeId' => 'timeless',
828 'settings' => [], 'style' => ''
829 ],
830 ];
831 $customThemes = [];
832 foreach ( $themes as $theme ) {
833 if ( isset( $internalThemes[$theme['themeId']] ) ) {
834 $internalThemes[$theme['themeId']] = $theme;
835 continue;
836 }
837 $customThemes[] = $theme;
838 }
839 return array_merge( array_values( $internalThemes ), $customThemes );
840 }
841
842 public function update_themes( $themes ) {
843 update_option( $this->themes_option_name, $themes );
844 return $themes;
845 }
846
847 public function get_chatbots() {
848 $chatbots = get_option( $this->chatbots_option_name, [] );
849 $hasChanges = false;
850 if ( empty( $chatbots ) ) {
851 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
852 }
853 $hasDefault = false;
854 foreach ( $chatbots as &$chatbot ) {
855 if ( $chatbot['botId'] === 'default' ) {
856 $hasDefault = true;
857 }
858 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
859 // Use default value if not set.
860 if ( !isset( $chatbot[$key] ) ) {
861 $chatbot[$key] = $value;
862 }
863 }
864
865 /*
866 This is the best section to rename fields.
867 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).
868 */
869
870 // Migrate old file upload params to new unified system
871 if ( !isset( $chatbot['fileUpload'] ) ) {
872 // Set fileUpload based on old params (imageUpload, fileUploads, or vision checkbox)
873 $chatbot['fileUpload'] = !empty( $chatbot['imageUpload'] ) || ( isset( $chatbot['fileUploads'] ) && $chatbot['fileUploads'] > 0 );
874 $hasChanges = true;
875 }
876
877 // Ensure maxUploads is set
878 if ( !isset( $chatbot['maxUploads'] ) ) {
879 if ( isset( $chatbot['fileUploads'] ) && $chatbot['fileUploads'] > 0 ) {
880 $chatbot['maxUploads'] = $chatbot['fileUploads'];
881 } else {
882 $chatbot['maxUploads'] = 1; // Default to 1 file
883 }
884 $hasChanges = true;
885 }
886
887 // Sync fileUploads with fileUpload and maxUploads for consistency
888 if ( isset( $chatbot['fileUpload'] ) ) {
889 $chatbot['fileUploads'] = $chatbot['fileUpload'] ? $chatbot['maxUploads'] : 0;
890 $chatbot['multiUpload'] = $chatbot['fileUpload'] && $chatbot['maxUploads'] > 1;
891 $chatbot['imageUpload'] = $chatbot['fileUpload']; // Keep imageUpload in sync
892 }
893
894 // if ( isset( $chatbot['context'] ) ) {
895 // $chatbot['instructions'] = $chatbot['context'];
896 // unset( $chatbot['context'] );
897 // $hasChanges = true;
898 // }
899 }
900 if ( !$hasDefault ) {
901 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
902 array_unshift( $chatbots, $defaultBot );
903 $hasChanges = true;
904 }
905 if ( $hasChanges ) {
906 update_option( $this->chatbots_option_name, $chatbots );
907 }
908 return $chatbots;
909 }
910
911 public function get_chatbot( $botId ) {
912 $chatbots = $this->get_chatbots();
913 foreach ( $chatbots as $chatbot ) {
914 if ( $chatbot['botId'] === (string) $botId ) {
915 return $chatbot;
916 }
917 }
918 return null;
919 }
920
921 public function get_embeddings_env( $envId ) {
922 return $this->modelEnvironmentService->get_embeddings_env( $envId );
923 }
924
925 public function get_ai_env( $envId ) {
926 return $this->modelEnvironmentService->get_ai_env( $envId );
927 }
928
929 public function get_assistant( $envId, $assistantId ) {
930 return $this->modelEnvironmentService->get_assistant( $envId, $assistantId );
931 }
932
933 public function get_theme( $themeId ) {
934 $themes = $this->get_themes();
935 foreach ( $themes as $theme ) {
936 if ( $theme['themeId'] === $themeId ) {
937 // Append custom CSS to theme data for frontend rendering (check for non-empty trimmed string)
938 if ( isset( $theme['settings']['customCSS'] ) && trim( $theme['settings']['customCSS'] ) !== '' ) {
939 $customCSS = $theme['settings']['customCSS'];
940
941 // Add theme class prefix to all CSS rules for proper scoping
942 $themeClass = '.mwai-' . $themeId . '-theme';
943 $lines = explode( "\n", $customCSS );
944 $processedCSS = '';
945 $inRule = false;
946
947 foreach ( $lines as $line ) {
948 $trimmedLine = trim( $line );
949
950 // Skip empty lines and comments
951 if ( empty( $trimmedLine ) || strpos( $trimmedLine, '/*' ) === 0 ) {
952 $processedCSS .= $line . "\n";
953 continue;
954 }
955
956 // If line contains a selector (has { but not })
957 if ( strpos( $line, '{' ) !== false && strpos( $line, '}' ) === false ) {
958 // Extract selector and the rest
959 $parts = explode( '{', $line, 2 );
960 $selector = trim( $parts[0] );
961
962 // Add theme class prefix if not already present
963 if ( strpos( $selector, $themeClass ) !== 0 ) {
964 // Handle multiple selectors separated by comma
965 $selectors = explode( ',', $selector );
966 $prefixedSelectors = array_map( function( $sel ) use ( $themeClass ) {
967 $sel = trim( $sel );
968 // Don't prefix if it's a keyframe or similar
969 if ( strpos( $sel, '@' ) === 0 || strpos( $sel, 'from' ) === 0 || strpos( $sel, 'to' ) === 0 || preg_match( '/^\d+%/', $sel ) ) {
970 return $sel;
971 }
972 return $themeClass . ' ' . $sel;
973 }, $selectors );
974 $selector = implode( ', ', $prefixedSelectors );
975 }
976
977 $processedCSS .= $selector . ' {' . ( isset( $parts[1] ) ? $parts[1] : '' ) . "\n";
978 $inRule = true;
979 } else {
980 $processedCSS .= $line . "\n";
981 }
982 }
983
984 $customCSS = $processedCSS;
985
986 // For custom themes (type: 'css'), append to style property
987 if ( $theme['type'] === 'css' ) {
988 $theme['style'] = ( $theme['style'] ?? '' ) . "\n\n/* Custom CSS */\n" . $customCSS;
989 }
990 // For internal themes, add customCSS as a separate property
991 else {
992 $theme['customCSS'] = $customCSS;
993 }
994 }
995
996 // Add CSS URL for cross-site and dynamic loading support
997 // Internal themes can use physical file, custom themes use REST endpoint
998 $theme_type = $theme['type'] ?? 'internal';
999 if ( $theme_type === 'internal' ) {
1000 $theme['cssUrl'] = MWAI_URL . 'themes/' . $themeId . '.css';
1001 }
1002 else {
1003 // Custom themes use REST endpoint (requires Cross-Site module to be enabled)
1004 $theme['cssUrl'] = get_rest_url( null, 'mwai-ui/v1/cross-site/theme-css' ) . '?themeId=' . $themeId;
1005 }
1006
1007 return $theme;
1008 }
1009 }
1010 return null;
1011 }
1012
1013 public function update_chatbots( $chatbots ) {
1014 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
1015 // TODO: I think some HTML fields are missing, guestName, maybe others.
1016 $htmlFields = [ 'instructions', 'textCompliance', 'aiName', 'userName', 'startSentence' ];
1017 $keepLineReturnsFields = [ 'instructions' ];
1018 $whiteSpacedFields = [ 'context' ];
1019 // Boolean fields that need proper conversion
1020 $booleanFields = [ 'window', 'copyButton', 'fullscreen', 'localMemory', 'iconBubble', 'centerOpen',
1021 'imageUpload', 'fileUpload', 'multiUpload', 'fileSearch', 'contentAware', 'aiAvatar', 'userAvatar', 'guestAvatar' ];
1022 foreach ( $chatbots as &$chatbot ) {
1023 foreach ( $chatbot as $key => &$value ) {
1024 if ( in_array( $key, $deprecatedFields ) ) {
1025 unset( $chatbot[$key] );
1026 continue;
1027 }
1028 if ( in_array( $key, $htmlFields ) ) {
1029 $value = wp_kses_post( $value );
1030 }
1031 else if ( in_array( $key, $whiteSpacedFields ) ) {
1032 $value = sanitize_textarea_field( $value );
1033 }
1034 else if ( in_array( $key, $booleanFields ) ) {
1035 // Convert various representations to boolean
1036 if ( is_bool( $value ) ) {
1037 // Already boolean, keep as is
1038 } else if ( $value === 1 || $value === '1' || $value === true || $value === 'true' || $value === 'yes' ) {
1039 // These are true values
1040 $value = true;
1041 } else if ( $value === 0 || $value === '0' || $value === false || $value === 'false' || $value === 'no' || $value === '' || $value === null ) {
1042 // These are false values
1043 $value = false;
1044 } else {
1045 // Default to checking if not empty
1046 $value = !empty( $value );
1047 }
1048 }
1049 else if ( $key === 'functions' ) {
1050 $functions = [];
1051 foreach ( $value as $function ) {
1052 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
1053 $functions[] = [
1054 'id' => sanitize_text_field( $function['id'] ),
1055 'type' => sanitize_text_field( $function['type'] ),
1056 ];
1057 }
1058 }
1059 $value = $functions;
1060 }
1061 else if ( $key === 'mcpServers' ) {
1062 $mcpServers = [];
1063 foreach ( $value as $server ) {
1064 if ( isset( $server['id'] ) ) {
1065 $mcpServers[] = [
1066 'id' => sanitize_text_field( $server['id'] ),
1067 ];
1068 }
1069 }
1070 $value = $mcpServers;
1071 }
1072 else if ( $key === 'tools' ) {
1073 // Sanitize tools array (web_search, image_generation, thinking, etc)
1074 $tools = [];
1075 if ( is_array( $value ) ) {
1076 foreach ( $value as $tool ) {
1077 $sanitized_tool = sanitize_text_field( $tool );
1078 if ( in_array( $sanitized_tool, ['web_search', 'image_generation', 'thinking', 'code_interpreter'] ) ) {
1079 $tools[] = $sanitized_tool;
1080 }
1081 }
1082 }
1083 $value = $tools;
1084 }
1085 else if ( $key === 'crossSite' ) {
1086 // Handle crossSite object
1087 $crossSite = [
1088 'enabled' => isset( $value['enabled'] ) ? (bool) $value['enabled'] : false,
1089 'allowedDomains' => []
1090 ];
1091 if ( isset( $value['allowedDomains'] ) && is_array( $value['allowedDomains'] ) ) {
1092 foreach ( $value['allowedDomains'] as $domain ) {
1093 $sanitized_domain = sanitize_text_field( $domain );
1094 if ( !empty( $sanitized_domain ) ) {
1095 $crossSite['allowedDomains'][] = $sanitized_domain;
1096 }
1097 }
1098 }
1099 $value = $crossSite;
1100 }
1101 else {
1102 if ( in_array( $key, $keepLineReturnsFields ) ) {
1103 $value = preg_replace( '/\r\n/', '[==LINE_RETURN==]', $value );
1104 $value = preg_replace( '/\n/', '[==LINE_RETURN==]', $value );
1105 }
1106 $value = sanitize_text_field( $value );
1107 if ( in_array( $key, $keepLineReturnsFields ) ) {
1108 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
1109 }
1110 }
1111 }
1112
1113 // Sync upload params to ensure consistency
1114 // fileUpload is the master control - respect its value
1115 $fileUploadEnabled = !empty( $chatbot['fileUpload'] ) || !empty( $chatbot['imageUpload'] );
1116 $maxFiles = isset( $chatbot['maxUploads'] ) && $chatbot['maxUploads'] > 0 ? (int) $chatbot['maxUploads'] : 1;
1117
1118 $chatbot['imageUpload'] = $fileUploadEnabled;
1119 $chatbot['fileUploads'] = $fileUploadEnabled ? $maxFiles : 0;
1120 $chatbot['multiUpload'] = $fileUploadEnabled && $maxFiles > 1;
1121 }
1122 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
1123 Meow_MWAI_Logging::warn( 'Could not update chatbots.' );
1124 $chatbots = get_option( $this->chatbots_option_name, [] );
1125 return $chatbots;
1126 }
1127 return $chatbots;
1128 }
1129
1130 public function populate_dynamic_options( $options ) {
1131 static $populating = false;
1132
1133 // Prevent infinite recursion
1134 if ( $populating ) {
1135 return $options;
1136 }
1137
1138 $populating = true;
1139
1140 // Languages - use custom languages as the complete list
1141 $custom_languages = isset( $options['custom_languages'] ) && !empty( $options['custom_languages'] )
1142 ? $options['custom_languages']
1143 : [];
1144
1145 // If no custom languages defined, fall back to defaults
1146 if ( empty( $custom_languages ) ) {
1147 $options['languages'] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
1148 } else {
1149 // Process custom languages
1150 $processed_languages = [];
1151 foreach ( $custom_languages as $custom_lang ) {
1152 // Support formats like "Russian (ru)" or just "Russian"
1153 $custom_lang = trim( $custom_lang );
1154 if ( !empty( $custom_lang ) ) {
1155 // Check if language code is provided in parentheses
1156 if ( preg_match( '/^(.+)\s*\(([a-z]{2,3})\)$/i', $custom_lang, $matches ) ) {
1157 $lang_name = trim( $matches[1] );
1158 $lang_code = strtolower( trim( $matches[2] ) );
1159 $processed_languages[$lang_code] = $lang_name;
1160 } else {
1161 // No code provided, add as-is
1162 $processed_languages[] = $custom_lang;
1163 }
1164 }
1165 }
1166
1167 $options['languages'] = apply_filters( 'mwai_languages', $processed_languages );
1168 }
1169
1170 // Consolidate the Engines and their Models
1171 // PS: We should ABSOLUTELY AVOID to use ai_models directly (except for saving)
1172 // Engine Example: [ 'name' => 'Ollama', 'type' => 'ollama', inputs => ['apikey', 'endpoint'], models => [] ]
1173 $options['ai_engines'] = apply_filters( 'mwai_engines', MWAI_ENGINES );
1174 foreach ( $options['ai_engines'] as &$engine ) {
1175 if ( $engine['type'] === 'openai' ) {
1176 $engine['models'] = apply_filters(
1177 'mwai_openai_models',
1178 Meow_MWAI_Engines_OpenAI::get_models_static()
1179 );
1180 }
1181 else if ( $engine['type'] === 'anthropic' ) {
1182 $engine['models'] = apply_filters(
1183 'mwai_anthropic_models',
1184 Meow_MWAI_Engines_Anthropic::get_models_static()
1185 );
1186 }
1187 else if ( $engine['type'] === 'perplexity' ) {
1188 $engine['models'] = apply_filters(
1189 'mwai_perplexity_models',
1190 Meow_MWAI_Engines_Perplexity::get_models_static()
1191 );
1192 }
1193 else if ( $engine['type'] === 'mistral' ) {
1194 $engine['models'] = apply_filters(
1195 'mwai_mistral_models',
1196 Meow_MWAI_Engines_Mistral::get_models_static()
1197 );
1198 }
1199 else {
1200 $engine['models'] = [];
1201 foreach ( $options['ai_models'] as $model ) {
1202 if ( $model['type'] === $engine['type'] ) {
1203 $engine['models'][] = $model;
1204 }
1205 }
1206 }
1207 }
1208
1209 // Functions via Code Engine (or custom code)
1210 $json = [];
1211 $functions = apply_filters( 'mwai_functions_list', [] );
1212 foreach ( $functions as $function ) {
1213 $json[] = Meow_MWAI_Query_Function::toJson( $function );
1214 }
1215 $options['functions'] = $json;
1216
1217 // Addons
1218 $options['addons'] = apply_filters( 'mwai_addons', [
1219 [
1220 'slug' => 'mwai-notifications',
1221 'name' => 'Notifications',
1222 'description' => 'Get real-time alerts for new discussions in your chatbot, so you never miss a chance to engage.',
1223 'install_url' => 'https://meowapps.com/products/mwai-notifications/',
1224 'settings_url' => null,
1225 'stars' => 4,
1226 'enabled' => false
1227 ],
1228 [
1229 'slug' => 'mwai-ollama',
1230 'name' => 'Ollama',
1231 'description' => 'Leverage local LLM integration through Ollama; refresh and use your own models for a flexible, cost-free approach.',
1232 'install_url' => 'https://meowapps.com/products/mwai-ollama/',
1233 'settings_url' => null,
1234 'stars' => 3,
1235 'enabled' => false
1236 ],
1237 [
1238 'slug' => 'mwai-deepseek',
1239 'name' => 'DeepSeek',
1240 'description' => 'Support for DeepSeek, a Chinese AI company that provides extremely powerful LLM models.',
1241 'install_url' => 'https://meowapps.com/products/deepseek/',
1242 'settings_url' => null,
1243 'stars' => 3,
1244 'enabled' => false
1245 ],
1246 [
1247 'slug' => 'mwai-websearch',
1248 'name' => 'Web Search',
1249 'description' => 'Enhance chatbot responses by pulling context from Google and Tavily, delivering more accurate answers.',
1250 'install_url' => 'https://meowapps.com/products/mwai-websearch/',
1251 'settings_url' => null,
1252 'stars' => 5,
1253 'enabled' => false
1254 ],
1255 [
1256 'slug' => 'mwai-better-links',
1257 'name' => 'Better Links',
1258 'description' => 'Validate internal and external links and map specific terms to custom URLs, ensuring smoother navigation and references.',
1259 'install_url' => 'https://meowapps.com/products/mwai-better-links/',
1260 'settings_url' => null,
1261 'stars' => 3,
1262 'enabled' => false
1263 ],
1264 [
1265 'slug' => 'mwai-woo-basics',
1266 'name' => 'Woo Basics',
1267 'description' => 'Access essential WooCommerce data so your chatbot can understand products, orders, and more for a richer shopping experience.',
1268 'install_url' => 'https://meowapps.com/products/mwai-woo-basics/',
1269 'settings_url' => null,
1270 'stars' => 2,
1271 'enabled' => false
1272 ],
1273 [
1274 'slug' => 'mwai-quick-actions',
1275 'name' => 'Quick Actions',
1276 'description' => 'Enable dynamic quick actions at chat start or during events, helping users find what they need faster.',
1277 'install_url' => 'https://meowapps.com/products/mwai-quick-actions/',
1278 'settings_url' => null,
1279 'stars' => 3,
1280 'enabled' => false
1281 ],
1282 [
1283 'slug' => 'mwai-content-parser',
1284 'name' => 'Content Parser',
1285 'description' => 'Parse complex website content, including ACF fields and page builders, for more precise embeddings and knowledge retrieval.',
1286 'install_url' => 'https://meowapps.com/products/mwai-content-parser/',
1287 'settings_url' => null,
1288 'stars' => 2,
1289 'enabled' => false
1290 ],
1291 [
1292 'slug' => 'mwai-visitor-form',
1293 'name' => 'Visitor Form',
1294 'description' => 'Add a customizable form triggered by specific events in your chatbot to collect key visitor information seamlessly.',
1295 'install_url' => 'https://meowapps.com/products/mwai-visitor-form/',
1296 'settings_url' => null,
1297 'stars' => 2,
1298 'enabled' => false
1299 ],
1300 [
1301 'slug' => 'mwai-dynamic-keys',
1302 'name' => 'Dynamic Keys',
1303 'description' => 'Rotate multiple API keys dynamically for any environment, balancing usage and ensuring smooth performance.',
1304 'install_url' => 'https://meowapps.com/products/mwai-dynamic-keys/',
1305 'settings_url' => null,
1306 'stars' => 1,
1307 'enabled' => false
1308 ],
1309 ] );
1310
1311 // Populate usage data from ai_usage to ai_models_usage for the frontend
1312 $ai_usage = $this->get_option( 'ai_usage', [] );
1313 $options['ai_models_usage'] = $ai_usage;
1314
1315 // Also include daily usage data
1316 $ai_usage_daily = $this->get_option( 'ai_usage_daily', [] );
1317 $options['ai_models_usage_daily'] = $ai_usage_daily;
1318
1319 $populating = false;
1320 return $options;
1321 }
1322
1323 public function get_all_options( $force = false, $sanitize = false ) {
1324 if ( $force || is_null( $this->options ) ) {
1325 $options = get_option( $this->option_name, [] );
1326 $init_mode = empty( $options );
1327 foreach ( MWAI_OPTIONS as $key => $value ) {
1328 if ( !isset( $options[$key] ) ) {
1329 $options[$key] = $value;
1330 }
1331 }
1332 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
1333 $options['default_limits'] = MWAI_LIMITS;
1334
1335 // Force sanitization if custom_languages is not set (migration)
1336 $needs_language_migration = !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] );
1337
1338 if ( $sanitize || $init_mode || $needs_language_migration ) {
1339 $options = $this->sanitize_options( $options );
1340 }
1341 $this->options = $options;
1342 }
1343 $options = $this->populate_dynamic_options( $this->options );
1344 return $options;
1345 }
1346
1347 // Sanitize options when we update the plugin or perform some updates
1348 // if we change the structure of the options.
1349 public function sanitize_options( $options ) {
1350 $needs_update = false;
1351
1352 // Removing old options of options renaming should be done here, as it was done before.
1353 // Check version 2.6.8 for an example.
1354
1355 // Avoid the logs_path to be a PHP file.
1356 if ( isset( $options['logs_path'] ) ) {
1357 $logs_path = $options['logs_path'];
1358 if ( substr( $logs_path, -4 ) !== '.log' ) {
1359 $options['logs_path'] = '';
1360 $needs_update = true;
1361 }
1362 }
1363
1364 // The IDs for the embeddings environments are generated here.
1365 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1366 $embeddings_default_exists = false;
1367 if ( isset( $options['embeddings_envs'] ) ) {
1368 foreach ( $options['embeddings_envs'] as &$env ) {
1369 if ( !isset( $env['id'] ) ) {
1370 $env['id'] = $this->get_random_id();
1371 $needs_update = true;
1372 }
1373 if ( $env['id'] === $options['embeddings_default_env'] ) {
1374 $embeddings_default_exists = true;
1375 }
1376 }
1377 }
1378 if ( !$embeddings_default_exists ) {
1379 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1380 $needs_update = true;
1381 }
1382
1383 // The IDs for the AI environments are generated here.
1384 $allEnvIds = [];
1385 $ai_default_exists = false;
1386 if ( isset( $options['ai_envs'] ) ) {
1387 foreach ( $options['ai_envs'] as &$env ) {
1388 if ( !isset( $env['id'] ) ) {
1389 $env['id'] = $this->get_random_id();
1390 $needs_update = true;
1391 }
1392 if ( $env['id'] === $options['ai_default_env'] ) {
1393 $ai_default_exists = true;
1394 }
1395 $allEnvIds[] = $env['id'];
1396 }
1397 }
1398 if ( !$ai_default_exists ) {
1399 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1400 $needs_update = true;
1401 }
1402
1403 // The IDs for the MCP environments are generated here.
1404 if ( isset( $options['mcp_envs'] ) ) {
1405 foreach ( $options['mcp_envs'] as &$env ) {
1406 if ( !isset( $env['id'] ) ) {
1407 $env['id'] = $this->get_random_id();
1408 $needs_update = true;
1409 }
1410 }
1411 }
1412
1413 // All the models with an envId that does not exist anymore are removed.
1414 if ( isset( $options['ai_models'] ) ) {
1415 $options['ai_models'] = array_values( array_filter(
1416 $options['ai_models'],
1417 function ( $model ) use ( $allEnvIds, &$needs_update ) {
1418 if ( isset( $model['envId'] ) && !in_array( $model['envId'], $allEnvIds ) ) {
1419 $needs_update = true;
1420 return false;
1421 }
1422 return true;
1423 }
1424 ) );
1425 }
1426
1427 // Migration: Populate custom_languages if empty for existing installations
1428 if ( !isset( $options['custom_languages'] ) || empty( $options['custom_languages'] ) ) {
1429 $options['custom_languages'] = [
1430 'English (en)',
1431 'German (de)',
1432 'French (fr)',
1433 'Spanish (es)',
1434 'Italian (it)',
1435 'Chinese (zh)',
1436 'Japanese (ja)',
1437 'Portuguese (pt)'
1438 ];
1439 $needs_update = true;
1440 }
1441
1442 if ( $needs_update ) {
1443 ksort( $options );
1444 update_option( $this->option_name, $options, false );
1445 }
1446
1447 return $options;
1448 }
1449
1450 public function update_options( $options ) {
1451 if ( !update_option( $this->option_name, $options, false ) ) {
1452 return false;
1453 }
1454 $options = $this->get_all_options( true, true );
1455 return $options;
1456 }
1457
1458 public function update_option( $option, $value ) {
1459 $options = $this->get_all_options( true );
1460 $options[$option] = $value;
1461 return $this->update_options( $options );
1462 }
1463
1464 public function get_option( $option, $default = null ) {
1465 $options = $this->get_all_options();
1466 return $options[$option] ?? $default;
1467 }
1468
1469 public function update_ai_env( $env_id, $option, $value ) {
1470 $options = $this->get_all_options( true );
1471 foreach ( $options['ai_envs'] as &$env ) {
1472 if ( $env['id'] === $env_id ) {
1473 $env[$option] = $value;
1474 break;
1475 }
1476 }
1477 return $this->update_options( $options );
1478 }
1479
1480 public function get_engine_models( $engineType ) {
1481 // This method is called by engines with just a string type
1482 // We need to get the models differently
1483 $options = $this->get_all_options();
1484 $engines = $options['ai_envs'];
1485 $models = [];
1486
1487 // Find all models for this engine type
1488 foreach ( $engines as $engine ) {
1489 if ( $engine['type'] === $engineType ) {
1490 if ( isset( $engine['models'] ) ) {
1491 foreach ( $engine['models'] as $model ) {
1492 $models[] = $model;
1493 }
1494 }
1495 }
1496 }
1497
1498 // Also check custom models
1499 if ( isset( $options['ai_models'] ) ) {
1500 foreach ( $options['ai_models'] as $model ) {
1501 if ( $model['type'] === $engineType ) {
1502 $models[] = $model;
1503 }
1504 }
1505 }
1506
1507 return $models;
1508 }
1509
1510 public function reset_options() {
1511 delete_option( $this->themes_option_name );
1512 delete_option( $this->chatbots_option_name );
1513 delete_option( $this->option_name );
1514 return $this->get_all_options( true );
1515 }
1516 #endregion
1517
1518 #region Cron Tracking
1519 public function track_cron_start( $hook ) {
1520 // Set running transient (expires in 5 minutes as a safety measure)
1521 set_transient( 'mwai_cron_running_' . $hook, true, 300 );
1522 }
1523
1524 public function track_cron_end( $hook, $status = 'success', $error_message = '' ) {
1525 // Remove running transient
1526 delete_transient( 'mwai_cron_running_' . $hook );
1527
1528 // Get existing data
1529 $cron_data = get_transient( 'mwai_cron_last_run' ) ?: [];
1530
1531 // Update this cron's data - use time() for consistency
1532 $cron_data[$hook] = [
1533 'time' => time(),
1534 'status' => $status,
1535 'error' => $error_message
1536 ];
1537
1538 // Store for 7 days
1539 set_transient( 'mwai_cron_last_run', $cron_data, 7 * DAY_IN_SECONDS );
1540 }
1541 #endregion
1542 }
1543