PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.3.7
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.3.7
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
1207 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 }
59 if ( $this->get_option( 'shortcode_chat' ) ) {
60 $this->chatbot = new Meow_MWAI_Modules_Chatbot();
61 $this->discussions = new Meow_MWAI_Modules_Discussions();
62 }
63
64 // Advanced core
65 if ( class_exists( 'MeowPro_MWAI_Core' ) ) {
66 new MeowPro_MWAI_Core( $this );
67 }
68
69 $mwai = new Meow_MWAI_API( $this->chatbot, $this->discussions );
70 }
71
72 public function register_scripts() {
73 wp_register_script( 'mwai_highlight', MWAI_URL . 'vendor/highlightjs/highlight.min.js', [], '11.7', false );
74 }
75
76 public function enqueue_scripts() {
77 $this->register_scripts();
78 wp_enqueue_script( "mwai_highlight" );
79 }
80
81 #endregion
82
83 #region Roles & Capabilities
84
85 function can_access_settings() {
86 return apply_filters( 'mwai_allow_setup', current_user_can( 'manage_options' ) );
87 }
88
89 function can_access_features() {
90 $editor_or_admin = current_user_can( 'editor' ) || current_user_can( 'administrator' );
91 return apply_filters( 'mwai_allow_usage', $editor_or_admin );
92 }
93
94 function can_access_public_api( $feature, $extra ) {
95 $logged_in = is_user_logged_in();
96 return apply_filters( 'mwai_allow_public_api', $logged_in, $feature, $extra );
97 }
98
99 #endregion
100
101 #region AI-Related Helpers
102 function run_query( $query, $streamCallback = null, $markdown = false ) {
103 $envId = !empty( $query->envId ) ? $query->envId : null;
104 $engine = Meow_MWAI_Engines_Factory::get( $this, $envId );
105
106 // If the engine is not set, we need to set it to the default one.
107 if ( !$envId || !$engine->retrieve_model_info( $query->model ) ) {
108 if ( $query instanceof Meow_MWAI_Query_Text ) {
109 $this->set_if_empty_defaults( $query, 'ai_default_env', 'ai_default_model' );
110 }
111 if ( $query instanceof Meow_MWAI_Query_Embed ) {
112 $this->set_if_empty_defaults( $query, 'ai_embeddings_default_env', 'ai_embeddings_default_model' );
113 }
114 else if ( $query instanceof Meow_MWAI_Query_Image ) {
115 $this->set_if_empty_defaults( $query, 'ai_images_default_env', 'ai_images_default_model' );
116 }
117 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
118 $this->set_if_empty_defaults( $query, 'ai_audio_default_env', 'ai_audio_default_model' );
119 }
120 $engine = Meow_MWAI_Engines_Factory::get( $this, $query->envId );
121 }
122
123 // Let's run the query.
124 $reply = $engine->run( $query, $streamCallback );
125
126 // Let's allow to modify the reply before it is sent.
127 if ( $markdown ) {
128 if ( $query instanceof Meow_MWAI_Query_Image ) {
129 $reply->result = "";
130 foreach ( $reply->results as $result ) {
131 $reply->result .= "![Image]($result)\n";
132 }
133 }
134 }
135
136 return $reply;
137 }
138
139 private function set_if_empty_defaults( $query, $envOption, $modelOption ) {
140 $defaultEnv = $this->get_option( $envOption );
141 $defaultModel = $this->get_option( $modelOption );
142 if ( empty( $defaultEnv ) || empty( $defaultModel ) ) {
143 throw new Exception( 'AI Engine: The default environment and model are not set.' );
144 }
145 $query->set_env_id( $defaultEnv );
146 $query->set_model( $defaultModel );
147 }
148 #endregion
149
150 #region Text-Related Helpers
151
152 // Clean the text perfectly, resolve shortcodes, etc, etc.
153 function clean_text( $rawText = "" ) {
154 $text = html_entity_decode( $rawText );
155 $text = wp_strip_all_tags( $text );
156 $text = preg_replace( '/[\r\n]+/', "\n", $text );
157 $text = preg_replace( '/\n+/', "\n", $text );
158 $text = preg_replace( '/\t+/', "\t", $text );
159 return $text . " ";
160 }
161
162 // Make sure there are no duplicate sentences, and keep the length under a maximum length.
163 function clean_sentences( $text, $maxLength = null ) {
164 // Step 1: Identify URLs and replace them with a placeholder.
165 $urlPattern = '/\bhttps?:\/\/[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|\/))/';
166 preg_match_all($urlPattern, $text, $urls);
167 $urlPlaceholders = array();
168 foreach ($urls[0] as $index => $url) {
169 $placeholder = "{urlPlaceholder" . $index . "}";
170 $text = str_replace($url, $placeholder, $text);
171 $urlPlaceholders[$placeholder] = $url;
172 }
173
174 $maxLength = (int)($maxLength ? $maxLength : $this->get_option( 'context_max_length', 4096 ));
175 $sentences = preg_split('/(?<=[.?!。.!?])\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
176 $hashes = array();
177 $uniqueSentences = array();
178 $total = 0;
179
180 foreach ( $sentences as $sentence ) {
181 $sentence = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $sentence );
182 $hash = md5( $sentence );
183 if ( !in_array( $hash, $hashes ) ) {
184 $length = mb_strlen( $sentence, 'UTF-8' );
185 if ( $total + $length > $maxLength ) {
186 continue;
187 }
188 $hashes[] = $hash;
189 $uniqueSentences[] = $sentence;
190 $total += $length;
191 }
192 }
193
194 $freshText = implode( " ", $uniqueSentences );
195
196 // Step 3: Restore URLs in the final text.
197 foreach ($urlPlaceholders as $placeholder => $url) {
198 $freshText = str_replace($placeholder, $url, $freshText);
199 }
200
201 $freshText = preg_replace( '/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $freshText );
202 return $freshText;
203 }
204
205 function get_post_content( $postId ) {
206 $post = get_post( $postId );
207 if ( !$post ) {
208 return false;
209 }
210 $text = apply_filters( 'mwai_pre_post_content', $post->post_content, $postId );
211 $pattern = '/\[mwai_.*?\]/';
212 $text = preg_replace( $pattern, '', $text );
213 if ( $this->get_option( 'resolve_shortcodes' ) ) {
214 $text = apply_filters( 'the_content', $text );
215 }
216 else {
217 $pattern = "/\[[^\]]+\]/";
218 $text = preg_replace( $pattern, '', $text );
219 $pattern = "/<!--\s*\/?wp:[^\>]+-->/";
220 $text = preg_replace( $pattern, '', $text );
221 }
222 $text = $this->clean_text( $text );
223 $text = $this->clean_sentences( $text );
224 $text = apply_filters( 'mwai_post_content', $text, $postId );
225 return $text;
226 }
227
228 function markdown_to_html( $content ) {
229 $Parsedown = new Parsedown();
230 $content = $Parsedown->text( $content );
231 return $content;
232 }
233
234 function get_post_language( $postId ) {
235 $locale = get_locale();
236 $code = strtolower( substr( $locale, 0, 2 ) );
237 $humanLanguage = strtr( $code, MWAI_ALL_LANGUAGES );
238 $lang = apply_filters( 'wpml_post_language_details', null, $postId );
239 if ( !empty( $lang ) ) {
240 $locale = $lang['locale'];
241 $humanLanguage = $lang['display_name'];
242 }
243 return strtolower( "$locale ($humanLanguage)" );
244 }
245 #endregion
246
247 #region Image-Related Helpers
248 static function is_image( $file ) {
249 $mimeType = Meow_MWAI_Core::get_mime_type( $file );
250 if ( strpos( $mimeType, 'image' ) !== false ) {
251 return true;
252 }
253 return false;
254 }
255
256 static function get_mime_type( $file ) {
257 $mimeType = null;
258
259 // Let's try to use mime_content_type if the function exists
260 $isUrl = filter_var( $file, FILTER_VALIDATE_URL );
261 if ( !$isUrl && function_exists( 'mime_content_type' ) ) {
262 $mimeType = mime_content_type( $file );
263 }
264
265 // Otherwise, let's check the file extension (which can actually also be an URL)
266 if ( !$mimeType ) {
267 $extension = pathinfo( $file, PATHINFO_EXTENSION );
268 $extension = strtolower( $extension );
269 $mimeTypes = [
270 'jpg' => 'image/jpeg',
271 'jpeg' => 'image/jpeg',
272 'png' => 'image/png',
273 'gif' => 'image/gif',
274 'webp' => 'image/webp',
275 'bmp' => 'image/bmp',
276 'tiff' => 'image/tiff',
277 'tif' => 'image/tiff',
278 'svg' => 'image/svg+xml',
279 'ico' => 'image/x-icon',
280 'pdf' => 'application/pdf',
281 ];
282 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
283 }
284
285 return $mimeType;
286 }
287
288 function download_image( $url ) {
289 $args = array( 'timeout' => 60, );
290 $response = wp_safe_remote_get( $url, $args );
291 if ( is_wp_error( $response ) ) {
292 throw new Exception( $response->get_error_message() );
293 }
294 $output = wp_remote_retrieve_body( $response );
295 if ( is_wp_error( $output ) ) {
296 throw new Exception( $output->get_error_message() );
297 }
298 return $output;
299 }
300
301 /**
302 * Add an image from a URL to the Media Library.
303 * @param string $url The URL of the image to be downloaded.
304 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
305 * @param string $title The title of the image.
306 * @param string $description The description of the image.
307 * @param string $caption The caption of the image.
308 * @param string $alt The alt text of the image.
309 * @return int The attachment ID of the image.
310 */
311 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null ) {
312 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
313 $url_filename = $path_parts['basename'];
314 $file_type = wp_check_filetype( $url_filename, null );
315 $allowed_types = get_allowed_mime_types();
316 if ( !$file_type || !in_array( $file_type['type'], $allowed_types ) ) {
317 throw new Exception( 'Invalid file type from URL.' );
318 }
319
320 // Initial extension from URL file name
321 $extension = $file_type['ext'];
322
323 if ( !empty( $filename ) ) {
324 $custom_file_type = wp_check_filetype( $filename, null );
325 if ( !$custom_file_type || !in_array( $custom_file_type['type'], $allowed_types ) ) {
326 throw new Exception( 'Invalid custom file type.' );
327 }
328 // Use the extension from the custom filename if valid
329 $extension = $custom_file_type['ext'];
330 }
331
332 $image_data = $this->download_image( $url );
333 if ( !$image_data ) {
334 throw new Exception( 'Could not download the image.' );
335 }
336 $upload_dir = wp_upload_dir();
337
338 // Filename handling including 'generated_' prefix scenario
339 if ( empty( $filename ) ) {
340 $filename = sanitize_file_name( $url_filename );
341 if ( empty( $extension ) ) { // This condition might now be redundant
342 $extension = $file_type['ext'];
343 }
344 // Filename length check and prepend if conditions met
345 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
346 $filename = $this->get_random_id( 16 ) . '.' . $extension;
347 }
348 if ( strpos( $filename, '.' ) === false ) {
349 $filename .= '.' . $extension;
350 }
351 }
352
353 // Directory and file path handling
354 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
355 $file = $upload_dir['path'] . '/' . $filename;
356 }
357 else {
358 $file = $upload_dir['basedir'] . '/' . $filename;
359 }
360
361 // Ensure file name uniqueness in the directory
362 $i = 1;
363 $parts = pathinfo( $file );
364 while ( file_exists( $file ) ) {
365 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
366 $i++;
367 }
368
369 // Writing the file to disk
370 file_put_contents( $file, $image_data );
371
372 // Attachment and metadata handling in WP
373 $attachment = [
374 'post_mime_type' => $file_type['type'],
375 'post_title' => $title ?? '',
376 'post_content' => $description ?? '',
377 'post_excerpt' => $caption ?? '',
378 'post_status' => 'inherit'
379 ];
380 $attachmentId = wp_insert_attachment( $attachment, $file );
381 require_once( ABSPATH . 'wp-admin/includes/image.php' );
382 $attachment_data = wp_generate_attachment_metadata( $attachmentId, $file );
383 wp_update_attachment_metadata( $attachmentId, $attachment_data );
384 update_post_meta( $attachmentId, '_wp_attachment_image_alt', $alt );
385
386 return $attachmentId;
387 }
388 #endregion
389
390 #region Context-Related Helpers
391 function retrieve_context( $params, $query ) {
392 $contextMaxLength = $params['contextMaxLength'] ?? $this->get_option( 'context_max_length', 4096 );
393 $embeddingsEnvId = $params['embeddingsEnvId'] ?? null;
394 $context = apply_filters( 'mwai_context_search', [], $query, [
395 'embeddingsEnvId' => $embeddingsEnvId
396 ]);
397 if ( empty( $context ) ) {
398 return null;
399 }
400 else if ( !isset( $context['content'] ) ) {
401 $this->log( "⚠️ A context without content was returned." );
402 return null;
403 }
404 $context['content'] = $this->clean_sentences( $context['content'], $contextMaxLength );
405 $context['length'] = strlen( $context['content'] );
406 return $context;
407 }
408 #endregion
409
410 #region Users/Sessions Helpers
411
412 function get_nonce() {
413 // if ( !is_user_logged_in() ) {
414 // return null;
415 // }
416 if ( isset( $this->nonce ) ) {
417 return $this->nonce;
418 }
419 $this->nonce = wp_create_nonce( 'wp_rest' );
420 return $this->nonce;
421 }
422
423 function get_session_id() {
424 if ( isset( $_COOKIE['mwai_session_id'] ) ) {
425 return $_COOKIE['mwai_session_id'];
426 }
427 return "N/A";
428 }
429
430 // Get the UserID from the data, or from the current user
431 function get_user_id( $data = null ) {
432 // TODO: Not sure if that's the best way, but we should probably use an admin user as a fallback for CRON.
433 if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
434 $admin = get_users( [ 'role' => 'administrator' ] );
435 if ( !empty( $admin ) ) {
436 return $admin[0]->ID;
437 }
438 }
439 if ( isset( $data ) && isset( $data['userId'] ) ) {
440 return (int)$data['userId'];
441 }
442 if ( is_user_logged_in() ) {
443 $current_user = wp_get_current_user();
444 if ( $current_user->ID > 0 ) {
445 return $current_user->ID;
446 }
447 }
448 return null;
449 }
450
451 function get_admin_user() {
452 $admin = get_users( [ 'role' => 'administrator' ] );
453 if ( !empty( $admin ) ) {
454 return $admin[0];
455 }
456 return null;
457 }
458
459 function get_user_data() {
460 $user = wp_get_current_user();
461 if ( empty( $user ) || empty( $user->ID ) ) {
462 return null;
463 }
464 $placeholders = array(
465 'FIRST_NAME' => get_user_meta( $user->ID, 'first_name', true ),
466 'LAST_NAME' => get_user_meta( $user->ID, 'last_name', true ),
467 'USER_LOGIN' => isset( $user ) && isset($user->data) && isset( $user->data->user_login ) ?
468 $user->data->user_login : null,
469 'DISPLAY_NAME' => isset( $user ) && isset( $user->data ) && isset( $user->data->display_name ) ?
470 $user->data->display_name : null,
471 'AVATAR_URL' => get_avatar_url( get_current_user_id() ),
472 );
473 return $placeholders;
474 }
475
476 function get_ip_address( $params = null ) {
477 $ip = '127.0.0.1';
478 $headers = [
479 'HTTP_TRUE_CLIENT_IP',
480 'HTTP_CF_CONNECTING_IP',
481 'HTTP_X_REAL_IP',
482 'HTTP_CLIENT_IP',
483 'HTTP_X_FORWARDED_FOR',
484 'HTTP_X_FORWARDED',
485 'HTTP_X_CLUSTER_CLIENT_IP',
486 'HTTP_FORWARDED_FOR',
487 'HTTP_FORWARDED',
488 'REMOTE_ADDR',
489 ];
490
491 if ( isset( $params ) && isset( $params[ 'ip' ] ) ) {
492 $ip = ( string )$params[ 'ip' ];
493 } else {
494 foreach ( $headers as $header ) {
495 if ( array_key_exists( $header, $_SERVER ) && !empty( $_SERVER[ $header ] && $_SERVER[ $header ] != '::1' ) ) {
496 $address_chain = explode( ',', wp_unslash( $_SERVER [ $header ] ) );
497 $ip = filter_var( trim( $address_chain[ 0 ] ), FILTER_VALIDATE_IP );
498 break;
499 }
500 }
501 }
502
503 return filter_var( apply_filters( 'mwai_get_ip_address', $ip ), FILTER_VALIDATE_IP );
504 }
505
506 #endregion
507
508 #region Other Helpers
509
510 public function check_rest_nonce( $request ) {
511 $nonce = $request->get_header( 'X-WP-Nonce' );
512 $rest_nonce = wp_verify_nonce( $nonce, 'wp_rest' );
513 return apply_filters( 'mwai_rest_authorized', $rest_nonce, $request );
514 }
515
516 function get_random_id( $length = 8, $excludeIds = [] ) {
517 $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
518 $charactersLength = strlen( $characters );
519 $randomId = '';
520 for ( $i = 0; $i < $length; $i++ ) {
521 $randomId .= $characters[rand( 0, $charactersLength - 1 )];
522 }
523 if ( in_array( $randomId, $excludeIds ) ) {
524 return $this->get_random_id( $length, $excludeIds );
525 }
526 return $randomId;
527 }
528
529 function is_url( $url ) {
530 return strpos( $url, 'http' ) === 0 ? true : false;
531 }
532
533 function get_post_types() {
534 $excluded = array( 'attachment', 'revision', 'nav_menu_item' );
535 $post_types = array();
536 $types = get_post_types( [], 'objects' );
537
538 // Let's get the Post Types that are enabled for Embeddings Sync
539 $embeddingsSettings = $this->get_option( 'embeddings' );
540 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
541
542 foreach ( $types as $type ) {
543 $forced = in_array( $type->name, $syncPostTypes );
544 // Should not be excluded.
545 if ( !$forced && in_array( $type->name, $excluded ) ) {
546 continue;
547 }
548 // Should be public.
549 if ( !$forced && !$type->public ) {
550 continue;
551 }
552 $post_types[] = array(
553 'name' => $type->labels->name,
554 'type' => $type->name,
555 );
556 }
557
558 // Let's get the Post Types that are enabled for Embeddings Sync
559 $embeddingsSettings = $this->get_option( 'embeddings' );
560 $syncPostTypes = isset( $embeddingsSettings['syncPostTypes'] ) ? $embeddingsSettings['syncPostTypes'] : [];
561
562 return $post_types;
563 }
564
565 function get_post( $post ) {
566 if ( is_numeric( $post ) ) {
567 $post = get_post( $post );
568 }
569 if ( is_object( $post ) ) {
570 $post = (array)$post;
571 }
572 if ( !is_array( $post ) ) {
573 return null;
574 }
575 $language = $this->get_post_language( $post['ID'] );
576 $content = $this->get_post_content( $post['ID'] );
577 $title = $post['post_title'];
578 $excerpt = $post['post_excerpt'];
579 $url = get_permalink( $post['ID'] );
580 $checksum = wp_hash( $content . $title . $url );
581 return [
582 'postId' => (int)$post['ID'],
583 'title' => $title,
584 'content' => $content,
585 'excerpt' => $excerpt,
586 'url' => $url,
587 'language' => $language ?? 'english',
588 'checksum' => $checksum,
589 ];
590 }
591 #endregion
592
593 #region Usage & Costs
594
595 // Quick and dirty token estimation
596 // Let's keep this synchronized with Helpers in JS
597 static function estimate_tokens( ...$args ): int {
598 $text = "";
599 foreach ( $args as $arg ) {
600 if ( is_array( $arg ) ) {
601 foreach ( $arg as $message ) {
602 $text .= isset( $message['content']['text'] ) ? $message['content']['text'] : "";
603 $text .= isset( $message['content'] ) && is_string( $message['content'] ) ? $message['content'] : "";
604 }
605 }
606 else if ( is_string( $arg ) ) {
607 $text .= $arg;
608 }
609 }
610 $averageTokenLength = 4;
611 $words = preg_split( '/\s+/', trim( $text ) );
612 $tokenCount = 0;
613 foreach ( $words as $word ) {
614 $tokenCount += ceil( strlen( $word ) / $averageTokenLength );
615 }
616 return apply_filters( 'mwai_estimate_tokens', $tokenCount, $text );
617 }
618
619 public function record_tokens_usage( $model, $in_tokens, $out_tokens = 0, $returned_price = null ) {
620 if ( !is_numeric( $in_tokens ) ) {
621 throw new Exception( 'AI Engine: in_tokens must be a number.' );
622 }
623 if ( !is_numeric( $out_tokens ) ) {
624 $out_tokens = 0;
625 }
626 if ( !$model ) {
627 throw new Exception( 'AI Engine: model is required.' );
628 }
629 $usage = $this->get_option( 'openai_usage' );
630 $month = date( 'Y-m' );
631 if ( !isset( $usage[$month] ) ) {
632 $usage[$month] = array();
633 }
634 if ( !isset( $usage[$month][$model] ) ) {
635 $usage[$month][$model] = array( 'prompt_tokens' => 0, 'completion_tokens' => 0, 'total_tokens' => 0 );
636 }
637 $usage[$month][$model]['prompt_tokens'] += $in_tokens;
638 $usage[$month][$model]['completion_tokens'] += $out_tokens;
639 $usage[$month][$model]['total_tokens'] += $in_tokens + $out_tokens;
640 $this->update_option( 'openai_usage', $usage );
641 $usageInfo = [
642 'prompt_tokens' => $in_tokens,
643 'completion_tokens' => $out_tokens,
644 'total_tokens' => $in_tokens + $out_tokens,
645 ];
646 if ( $returned_price !== null ) {
647 $usageInfo['price'] = $returned_price;
648 }
649 return $usageInfo;
650 }
651
652 public function record_audio_usage( $model, $seconds ) {
653 if ( !is_numeric( $seconds ) ) {
654 throw new Exception( 'AI Engine: seconds must be a number.' );
655 }
656 if ( !$model ) {
657 throw new Exception( 'AI Engine: model is required.' );
658 }
659 $usage = $this->get_option( 'openai_usage' );
660 $month = date( 'Y-m' );
661 if ( !isset( $usage[$month] ) ) {
662 $usage[$month] = array();
663 }
664 if ( !isset( $usage[$month][$model] ) ) {
665 $usage[$month][$model] = array( 'seconds' => 0 );
666 }
667 $usage[$month][$model]['seconds'] += $seconds;
668 $this->update_option( 'openai_usage', $usage );
669 return [ 'seconds' => $seconds ];
670 }
671
672 public function record_images_usage( $model, $resolution, $images ) {
673 if ( !$model || !$resolution || !$images ) {
674 throw new Exception( 'Missing parameters for record_image_usage.' );
675 }
676 $usage = $this->get_option( 'openai_usage' );
677 $month = date( 'Y-m' );
678 if ( !isset( $usage[$month] ) ) {
679 $usage[$month] = array();
680 }
681 if ( !isset( $usage[$month][$model] ) ) {
682 $usage[$month][$model] = array( 'resolution' => array(), 'images' => 0 );
683 }
684 if ( !isset( $usage[$month][$model]['resolution'][$resolution] ) ) {
685 $usage[$month][$model]['resolution'][$resolution] = 0;
686 }
687 $usage[$month][$model]['resolution'][$resolution] += $images;
688 $usage[$month][$model]['images'] += $images;
689 $this->update_option( 'openai_usage', $usage );
690 return [ 'resolution' => $resolution, 'images' => $images ];
691 }
692
693 #endregion
694
695 #region Streaming
696 public function stream_push( $data ) {
697 $out = "data: " . json_encode( $data );
698 echo $out;
699 echo "\n\n";
700 if ( ob_get_level() > 0 ) {
701 ob_end_flush();
702 }
703 flush();
704 }
705 #endregion
706
707 #region Options
708 function get_themes() {
709 $themes = get_option( $this->themes_option_name, [] );
710 $themes = empty( $themes ) ? [] : $themes;
711
712 $internalThemes = [
713 'chatgpt' => [
714 'type' => 'internal', 'name' => 'ChatGPT', 'themeId' => 'chatgpt',
715 'settings' => [], 'style' => ""
716 ],
717 'messages' => [
718 'type' => 'internal', 'name' => 'Messages', 'themeId' => 'messages',
719 'settings' => [], 'style' => ""
720 ],
721 ];
722 $customThemes = [];
723 foreach ( $themes as $theme ) {
724 if ( isset( $internalThemes[$theme['themeId']] ) ) {
725 $internalThemes[$theme['themeId']] = $theme;
726 continue;
727 }
728 $customThemes[] = $theme;
729 }
730 return array_merge(array_values($internalThemes), $customThemes);
731 }
732
733 function update_themes( $themes ) {
734 update_option( $this->themes_option_name, $themes );
735 return $themes;
736 }
737
738 function get_chatbots() {
739 $chatbots = get_option( $this->chatbots_option_name, [] );
740 $hasChanges = false;
741 if ( empty( $chatbots ) ) {
742 $chatbots = [ array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] ) ];
743 }
744 $hasDefault = false;
745 foreach ( $chatbots as &$chatbot ) {
746 if ( $chatbot['botId'] === 'default' ) {
747 $hasDefault = true;
748 }
749 foreach ( MWAI_CHATBOT_DEFAULT_PARAMS as $key => $value ) {
750 // Use default value if not set.
751 if ( !isset( $chatbot[$key] ) ) {
752 $chatbot[$key] = $value;
753 }
754 }
755 // TODO: After October 2024, let's remove this.
756 if ( isset( $chatbot['context'] ) ) {
757 $chatbot['instructions'] = $chatbot['context'];
758 unset( $chatbot['context'] );
759 $hasChanges = true;
760 }
761 // TODO: After October 2024, let's remove this.
762 if ( isset( $chatbot['fileUpload'] ) ) {
763 $chatbot['fileSearch'] = $chatbot['fileUpload'];
764 unset( $chatbot['fileUpload'] );
765 $hasChanges = true;
766 }
767 }
768 if ( !$hasDefault ) {
769 $defaultBot = array_merge( MWAI_CHATBOT_DEFAULT_PARAMS, ['name' => 'Default', 'botId' => 'default' ] );
770 array_unshift( $chatbots, $defaultBot );
771 $hasChanges = true;
772 }
773 if ( $hasChanges ) {
774 update_option( $this->chatbots_option_name, $chatbots );
775 }
776 return $chatbots;
777 }
778
779 function get_chatbot( $botId ) {
780 $chatbots = $this->get_chatbots();
781 foreach ( $chatbots as $chatbot ) {
782 if ( $chatbot['botId'] === (string)$botId ) {
783 return $chatbot;
784 }
785 }
786 return null;
787 }
788
789 function get_embeddings_env( $envId ) {
790 $envs = $this->get_option( 'embeddings_envs' );
791 foreach ( $envs as $env ) {
792 if ( $env['id'] === $envId ) {
793 return $env;
794 }
795 }
796 return null;
797 }
798
799 function get_ai_env( $envId ) {
800 $envs = $this->get_option( 'ai_envs' );
801 foreach ( $envs as $env ) {
802 if ( $env['id'] === $envId ) {
803 return $env;
804 }
805 }
806 return null;
807 }
808
809 function get_assistant( $envId, $assistantId ) {
810 $env = $this->get_ai_env( $envId );
811 if ( !$env ) {
812 return null;
813 }
814 $assistants = $env['assistants'];
815 foreach ( $assistants as $assistant ) {
816 if ( $assistant['id'] === $assistantId ) {
817 return $assistant;
818 }
819 }
820 return null;
821 }
822
823 function get_theme( $themeId ) {
824 $themes = $this->get_themes();
825 foreach ( $themes as $theme ) {
826 if ( $theme['themeId'] === $themeId ) {
827 return $theme;
828 }
829 }
830 return null;
831 }
832
833 function update_chatbots( $chatbots ) {
834 $deprecatedFields = [ 'env', 'embeddingsIndex', 'embeddingsNamespace', 'service' ];
835 $htmlFields = [ 'textCompliance', 'aiName', 'userName', 'startSentence' ];
836 $keepLineReturnsFields = [ 'instructions' ];
837 $whiteSpacedFields = [ 'context' ];
838 foreach ( $chatbots as &$chatbot ) {
839 foreach ( $chatbot as $key => &$value ) {
840 if ( in_array( $key, $deprecatedFields ) ) {
841 unset( $chatbot[$key] );
842 continue;
843 }
844 if ( in_array( $key, $htmlFields ) ) {
845 $value = wp_kses_post( $value );
846 }
847 else if ( in_array( $key, $whiteSpacedFields ) ) {
848 $value = sanitize_textarea_field( $value );
849 }
850 else if ( $key === 'functions' ) {
851 $functions = [];
852 foreach ( $value as $function ) {
853 if ( isset( $function['id'] ) && isset( $function['type'] ) ) {
854 $functions[] = [
855 'id' => sanitize_text_field( $function['id'] ),
856 'type' => sanitize_text_field( $function['type'] ),
857 ];
858 }
859 }
860 $value = $functions;
861 }
862 else {
863 if ( in_array( $key, $keepLineReturnsFields ) ) {
864 $value = preg_replace( '/\r\n/', "[==LINE_RETURN==]", $value );
865 $value = preg_replace( '/\n/', "[==LINE_RETURN==]", $value );
866 }
867 $value = sanitize_text_field( $value );
868 if ( in_array( $key, $keepLineReturnsFields ) ) {
869 $value = preg_replace( '/\[==LINE_RETURN==\]/', "\n", $value );
870 }
871 }
872 }
873 }
874 if ( !update_option( $this->chatbots_option_name, $chatbots ) ) {
875 $this->log( '⚠️ Could not update chatbots.' );
876 $chatbots = get_option( $this->chatbots_option_name, [] );
877 return $chatbots;
878 }
879 return $chatbots;
880 }
881
882 function get_all_options( $force = false ) {
883 // We could cache options this way, but if we do, the apply_filters seems to be called too early.
884 // That causes issues with the mwai_languages filter.
885 // if ( !$force && !is_null( $this->options ) ) {
886 // return $this->options;
887 // }
888 $options = get_option( $this->option_name, [] );
889 $options = $this->sanitize_options( $options );
890 foreach ( MWAI_OPTIONS as $key => $value ) {
891 if ( !isset( $options[$key] ) ) {
892 $options[$key] = $value;
893 }
894 if ( $key === 'languages' ) {
895 // NOTE: If we decide to make a set of options for languages, we can keep it in the settings
896 $options[$key] = apply_filters( 'mwai_languages', MWAI_LANGUAGES );
897 }
898 }
899 $options['chatbot_defaults'] = MWAI_CHATBOT_DEFAULT_PARAMS;
900 $options['default_limits'] = MWAI_LIMITS;
901 $options['openai_models'] = apply_filters(
902 'mwai_openai_models',
903 Meow_MWAI_Engines_OpenAI::get_models_static()
904 );
905 $options['anthropic_models'] = apply_filters(
906 'mwai_anthropic_models',
907 Meow_MWAI_Engines_Anthropic::get_models_static()
908 );
909 $options['fallback_model'] = MWAI_FALLBACK_MODEL;
910
911 // Support for functions from Snippet Vault
912 $options['functions'] = apply_filters( 'mwai_functions_list', [] );
913
914 //$this->options = $options;
915 return $options;
916 }
917
918 // Sanitize options when we update the plugi or perform some updates
919 // if we change the structure of the options.
920 function sanitize_options( $options ) {
921 $needs_update = false;
922
923 // This list was updated on December 11, 2023. After May 2024, let's remove this.
924 $old_options = [
925 'shortcode_chat_default_params',
926 'shortcode_chat_params_override',
927 'module_legacy_finetunes',
928 'shortcode_chat_legacy',
929 'shortcode_chat_inject',
930 'shortcode_chat_styles',
931 'dynamic_max_tokens',
932 'shortcode_chat_formatting',
933 'shortcode_forms_legacy',
934 ];
935 foreach ( $old_options as $old_option ) {
936 if ( isset( $options[$old_option] ) ) {
937 unset( $options[$old_option] );
938 $needs_update = true;
939 }
940 }
941
942 // This upgrades namespace to multi-namespaces (June 2023)
943 // After January 2024, let's remove this.
944 if ( isset( $options['pinecone'] ) && isset( $options['pinecone']['namespace'] ) ) {
945 $options['pinecone']['namespaces'] = [ $options['pinecone']['namespace'] ];
946 unset( $options['pinecone']['namespace'] );
947 $needs_update = true;
948 }
949 // Support for Multi Vector DB Environments
950 // After June 2024, let's remove this.
951 if ( !isset( $options['embeddings_envs'] ) ) {
952 $options['embeddings_envs'] = [];
953 $default_id = $this->get_random_id();
954 $pinecone = isset( $options['pinecone'] ) ? $options['pinecone'] : [];
955 $options['embeddings_envs'][] = [
956 'id' => $default_id,
957 'name' => 'Pinecone',
958 'type' => 'pinecone',
959 'apikey' => isset( $pinecone['apikey'] ) ? $pinecone['apikey'] : '',
960 'server' => isset( $pinecone['server'] ) ? $pinecone['server'] : 'gcp-starter',
961 'indexes' => isset( $pinecone['indexes'] ) ? $pinecone['indexes'] : [],
962 'namespaces' => isset( $pinecone['namespaces'] ) ? $pinecone['namespaces'] : [],
963 'index' => isset( $pinecone['index'] ) ? $pinecone['index'] : null,
964 ];
965 $options['embeddings_default_env'] = $default_id;
966 $needs_update = true;
967 }
968 if ( isset( $options['pinecone'] ) ) {
969 unset( $options['pinecone'] );
970 $needs_update = true;
971 }
972 // Support for Multi AI Environments
973 // After June 2024, let's remove this.
974 if ( !isset( $options['ai_envs'] ) ) {
975 $options['ai_envs'] = [];
976 $default_openai_id = $this->get_random_id();
977 $default_azure_id = $this->get_random_id();
978 $openai_service = isset( $options['openai_service'] ) ? $options['openai_service'] : 'openai';
979 $openai_apikey = isset( $options['openai_apikey'] ) ? $options['openai_apikey'] : '';
980 $azure_endpoint = isset( $options['openai_azure_endpoint'] ) ? $options['openai_azure_endpoint'] : '';
981
982 // OpenAI
983 // We create a default OpenAI environment if the API Key is set, or if the Azure Endpoint is not set.
984 if ( !empty( $openai_apikey ) || empty( $azure_endpoint ) ) {
985 $openai_finetunes = isset( $options['openai_finetunes'] ) ? $options['openai_finetunes'] : [];
986 $openai_finetunes_deleted = isset( $options['openai_finetunes_deleted'] ) ?
987 $options['openai_finetunes_deleted'] : [];
988 $openai_legacy_finetunes = isset( $options['openai_legacy_finetunes'] ) ?
989 $options['openai_legacy_finetunes'] : [];
990 $openai_legacy_finetunes_deleted = isset( $options['openai_legacy_finetunes_deleted'] ) ?
991 $options['openai_legacy_finetunes_deleted'] : [];
992 $options['ai_envs'][] = [
993 'id' => $default_openai_id,
994 'name' => 'OpenAI',
995 'type' => 'openai',
996 'apikey' => $openai_apikey,
997 'finetunes' => $openai_finetunes,
998 'finetunes_deleted' => $openai_finetunes_deleted,
999 'legacy_finetunes' => $openai_legacy_finetunes,
1000 'legacy_finetunes_deleted' => $openai_legacy_finetunes_deleted
1001 ];
1002 }
1003
1004 // Azure
1005 if ( !empty( $azure_endpoint ) ) {
1006 $azure_apikey = isset( $options['openai_azure_apikey'] ) ? $options['openai_azure_apikey'] : '';
1007 $azure_deployments = isset( $options['openai_azure_deployments'] ) ? $options['openai_azure_deployments'] : [];
1008 $options['ai_envs'][] = [
1009 'id' => $default_azure_id,
1010 'name' => 'Azure',
1011 'type' => 'azure',
1012 'apikey' => $azure_apikey,
1013 'endpoint' => $azure_endpoint,
1014 'deployments' => $azure_deployments,
1015 ];
1016 }
1017
1018 $options['ai_default_env'] = $default_openai_id;
1019 if ( $openai_service === 'azure' ) {
1020 $options['ai_default_env'] = $default_azure_id;
1021 }
1022 $needs_update = true;
1023 }
1024 if ( !empty( $options['openai_apikey'] ) || !empty( $options['openai_azure_apikey'] ) ) {
1025 unset( $options['openai_apikey'] );
1026 unset( $options['openai_finetunes'] );
1027 unset( $options['openai_finetunes_deleted'] );
1028 unset( $options['openai_legacy_finetunes'] );
1029 unset( $options['openai_legacy_finetunes_deleted'] );
1030 unset( $options['openai_azure_apikey'] );
1031 unset( $options['openai_azure_endpoint'] );
1032 unset( $options['openai_azure_deployments'] );
1033 unset( $options['openai_service'] );
1034 $needs_update = true;
1035 }
1036
1037 // The IDs for the embeddings environments are generated here.
1038 // TODO: We should handle this more gracefully via an option in the Embeddings Settings.
1039 $embeddings_default_exists = false;
1040 if ( isset( $options['embeddings_envs'] ) ) {
1041 foreach ( $options['embeddings_envs'] as &$env ) {
1042 if ( !isset( $env['id'] ) ) {
1043 $env['id'] = $this->get_random_id();
1044 $needs_update = true;
1045 }
1046 if ( $env['id'] === $options['embeddings_default_env'] ) {
1047 $embeddings_default_exists = true;
1048 }
1049 }
1050 }
1051 if ( !$embeddings_default_exists ) {
1052 $options['embeddings_default_env'] = $options['embeddings_envs'][0]['id'] ?? null;
1053 $needs_update = true;
1054 }
1055
1056 // The IDs for the AI environments are generated here.
1057 $ai_default_exists = false;
1058 if ( isset( $options['ai_envs'] ) ) {
1059 foreach ( $options['ai_envs'] as &$env ) {
1060 if ( !isset( $env['id'] ) ) {
1061 $env['id'] = $this->get_random_id();
1062 $needs_update = true;
1063 }
1064 if ( $env['id'] === $options['ai_default_env'] ) {
1065 $ai_default_exists = true;
1066 }
1067 }
1068 }
1069 if ( !$ai_default_exists ) {
1070 $options['ai_default_env'] = $options['ai_envs'][0]['id'] ?? null;
1071 $needs_update = true;
1072 }
1073
1074 if ( $needs_update ) {
1075 update_option( $this->option_name, $options, false );
1076 }
1077
1078 return $options;
1079 }
1080
1081 function update_options( $options ) {
1082 if ( !update_option( $this->option_name, $options, false ) ) {
1083 return false;
1084 }
1085 $options = $this->get_all_options( true );
1086 return $options;
1087 }
1088
1089 function update_option( $option, $value ) {
1090 $options = $this->get_all_options( true );
1091 $options[$option] = $value;
1092 return $this->update_options( $options );
1093 }
1094
1095 function get_option( $option, $default = null ) {
1096 $options = $this->get_all_options();
1097 return $options[$option] ?? $default;
1098 }
1099
1100 function update_ai_env( $env_id, $option, $value ) {
1101 $options = $this->get_all_options( true );
1102 foreach ( $options['ai_envs'] as &$env ) {
1103 if ( $env['id'] === $env_id ) {
1104 $env[$option] = $value;
1105 break;
1106 }
1107 }
1108 return $this->update_options( $options );
1109 }
1110
1111 function reset_options() {
1112 delete_option( $this->themes_option_name );
1113 delete_option( $this->chatbots_option_name );
1114 delete_option( $this->option_name );
1115 return $this->get_all_options( true );
1116 }
1117 #endregion
1118
1119 #region Logs
1120
1121 function get_logs() {
1122 $log_file_path = $this->get_logs_path();
1123
1124 if ( !file_exists( $log_file_path ) ) {
1125 return "Empty log file.";
1126 }
1127
1128 $content = file_get_contents( $log_file_path );
1129 $lines = explode( "\n", $content );
1130 $lines = array_filter( $lines );
1131 $lines = array_reverse( $lines );
1132 $content = implode( "\n", $lines );
1133 return $content;
1134 }
1135
1136 function clear_logs() {
1137 $logPath = $this->get_logs_path();
1138 if ( file_exists( $logPath ) ) {
1139 unlink( $logPath );
1140 }
1141
1142 $options = $this->get_all_options();
1143 $options['logs_path'] = null;
1144 $this->update_options( $options );
1145 }
1146
1147 function get_logs_path() {
1148 $uploads_dir = wp_upload_dir();
1149 $uploads_dir_path = trailingslashit( $uploads_dir['basedir'] );
1150
1151 $path = $this->get_option( 'logs_path' );
1152
1153 if ( $path && file_exists( $path ) ) {
1154 // make sure the path is legal (within the uploads directory with the MWAI_PREFIX and log extension)
1155 if ( strpos( $path, $uploads_dir_path ) !== 0 || strpos( $path, MWAI_PREFIX ) === false || substr( $path, -4 ) !== '.log' ) {
1156 $path = null;
1157 } else {
1158 return $path;
1159 }
1160 }
1161
1162 if ( !$path ) {
1163 $path = $uploads_dir_path . MWAI_PREFIX . "_" . $this->random_ascii_chars() . ".log";
1164 if ( !file_exists( $path ) ) {
1165 touch( $path );
1166 }
1167 $options = $this->get_all_options();
1168 $options['logs_path'] = $path;
1169 $this->update_options( $options );
1170 }
1171
1172 return $path;
1173 }
1174
1175 function log( $data = null ) {
1176 if ( !$this->get_option( 'server_debug_mode', false ) ) { return false; }
1177 $log_file_path = $this->get_logs_path();
1178 $fh = @fopen( $log_file_path, 'a' );
1179 if ( !$fh ) { return false; }
1180 $date = date( "Y-m-d H:i:s" );
1181 if ( is_null( $data ) ) {
1182 fwrite( $fh, "\n" );
1183 }
1184 else {
1185 fwrite( $fh, "$date: {$data}\n" );
1186 //error_log( "[MWAI] $data" );
1187 }
1188 fclose( $fh );
1189 return true;
1190 }
1191
1192 private function random_ascii_chars( $length = 8 ) {
1193 $characters = array_merge( range( 'A', 'Z' ), range( 'a', 'z' ), range( '0', '9' ) );
1194 $characters_length = count( $characters );
1195 $random_string = '';
1196
1197 for ( $i = 0; $i < $length; $i++ ) {
1198 $random_string .= $characters[rand(0, $characters_length - 1)];
1199 }
1200
1201 return $random_string;
1202 }
1203
1204 #endregion
1205 }
1206
1207 ?>