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