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