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