PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.3.8
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.3.8
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / core.php
ai-engine / classes Last commit date
engines 2 years ago modules 2 years ago queries 2 years ago admin.php 2 years ago api.php 2 years ago core.php 2 years ago init.php 2 years ago reply.php 2 years ago rest.php 2 years ago
core.php
1211 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 {
14 public $admin = null;
15 public $is_rest = false;
16 public $is_cli = false;
17 public $site_url = null;
18 public $files = null;
19 public $tasks = null;
20 public $magicWand = 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
29 public function __construct() {
30 $this->site_url = get_site_url();
31 $this->is_rest = MeowCommon_Helpers::is_rest();
32 $this->is_cli = defined( 'WP_CLI' );
33 $this->files = new Meow_MWAI_Modules_Files( $this );
34 $this->tasks = new Meow_MWAI_Modules_Tasks( $this );
35
36 if ( $this->get_option( 'module_suggestions' ) ) {
37 $this->magicWand = new Meow_MWAI_Modules_Wand( $this );
38 }
39
40 add_action( 'plugins_loaded', array( $this, 'init' ) );
41 add_action( 'wp_register_script', array( $this, 'register_scripts' ) );
42 add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) );
43 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
44 }
45
46 #region Init & Scripts
47 function init() {
48 global $mwai;
49 $this->chatbot = null;
50 $this->discussions = null;
51 new Meow_MWAI_Modules_Security( $this );
52 if ( $this->is_rest ) {
53 new Meow_MWAI_Rest( $this );
54 }
55 if ( is_admin() ) {
56 new Meow_MWAI_Admin( $this );
57 new Meow_MWAI_Modules_Utilities( $this );
58 $module_advisor = $this->get_option( 'module_advisor' );
59 if ( $module_advisor ) {
60 new Meow_MWAI_Modules_Advisor( $this );
61 }
62 }
63 if ( $this->get_option( 'shortcode_chat' ) ) {
64 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
65 $this->discussions = new Meow_MWAI_Modules_Discussions();
66 }
67
68 // Advanced core
69 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
70 new MeowPro_MWAI_Core( $this );
71 }
72
73 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions );
74 }
75
76 public function register_scripts() {
77 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
78 }
79
80 public function enqueue_scripts() {
81 $this->register_scripts();
82 wp_enqueue_script( "mwai_highlight" );
83 }
84
85 #endregion
86
87 #region Roles & Capabilities
88
89 function can_access_settings() {
90 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
91 }
92
93 function can_access_features() {
94 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
95 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
96 }
97
98 function can_access_public_api( $feature, $extra ) {
99 $logged_in = is_user_logged_in();
100 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
101 }
102
103 #endregion
104
105 #region AI-Related Helpers
106 function run_query( $query, $streamCallback = null, $markdown = false ) {
107 $envId = !empty( $query->envId ) ? $query->envId : null;
108 $engine = Meow_MWAI_Engines_Factory::get( $this, $envId );
109
110 // If the engine is not set, we need to set it to the default one.
111 if ( !$envId || !$engine->retrieve_model_info( $query->model ) ) {
112 if ( $query instanceof Meow_MWAI_Query_Text ) {
113 $this->set_if_empty_defaults( $query, 'ai_default_env', 'ai_default_model' );
114 }
115 if ( $query instanceof Meow_MWAI_Query_Embed ) {
116 $this->set_if_empty_defaults( $query, 'ai_embeddings_default_env', 'ai_embeddings_default_model' );
117 }
118 else if ( $query instanceof Meow_MWAI_Query_Image ) {
119 $this->set_if_empty_defaults( $query, 'ai_images_default_env', 'ai_images_default_model' );
120 }
121 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
122 $this->set_if_empty_defaults( $query, 'ai_audio_default_env', 'ai_audio_default_model' );
123 }
124 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
125 }
126
127 // Let's run the query.
128 $reply = $engine->run( $query, $streamCallback );
129
130 // Let's allow to modify the reply before it is sent.
131 if ( $markdown ) {
132 if ( $query instanceof Meow_MWAI_Query_Image ) {
133 $reply->result = "";
134 foreach ( $reply->results as $result ) {
135 $reply->result .= "![Image]($result)\n";
136 }
137 }
138 }
139
140 return $reply;
141 }
142
143 private function set_if_empty_defaults( $query, $envOption, $modelOption ) {
144 $defaultEnv = $this->get_option( $envOption );
145 $defaultModel = $this->get_option( $modelOption );
146 if ( empty( $defaultEnv ) || empty( $defaultModel ) ) {
147 throw new Exception( 'AI Engine: The default environment and model are not set.' );
148 }
149 $query->set_env_id( $defaultEnv );
150 $query->set_model( $defaultModel );
151 }
152 #endregion
153
154 #region Text-Related Helpers
155
156 // Clean the text perfectly, resolve shortcodes, etc, etc.
157 function clean_text( $rawText = "" ) {
158 $text = html_entity_decode( $rawText );
159 $text = wp_strip_all_tags( $text );
160 $text = preg_replace( '/[\r\n]+/', "\n", $text );
161 $text = preg_replace( '/\n+/', "\n", $text );
162 $text = preg_replace( '/\t+/', "\t", $text );
163 return $text . " ";
164 }
165
166 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
167 function clean_sentences( $text, $maxLength = null ) {
168 // Step 1: Identify URLs and replace them with a placeholder.
169 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
170 preg_match_all($urlPattern, $text, $urls);
171 $urlPlaceholders = array();
172 foreach ($urls[0] as $index => $url) {
173 $placeholder = "{urlPlaceholder" . $index . "}";
174 $text = str_replace($url, $placeholder, $text);
175 $urlPlaceholders[$placeholder] = $url;
176 }
177
178 $maxLength = (int)($maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ));
179 $sentences = preg_split('/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
180 $hashes = array();
181 $uniqueSentences = array();
182 $total = 0;
183
184 foreach ( $sentences as $sentence ) {
185 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
186 $hash = md5( $sentence );
187 if ( !in_array( $hash, $hashes ) ) {
188 $length = mb_strlen( $sentence, 'UTF-8' );
189 if ( $total + $length > $maxLength ) {
190 continue;
191 }
192 $hashes[] = $hash;
193 $uniqueSentences[] = $sentence;
194 $total += $length;
195 }
196 }
197
198 $freshText = implode( " ", $uniqueSentences );
199
200 // Step 3: Restore URLs in the final text.
201 foreach ($urlPlaceholders as $placeholder => $url) {
202 $freshText = str_replace($placeholder, $url, $freshText);
203 }
204
205 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
206 return $freshText;
207 }
208
209 function get_post_content( $postId ) {
210 $post = get_post( $postId );
211 if ( !$post ) {
212 return false;
213 }
214 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
215 $pattern = '/\[mwai_.*?\]/';
216 $text = preg_replace( $pattern, '', $text );
217 if ( $this->get_option( 'resolve_shortcodes' ) ) {
218 $text = apply_filters( 'the_content', $text );
219 }
220 else {
221 $pattern = "/\[[^\]]+\]/";
222 $text = preg_replace( $pattern, '', $text );
223 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
224 $text = preg_replace( $pattern, '', $text );
225 }
226 $text = $this->clean_text( $text );
227 $text = $this->clean_sentences( $text );
228 $text = apply_filters( 'mwai_post_content', $text, $postId );
229 return $text;
230 }
231
232 function markdown_to_html( $content ) {
233 $Parsedown = new Parsedown();
234 $content = $Parsedown->text( $content );
235 return $content;
236 }
237
238 function get_post_language( $postId ) {
239 $locale = get_locale();
240 $code = strtolower( substr( $locale, 0, 2 ) );
241 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
242 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
243 if ( !empty( $lang ) ) {
244 $locale = $lang['locale'];
245 $humanLanguage = $lang['display_name'];
246 }
247 return strtolower( "$locale ($humanLanguage)" );
248 }
249 #endregion
250
251 #region Image-Related Helpers
252 static function is_image( $file ) {
253 $mimeType = Meow_MWAI_Core::get_mime_type( $file );
254 if ( strpos( $mimeType, 'image' ) !== false ) {
255 return true;
256 }
257 return false;
258 }
259
260 static function get_mime_type( $file ) {
261 $mimeType = null;
262
263 // Let's try to use mime_content_type if the function exists
264 $isUrl = filter_var( $file, FILTER_VALIDATE_URL );
265 if ( !$isUrl && function_exists( 'mime_content_type' ) ) {
266 $mimeType = mime_content_type( $file );
267 }
268
269 // Otherwise, let's check the file extension (which can actually also be an URL)
270 if ( !$mimeType ) {
271 $extension = pathinfo( $file, PATHINFO_EXTENSION );
272 $extension = strtolower( $extension );
273 $mimeTypes = [
274 'jpg' => 'image/jpeg',
275 'jpeg' => 'image/jpeg',
276 'png' => 'image/png',
277 'gif' => 'image/gif',
278 'webp' => 'image/webp',
279 'bmp' => 'image/bmp',
280 'tiff' => 'image/tiff',
281 'tif' => 'image/tiff',
282 'svg' => 'image/svg+xml',
283 'ico' => 'image/x-icon',
284 'pdf' => 'application/pdf',
285 ];
286 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
287 }
288
289 return $mimeType;
290 }
291
292 function download_image( $url ) {
293 $args = array( 'timeout' => 60, );
294 $response = wp_safe_remote_get( $url, $args );
295 if ( is_wp_error( $response ) ) {
296 throw new Exception( $response->get_error_message() );
297 }
298 $output = wp_remote_retrieve_body( $response );
299 if ( is_wp_error( $output ) ) {
300 throw new Exception( $output->get_error_message() );
301 }
302 return $output;
303 }
304
305 /**
306 * Add an image from a URL to the Media Library.
307 * @param string $url The URL of the image to be downloaded.
308 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
309 * @param string $title The title of the image.
310 * @param string $description The description of the image.
311 * @param string $caption The caption of the image.
312 * @param string $alt The alt text of the image.
313 * @return int The attachment ID of the image.
314 */
315 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null ) {
316 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
317 $url_filename = $path_parts['basename'];
318 $file_type = wp_check_filetype( $url_filename, null );
319 $allowed_types = get_allowed_mime_types();
320 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
321 throw new Exception( 'Invalid file type from URL.' );
322 }
323
324 // Initial extension from URL file name
325 $extension = $file_type['ext'];
326
327 if ( !empty( $filename ) ) {
328 $custom_file_type = wp_check_filetype( $filename, null );
329 if ( !$custom_file_type || !in_array( $custom_file_type['type'], $allowed_types ) ) {
330 throw new Exception( 'Invalid custom file type.' );
331 }
332 // Use the extension from the custom filename if valid
333 $extension = $custom_file_type['ext'];
334 }
335
336 $image_data = $this->download_image( $url );
337 if ( !$image_data ) {
338 throw new Exception( 'Could not download the image.' );
339 }
340 $upload_dir = wp_upload_dir();
341
342 // Filename handling including 'generated_' prefix scenario
343 if ( empty( $filename ) ) {
344 $filename = sanitize_file_name( $url_filename );
345 if ( empty( $extension ) ) { // This condition might now be redundant
346 $extension = $file_type['ext'];
347 }
348 // Filename length check and prepend if conditions met
349 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
350 $filename = $this->get_random_id( 16 ) . '.' . $extension;
351 }
352 if ( strpos( $filename, '.' ) === false ) {
353 $filename .= '.' . $extension;
354 }
355 }
356
357 // Directory and file path handling
358 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
359 $file = $upload_dir['path'] . '/' . $filename;
360 }
361 else {
362 $file = $upload_dir['basedir'] . '/' . $filename;
363 }
364
365 // Ensure file name uniqueness in the directory
366 $i = 1;
367 $parts = pathinfo( $file );
368 while ( file_exists( $file ) ) {
369 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
370 $i++;
371 }
372
373 // Writing the file to disk
374 file_put_contents( $file, $image_data );
375
376 // Attachment and metadata handling in WP
377 $attachment = [
378 'post_mime_type' => $file_type['type'],
379 'post_title' => $title ?? '',
380 'post_content' => $description ?? '',
381 'post_excerpt' => $caption ?? '',
382 'post_status' => 'inherit'
383 ];
384 $attachmentId = wp_insert_attachment( $attachment, $file );
385 require_once( ABSPATH . 'wp-admin/includes/image.php' );
386 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
387 wp_update_attachment_metadata( $attachmentId, $attachment_data );
388 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
389
390 return $attachmentId;
391 }
392 #endregion
393
394 #region Context-Related Helpers
395 function retrieve_context( $params, $query ) {
396 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
397 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
398 $context = apply_filters( 'mwai_context_search', [], $query, [
399 'embeddingsEnvId' => $embeddingsEnvId
400 ]);
401 if ( empty( $context ) ) {
402 return null;
403 }
404 else if ( !isset( $context['content'] ) ) {
405 $this->log( "⚠️ A context without content was returned." );
406 return null;
407 }
408 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
409 $context['length'] = strlen( $context['content'] );
410 return $context;
411 }
412 #endregion
413
414 #region Users/Sessions Helpers
415
416 function get_nonce() {
417 // if ( !is_user_logged_in() ) {
418 // return null;
419 // }
420 if ( isset( $this->nonce ) ) {
421 return $this->nonce;
422 }
423 $this->nonce = wp_create_nonce( 'wp_rest' );
424 return $this->nonce;
425 }
426
427 function get_session_id() {
428 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
429 return $_COOKIE['mwai_session_id'];
430 }
431 return "N/A";
432 }
433
434 // Get the UserID from the data, or from the current user
435 function get_user_id( $data = null ) {
436 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
437 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
438 $admin = get_users( [ 'role' => 'administrator' ] );
439 if ( !empty( $admin ) ) {
440 return $admin[0]->ID;
441 }
442 }
443 if ( isset( $data ) && isset( $data['userId'] ) ) {
444 return (int)$data['userId'];
445 }
446 if ( is_user_logged_in() ) {
447 $current_user = wp_get_current_user();
448 if ( $current_user->ID > 0 ) {
449 return $current_user->ID;
450 }
451 }
452 return null;
453 }
454
455 function get_admin_user() {
456 $admin = get_users( [ 'role' => 'administrator' ] );
457 if ( !empty( $admin ) ) {
458 return $admin[0];
459 }
460 return null;
461 }
462
463 function get_user_data() {
464 $user = wp_get_current_user();
465 if ( empty( $user ) || empty( $user->ID ) ) {
466 return null;
467 }
468 $placeholders = array(
469 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
470 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
471 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
472 $user->data->user_login : null,
473 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
474 $user->data->display_name : null,
475 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
476 );
477 return $placeholders;
478 }
479
480 function get_ip_address( $params = null ) {
481 $ip = '127.0.0.1';
482 $headers = [
483 'HTTP_TRUE_CLIENT_IP',
484 'HTTP_CF_CONNECTING_IP',
485 'HTTP_X_REAL_IP',
486 'HTTP_CLIENT_IP',
487 'HTTP_X_FORWARDED_FOR',
488 'HTTP_X_FORWARDED',
489 'HTTP_X_CLUSTER_CLIENT_IP',
490 'HTTP_FORWARDED_FOR',
491 'HTTP_FORWARDED',
492 'REMOTE_ADDR',
493 ];
494
495 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
496 $ip = ( string )$params[ 'ip' ];
497 } else {
498 foreach ( $headers as $header ) {
499 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
500 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
501 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
502 break;
503 }
504 }
505 }
506
507 return filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
508 }
509
510 #endregion
511
512 #region Other Helpers
513
514 public function check_rest_nonce( $request ) {
515 $nonce = $request->get_header( 'X-WP-Nonce' );
516 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
517 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
518 }
519
520 function get_random_id( $length = 8, $excludeIds = [] ) {
521 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
522 $charactersLength = strlen( $characters );
523 $randomId = '';
524 for ( $i = 0; $i < $length; $i++ ) {
525 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
526 }
527 if ( in_array( $randomId, $excludeIds ) ) {
528 return $this->get_random_id( $length, $excludeIds );
529 }
530 return $randomId;
531 }
532
533 function is_url( $url ) {
534 return strpos( $url, 'http' ) === 0 ? true : false;
535 }
536
537 function get_post_types() {
538 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
539 $post_types = array();
540 $types = get_post_types( [], 'objects' );
541
542 // Let's get the Post Types that are enabled for Embeddings Sync
543 $embeddingsSettings = $this->get_option( 'embeddings' );
544 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
545
546 foreach ( $types as $type ) {
547 $forced = in_array( $type->name, $syncPostTypes );
548 // Should not be excluded.
549 if ( !$forced && in_array( $type->name, $excluded ) ) {
550 continue;
551 }
552 // Should be public.
553 if ( !$forced && !$type->public ) {
554 continue;
555 }
556 $post_types[] = array(
557 'name' => $type->labels->name,
558 'type' => $type->name,
559 );
560 }
561
562 // Let's get the Post Types that are enabled for Embeddings Sync
563 $embeddingsSettings = $this->get_option( 'embeddings' );
564 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
565
566 return $post_types;
567 }
568
569 function get_post( $post ) {
570 if ( is_numeric( $post ) ) {
571 $post = get_post( $post );
572 }
573 if ( is_object( $post ) ) {
574 $post = (array)$post;
575 }
576 if ( !is_array( $post ) ) {
577 return null;
578 }
579 $language = $this->get_post_language( $post['ID'] );
580 $content = $this->get_post_content( $post['ID'] );
581 $title = $post['post_title'];
582 $excerpt = $post['post_excerpt'];
583 $url = get_permalink( $post['ID'] );
584 $checksum = wp_hash( $content . $title . $url );
585 return [
586 'postId' => (int)$post['ID'],
587 'title' => $title,
588 'content' => $content,
589 'excerpt' => $excerpt,
590 'url' => $url,
591 'language' => $language ?? 'english',
592 'checksum' => $checksum,
593 ];
594 }
595 #endregion
596
597 #region Usage & Costs
598
599 // Quick and dirty token estimation
600 // Let's keep this synchronized with Helpers in JS
601 static function estimate_tokens( ...$args ): int {
602 $text = "";
603 foreach ( $args as $arg ) {
604 if ( is_array( $arg ) ) {
605 foreach ( $arg as $message ) {
606 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
607 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
608 }
609 }
610 else if ( is_string( $arg ) ) {
611 $text .= $arg;
612 }
613 }
614 $averageTokenLength = 4;
615 $words = preg_split( '/\s+/', trim( $text ) );
616 $tokenCount = 0;
617 foreach ( $words as $word ) {
618 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
619 }
620 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
621 }
622
623 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
624 if ( !is_numeric( $in_tokens ) ) {
625 throw new Exception( 'AI Engine: in_tokens must be a number.' );
626 }
627 if ( !is_numeric( $out_tokens ) ) {
628 $out_tokens = 0;
629 }
630 if ( !$model ) {
631 throw new Exception( 'AI Engine: model is required.' );
632 }
633 $usage = $this->get_option( 'openai_usage' );
634 $month = date( 'Y-m' );
635 if ( !isset( $usage[$month] ) ) {
636 $usage[$month] = array();
637 }
638 if ( !isset( $usage[$month][$model] ) ) {
639 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
640 }
641 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
642 $usage[$month][$model]['completion_tokens'] += $out_tokens;
643 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
644 $this->update_option( 'openai_usage', $usage );
645 $usageInfo = [
646 'prompt_tokens' => $in_tokens,
647 'completion_tokens' => $out_tokens,
648 'total_tokens' => $in_tokens + $out_tokens,
649 ];
650 if ( $returned_price !== null ) {
651 $usageInfo['price'] = $returned_price;
652 }
653 return $usageInfo;
654 }
655
656 public function record_audio_usage( $model, $seconds ) {
657 if ( !is_numeric( $seconds ) ) {
658 throw new Exception( 'AI Engine: seconds must be a number.' );
659 }
660 if ( !$model ) {
661 throw new Exception( 'AI Engine: model is required.' );
662 }
663 $usage = $this->get_option( 'openai_usage' );
664 $month = date( 'Y-m' );
665 if ( !isset( $usage[$month] ) ) {
666 $usage[$month] = array();
667 }
668 if ( !isset( $usage[$month][$model] ) ) {
669 $usage[$month][$model] = array( 'seconds' => 0 );
670 }
671 $usage[$month][$model]['seconds'] += $seconds;
672 $this->update_option( 'openai_usage', $usage );
673 return [ 'seconds' => $seconds ];
674 }
675
676 public function record_images_usage( $model, $resolution, $images ) {
677 if ( !$model || !$resolution || !$images ) {
678 throw new Exception( 'Missing parameters for record_image_usage.' );
679 }
680 $usage = $this->get_option( 'openai_usage' );
681 $month = date( 'Y-m' );
682 if ( !isset( $usage[$month] ) ) {
683 $usage[$month] = array();
684 }
685 if ( !isset( $usage[$month][$model] ) ) {
686 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
687 }
688 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
689 $usage[$month][$model]['resolution'][$resolution] = 0;
690 }
691 $usage[$month][$model]['resolution'][$resolution] += $images;
692 $usage[$month][$model]['images'] += $images;
693 $this->update_option( 'openai_usage', $usage );
694 return [ 'resolution' => $resolution, 'images' => $images ];
695 }
696
697 #endregion
698
699 #region Streaming
700 public function stream_push( $data ) {
701 $out = "data: " . json_encode( $data );
702 echo $out;
703 echo "\n\n";
704 if ( ob_get_level() > 0 ) {
705 ob_end_flush();
706 }
707 flush();
708 }
709 #endregion
710
711 #region Options
712 function get_themes() {
713 $themes = get_option( $this->themes_option_name, [] );
714 $themes = empty( $themes ) ? [] : $themes;
715
716 $internalThemes = [
717 'chatgpt' => [
718 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
719 'settings' => [], 'style' => ""
720 ],
721 'messages' => [
722 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
723 'settings' => [], 'style' => ""
724 ],
725 ];
726 $customThemes = [];
727 foreach ( $themes as $theme ) {
728 if ( isset( $internalThemes[$theme['themeId']] ) ) {
729 $internalThemes[$theme['themeId']] = $theme;
730 continue;
731 }
732 $customThemes[] = $theme;
733 }
734 return array_merge(array_values($internalThemes), $customThemes);
735 }
736
737 function update_themes( $themes ) {
738 update_option( $this->themes_option_name, $themes );
739 return $themes;
740 }
741
742 function get_chatbots() {
743 $chatbots = get_option( $this->chatbots_option_name, [] );
744 $hasChanges = false;
745 if ( empty( $chatbots ) ) {
746 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
747 }
748 $hasDefault = false;
749 foreach ( $chatbots as &$chatbot ) {
750 if ( $chatbot['botId'] === 'default' ) {
751 $hasDefault = true;
752 }
753 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
754 // Use default value if not set.
755 if ( !isset( $chatbot[$key] ) ) {
756 $chatbot[$key] = $value;
757 }
758 }
759 // TODO: After October 2024, let's remove this.
760 if ( isset( $chatbot['context'] ) ) {
761 $chatbot['instructions'] = $chatbot['context'];
762 unset( $chatbot['context'] );
763 $hasChanges = true;
764 }
765 // TODO: After October 2024, let's remove this.
766 if ( isset( $chatbot['fileUpload'] ) ) {
767 $chatbot['fileSearch'] = $chatbot['fileUpload'];
768 unset( $chatbot['fileUpload'] );
769 $hasChanges = true;
770 }
771 }
772 if ( !$hasDefault ) {
773 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
774 array_unshift( $chatbots, $defaultBot );
775 $hasChanges = true;
776 }
777 if ( $hasChanges ) {
778 update_option( $this->chatbots_option_name, $chatbots );
779 }
780 return $chatbots;
781 }
782
783 function get_chatbot( $botId ) {
784 $chatbots = $this->get_chatbots();
785 foreach ( $chatbots as $chatbot ) {
786 if ( $chatbot['botId'] === (string)$botId ) {
787 return $chatbot;
788 }
789 }
790 return null;
791 }
792
793 function get_embeddings_env( $envId ) {
794 $envs = $this->get_option( 'embeddings_envs' );
795 foreach ( $envs as $env ) {
796 if ( $env['id'] === $envId ) {
797 return $env;
798 }
799 }
800 return null;
801 }
802
803 function get_ai_env( $envId ) {
804 $envs = $this->get_option( 'ai_envs' );
805 foreach ( $envs as $env ) {
806 if ( $env['id'] === $envId ) {
807 return $env;
808 }
809 }
810 return null;
811 }
812
813 function get_assistant( $envId, $assistantId ) {
814 $env = $this->get_ai_env( $envId );
815 if ( !$env ) {
816 return null;
817 }
818 $assistants = $env['assistants'];
819 foreach ( $assistants as $assistant ) {
820 if ( $assistant['id'] === $assistantId ) {
821 return $assistant;
822 }
823 }
824 return null;
825 }
826
827 function get_theme( $themeId ) {
828 $themes = $this->get_themes();
829 foreach ( $themes as $theme ) {
830 if ( $theme['themeId'] === $themeId ) {
831 return $theme;
832 }
833 }
834 return null;
835 }
836
837 function update_chatbots( $chatbots ) {
838 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
839 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
840 $keepLineReturnsFields = [ 'instructions' ];
841 $whiteSpacedFields = [ 'context' ];
842 foreach ( $chatbots as &$chatbot ) {
843 foreach ( $chatbot as $key => &$value ) {
844 if ( in_array( $key, $deprecatedFields ) ) {
845 unset( $chatbot[$key] );
846 continue;
847 }
848 if ( in_array( $key, $htmlFields ) ) {
849 $value = wp_kses_post( $value );
850 }
851 else if ( in_array( $key, $whiteSpacedFields ) ) {
852 $value = sanitize_textarea_field( $value );
853 }
854 else if ( $key === 'functions' ) {
855 $functions = [];
856 foreach ( $value as $function ) {
857 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
858 $functions[] = [
859 'id' => sanitize_text_field( $function['id'] ),
860 'type' => sanitize_text_field( $function['type'] ),
861 ];
862 }
863 }
864 $value = $functions;
865 }
866 else {
867 if ( in_array( $key, $keepLineReturnsFields ) ) {
868 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
869 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
870 }
871 $value = sanitize_text_field( $value );
872 if ( in_array( $key, $keepLineReturnsFields ) ) {
873 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
874 }
875 }
876 }
877 }
878 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
879 $this->log( '⚠️ Could not update chatbots.' );
880 $chatbots = get_option( $this->chatbots_option_name, [] );
881 return $chatbots;
882 }
883 return $chatbots;
884 }
885
886 function get_all_options( $force = false ) {
887 // We could cache options this way, but if we do, the apply_filters seems to be called too early.
888 // That causes issues with the mwai_languages filter.
889 // if ( !$force && !is_null( $this->options ) ) {
890 // return $this->options;
891 // }
892 $options = get_option( $this->option_name, [] );
893 $options = $this->sanitize_options( $options );
894 foreach ( MWAI_OPTIONS as $key => $value ) {
895 if ( !isset( $options[$key] ) ) {
896 $options[$key] = $value;
897 }
898 if ( $key === 'languages' ) {
899 // NOTE: If we decide to make a set of options for languages, we can keep it in the settings
900 $options[$key] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
901 }
902 }
903 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
904 $options['default_limits'] = MWAI_LIMITS;
905 $options['openai_models'] = apply_filters(
906 'mwai_openai_models',
907 Meow_MWAI_Engines_OpenAI::get_models_static()
908 );
909 $options['anthropic_models'] = apply_filters(
910 'mwai_anthropic_models',
911 Meow_MWAI_Engines_Anthropic::get_models_static()
912 );
913 $options['fallback_model'] = MWAI_FALLBACK_MODEL;
914
915 // Support for functions from Snippet Vault
916 $options['functions'] = apply_filters( 'mwai_functions_list', [] );
917
918 //$this->options = $options;
919 return $options;
920 }
921
922 // Sanitize options when we update the plugi or perform some updates
923 // if we change the structure of the options.
924 function sanitize_options( $options ) {
925 $needs_update = false;
926
927 // This list was updated on December 11, 2023. After May 2024, let's remove this.
928 $old_options = [
929 'shortcode_chat_default_params',
930 'shortcode_chat_params_override',
931 'module_legacy_finetunes',
932 'shortcode_chat_legacy',
933 'shortcode_chat_inject',
934 'shortcode_chat_styles',
935 'dynamic_max_tokens',
936 'shortcode_chat_formatting',
937 'shortcode_forms_legacy',
938 ];
939 foreach ( $old_options as $old_option ) {
940 if ( isset( $options[$old_option] ) ) {
941 unset( $options[$old_option] );
942 $needs_update = true;
943 }
944 }
945
946 // This upgrades namespace to multi-namespaces (June 2023)
947 // After January 2024, let's remove this.
948 if ( isset( $options['pinecone'] ) && isset( $options['pinecone']['namespace'] ) ) {
949 $options['pinecone']['namespaces'] = [ $options['pinecone']['namespace'] ];
950 unset( $options['pinecone']['namespace'] );
951 $needs_update = true;
952 }
953 // Support for Multi Vector DB Environments
954 // After June 2024, let's remove this.
955 if ( !isset( $options['embeddings_envs'] ) ) {
956 $options['embeddings_envs'] = [];
957 $default_id = $this->get_random_id();
958 $pinecone = isset( $options['pinecone'] ) ? $options['pinecone'] : [];
959 $options['embeddings_envs'][] = [
960 'id' => $default_id,
961 'name' => 'Pinecone',
962 'type' => 'pinecone',
963 'apikey' => isset( $pinecone['apikey'] ) ? $pinecone['apikey'] : '',
964 'server' => isset( $pinecone['server'] ) ? $pinecone['server'] : 'gcp-starter',
965 'indexes' => isset( $pinecone['indexes'] ) ? $pinecone['indexes'] : [],
966 'namespaces' => isset( $pinecone['namespaces'] ) ? $pinecone['namespaces'] : [],
967 'index' => isset( $pinecone['index'] ) ? $pinecone['index'] : null,
968 ];
969 $options['embeddings_default_env'] = $default_id;
970 $needs_update = true;
971 }
972 if ( isset( $options['pinecone'] ) ) {
973 unset( $options['pinecone'] );
974 $needs_update = true;
975 }
976 // Support for Multi AI Environments
977 // After June 2024, let's remove this.
978 if ( !isset( $options['ai_envs'] ) ) {
979 $options['ai_envs'] = [];
980 $default_openai_id = $this->get_random_id();
981 $default_azure_id = $this->get_random_id();
982 $openai_service = isset( $options['openai_service'] ) ? $options['openai_service'] : 'openai';
983 $openai_apikey = isset( $options['openai_apikey'] ) ? $options['openai_apikey'] : '';
984 $azure_endpoint = isset( $options['openai_azure_endpoint'] ) ? $options['openai_azure_endpoint'] : '';
985
986 // OpenAI
987 // We create a default OpenAI environment if the API Key is set, or if the Azure Endpoint is not set.
988 if ( !empty( $openai_apikey ) || empty( $azure_endpoint ) ) {
989 $openai_finetunes = isset( $options['openai_finetunes'] ) ? $options['openai_finetunes'] : [];
990 $openai_finetunes_deleted = isset( $options['openai_finetunes_deleted'] ) ?
991 $options['openai_finetunes_deleted'] : [];
992 $openai_legacy_finetunes = isset( $options['openai_legacy_finetunes'] ) ?
993 $options['openai_legacy_finetunes'] : [];
994 $openai_legacy_finetunes_deleted = isset( $options['openai_legacy_finetunes_deleted'] ) ?
995 $options['openai_legacy_finetunes_deleted'] : [];
996 $options['ai_envs'][] = [
997 'id' => $default_openai_id,
998 'name' => 'OpenAI',
999 'type' => 'openai',
1000 'apikey' => $openai_apikey,
1001 'finetunes' => $openai_finetunes,
1002 'finetunes_deleted' => $openai_finetunes_deleted,
1003 'legacy_finetunes' => $openai_legacy_finetunes,
1004 'legacy_finetunes_deleted' => $openai_legacy_finetunes_deleted
1005 ];
1006 }
1007
1008 // Azure
1009 if ( !empty( $azure_endpoint ) ) {
1010 $azure_apikey = isset( $options['openai_azure_apikey'] ) ? $options['openai_azure_apikey'] : '';
1011 $azure_deployments = isset( $options['openai_azure_deployments'] ) ? $options['openai_azure_deployments'] : [];
1012 $options['ai_envs'][] = [
1013 'id' => $default_azure_id,
1014 'name' => 'Azure',
1015 'type' => 'azure',
1016 'apikey' => $azure_apikey,
1017 'endpoint' => $azure_endpoint,
1018 'deployments' => $azure_deployments,
1019 ];
1020 }
1021
1022 $options['ai_default_env'] = $default_openai_id;
1023 if ( $openai_service === 'azure' ) {
1024 $options['ai_default_env'] = $default_azure_id;
1025 }
1026 $needs_update = true;
1027 }
1028 if ( !empty( $options['openai_apikey'] ) || !empty( $options['openai_azure_apikey'] ) ) {
1029 unset( $options['openai_apikey'] );
1030 unset( $options['openai_finetunes'] );
1031 unset( $options['openai_finetunes_deleted'] );
1032 unset( $options['openai_legacy_finetunes'] );
1033 unset( $options['openai_legacy_finetunes_deleted'] );
1034 unset( $options['openai_azure_apikey'] );
1035 unset( $options['openai_azure_endpoint'] );
1036 unset( $options['openai_azure_deployments'] );
1037 unset( $options['openai_service'] );
1038 $needs_update = true;
1039 }
1040
1041 // The IDs for the embeddings environments are generated here.
1042 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1043 $embeddings_default_exists = false;
1044 if ( isset( $options['embeddings_envs'] ) ) {
1045 foreach ( $options['embeddings_envs'] as &$env ) {
1046 if ( !isset( $env['id'] ) ) {
1047 $env['id'] = $this->get_random_id();
1048 $needs_update = true;
1049 }
1050 if ( $env['id'] === $options['embeddings_default_env'] ) {
1051 $embeddings_default_exists = true;
1052 }
1053 }
1054 }
1055 if ( !$embeddings_default_exists ) {
1056 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1057 $needs_update = true;
1058 }
1059
1060 // The IDs for the AI environments are generated here.
1061 $ai_default_exists = false;
1062 if ( isset( $options['ai_envs'] ) ) {
1063 foreach ( $options['ai_envs'] as &$env ) {
1064 if ( !isset( $env['id'] ) ) {
1065 $env['id'] = $this->get_random_id();
1066 $needs_update = true;
1067 }
1068 if ( $env['id'] === $options['ai_default_env'] ) {
1069 $ai_default_exists = true;
1070 }
1071 }
1072 }
1073 if ( !$ai_default_exists ) {
1074 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1075 $needs_update = true;
1076 }
1077
1078 if ( $needs_update ) {
1079 update_option( $this->option_name, $options, false );
1080 }
1081
1082 return $options;
1083 }
1084
1085 function update_options( $options ) {
1086 if ( !update_option( $this->option_name, $options, false ) ) {
1087 return false;
1088 }
1089 $options = $this->get_all_options( true );
1090 return $options;
1091 }
1092
1093 function update_option( $option, $value ) {
1094 $options = $this->get_all_options( true );
1095 $options[$option] = $value;
1096 return $this->update_options( $options );
1097 }
1098
1099 function get_option( $option, $default = null ) {
1100 $options = $this->get_all_options();
1101 return $options[$option] ?? $default;
1102 }
1103
1104 function update_ai_env( $env_id, $option, $value ) {
1105 $options = $this->get_all_options( true );
1106 foreach ( $options['ai_envs'] as &$env ) {
1107 if ( $env['id'] === $env_id ) {
1108 $env[$option] = $value;
1109 break;
1110 }
1111 }
1112 return $this->update_options( $options );
1113 }
1114
1115 function reset_options() {
1116 delete_option( $this->themes_option_name );
1117 delete_option( $this->chatbots_option_name );
1118 delete_option( $this->option_name );
1119 return $this->get_all_options( true );
1120 }
1121 #endregion
1122
1123 #region Logs
1124
1125 function get_logs() {
1126 $log_file_path = $this->get_logs_path();
1127
1128 if ( !file_exists( $log_file_path ) ) {
1129 return "Empty log file.";
1130 }
1131
1132 $content = file_get_contents( $log_file_path );
1133 $lines = explode( "\n", $content );
1134 $lines = array_filter( $lines );
1135 $lines = array_reverse( $lines );
1136 $content = implode( "\n", $lines );
1137 return $content;
1138 }
1139
1140 function clear_logs() {
1141 $logPath = $this->get_logs_path();
1142 if ( file_exists( $logPath ) ) {
1143 unlink( $logPath );
1144 }
1145
1146 $options = $this->get_all_options();
1147 $options['logs_path'] = null;
1148 $this->update_options( $options );
1149 }
1150
1151 function get_logs_path() {
1152 $uploads_dir = wp_upload_dir();
1153 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
1154
1155 $path = $this->get_option( 'logs_path' );
1156
1157 if ( $path && file_exists( $path ) ) {
1158 // make sure the path is legal (within the uploads directory with the MWAI_PREFIX and log extension)
1159 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWAI_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
1160 $path = null;
1161 } else {
1162 return $path;
1163 }
1164 }
1165
1166 if ( !$path ) {
1167 $path = $uploads_dir_path . MWAI_PREFIX . "_" . $this->random_ascii_chars() . ".log";
1168 if ( !file_exists( $path ) ) {
1169 touch( $path );
1170 }
1171 $options = $this->get_all_options();
1172 $options['logs_path'] = $path;
1173 $this->update_options( $options );
1174 }
1175
1176 return $path;
1177 }
1178
1179 function log( $data = null ) {
1180 if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
1181 $log_file_path = $this->get_logs_path();
1182 $fh = @fopen( $log_file_path, 'a' );
1183 if ( !$fh ) { return false; }
1184 $date = date( "Y-m-d H:i:s" );
1185 if ( is_null( $data ) ) {
1186 fwrite( $fh, "\n" );
1187 }
1188 else {
1189 fwrite( $fh, "$date: {$data}\n" );
1190 //error_log( "[MWAI] $data" );
1191 }
1192 fclose( $fh );
1193 return true;
1194 }
1195
1196 private function random_ascii_chars( $length = 8 ) {
1197 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
1198 $characters_length = count( $characters );
1199 $random_string = '';
1200
1201 for ( $i = 0; $i < $length; $i++ ) {
1202 $random_string .= $characters[rand(0, $characters_length - 1)];
1203 }
1204
1205 return $random_string;
1206 }
1207
1208 #endregion
1209 }
1210
1211 ?>