PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.5.7
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.5.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 / services / image.php
ai-engine / classes / services Last commit date
image.php 1 day ago message-builder.php 3 weeks ago model-environment.php 7 months ago response-id-manager.php 1 day ago session.php 11 months ago usage-stats.php 1 month ago
image.php
263 lines
1 <?php
2
3 class Meow_MWAI_Services_Image {
4 private $core;
5
6 public function __construct( $core ) {
7 $this->core = $core;
8 }
9
10 public function is_image( $mimeType ) {
11 return strpos( $mimeType, 'image/' ) === 0;
12 }
13
14 public function get_image_resolution( $imageData ) {
15 try {
16 $tempFile = tmpfile();
17 $tempFilePath = stream_get_meta_data( $tempFile )['uri'];
18 fwrite( $tempFile, $imageData );
19 $imageSize = getimagesize( $tempFilePath );
20 fclose( $tempFile );
21 if ( $imageSize !== false ) {
22 return $imageSize[0] . 'x' . $imageSize[1];
23 }
24 }
25 catch ( Exception $e ) {
26 throw new Exception( 'Failed to get image resolution.' );
27 }
28 return null;
29 }
30
31 public function get_mime_type( $file, $fileData = null ) {
32 $mimeType = null;
33
34 // If we have file data, let's use it
35 if ( !empty( $fileData ) ) {
36 $f = finfo_open();
37 $mimeType = finfo_buffer( $f, $fileData, FILEINFO_MIME_TYPE );
38 }
39
40 // Try to use mime_content_type for local files
41 if ( !$mimeType ) {
42 $isUrl = filter_var( $file, FILTER_VALIDATE_URL );
43 if ( !$isUrl && function_exists( 'mime_content_type' ) ) {
44 try {
45 // Sanitize file path to prevent PHAR deserialization attacks
46 $sanitized_file = Meow_MWAI_Core::sanitize_file_path( $file );
47 if ( file_exists( $sanitized_file ) ) {
48 $mimeType = mime_content_type( $sanitized_file );
49 }
50 }
51 catch ( Exception $e ) {
52 // If sanitization fails, fall through to extension-based detection
53 Meow_MWAI_Logging::warn( 'File path sanitization failed: ' . $e->getMessage() );
54 }
55 }
56 }
57
58 // Otherwise, let's check the file extension (which can actually also be an URL)
59 if ( !$mimeType ) {
60 $extension = pathinfo( $file, PATHINFO_EXTENSION );
61 $extension = strtolower( $extension );
62 $mimeTypes = [
63 'jpg' => 'image/jpeg',
64 'jpeg' => 'image/jpeg',
65 'png' => 'image/png',
66 'gif' => 'image/gif',
67 'webp' => 'image/webp',
68 'bmp' => 'image/bmp',
69 'tiff' => 'image/tiff',
70 'tif' => 'image/tiff',
71 'svg' => 'image/svg+xml',
72 'ico' => 'image/x-icon',
73 'pdf' => 'application/pdf',
74 ];
75 $mimeType = isset( $mimeTypes[$extension] ) ? $mimeTypes[$extension] : null;
76 }
77
78 return $mimeType;
79 }
80
81 public function download_image( $url ) {
82 // Handle data URLs (base64-encoded images from Google Gemini, etc.)
83 if ( strpos( $url, 'data:' ) === 0 ) {
84 // Extract base64 data from data URL
85 // Format: data:image/png;base64,iVBORw0KGgoAAAANS...
86 $parts = explode( ',', $url, 2 );
87 if ( count( $parts ) !== 2 ) {
88 throw new Exception( 'Invalid data URL format.' );
89 }
90
91 // Validate it's an image data URL
92 if ( stripos( $parts[0], 'image/' ) === false ) {
93 throw new Exception( 'Data URL is not an image.' );
94 }
95
96 // Decode base64 data
97 $image_data = base64_decode( $parts[1] );
98 if ( $image_data === false ) {
99 throw new Exception( 'Failed to decode base64 image data.' );
100 }
101
102 return $image_data;
103 }
104
105 // Validate URL scheme (only allow http/https)
106 $parsed_url = parse_url( $url );
107 if ( empty( $parsed_url['scheme'] ) || !in_array( $parsed_url['scheme'], [ 'http', 'https' ] ) ) {
108 throw new Exception( 'Invalid URL scheme. Only HTTP and HTTPS are allowed.' );
109 }
110
111 // Check against banned IPs to prevent SSRF attacks
112 $host = $parsed_url['host'] ?? '';
113 if ( empty( $host ) ) {
114 throw new Exception( 'Invalid URL: no host specified.' );
115 }
116
117 // Resolve hostname to IP
118 $ip = gethostbyname( $host );
119
120 // Check if the resolved IP is in the banned IPs list
121 $banned_ips = $this->core->get_option( 'banned_ips' );
122 if ( !empty( $banned_ips ) && !empty( $this->core->security ) ) {
123 if ( $this->core->security->is_blocked_ip( $ip, $banned_ips ) ) {
124 throw new Exception( 'Access to this IP address is not allowed.' );
125 }
126 }
127
128 // Disable redirects to prevent bypass attacks
129 $response = wp_safe_remote_get( $url, [
130 'timeout' => 60,
131 'redirection' => 0 // Prevent redirect-based bypass
132 ] );
133
134 if ( is_wp_error( $response ) ) {
135 throw new Exception( $response->get_error_message() );
136 }
137
138 // Validate response is actually an image
139 $content_type = wp_remote_retrieve_header( $response, 'content-type' );
140 if ( empty( $content_type ) || stripos( $content_type, 'image/' ) !== 0 ) {
141 throw new Exception( 'URL did not return an image. Content-Type: ' . $content_type );
142 }
143
144 return wp_remote_retrieve_body( $response );
145 }
146
147 /**
148 * Add an image from a URL to the Media Library.
149 * @param string $url The URL of the image to be downloaded.
150 * @param string $filename The filename of the image, if not set, it will be the basename of the URL.
151 * @param string $title The title of the image.
152 * @param string $description The description of the image.
153 * @param string $caption The caption of the image.
154 * @param string $alt The alt text of the image.
155 * @param array $ai_metadata AI-related metadata (model, latency, env_id).
156 * @return int The attachment ID of the image.
157 */
158 public function add_image_from_url( $url, $filename = null, $title = null, $description = null, $caption = null, $alt = null, $attachedPost = null, $post_status = 'inherit', $post_type = 'attachment', $ai_metadata = [] ) {
159 $path_parts = pathinfo( parse_url( $url, PHP_URL_PATH ) );
160 $url_filename = $path_parts['basename'];
161 $file_type = wp_check_filetype( $url_filename, null );
162 $allowed_types = get_allowed_mime_types();
163
164 // For URLs without file extensions (like Google Gemini), default to PNG
165 $extension = 'png';
166 if ( $file_type && $file_type['ext'] && in_array( $file_type['type'], $allowed_types ) ) {
167 $extension = $file_type['ext'];
168 }
169
170 if ( !empty( $filename ) ) {
171 $custom_file_type = wp_check_filetype( $filename, null );
172 if ( !$custom_file_type || !in_array( $custom_file_type['type'], $allowed_types ) ) {
173 throw new Exception( 'Invalid custom file type.' );
174 }
175 // Use the extension from the custom filename if valid
176 $extension = $custom_file_type['ext'];
177 // Strip any directory components so a crafted filename (e.g.
178 // "../../../evil.png") cannot escape the uploads directory. The
179 // empty-filename branch below already sanitizes; this mirrors the
180 // hardening applied to the media-rename handler (CVE-2026-1400).
181 // Path traversal reported by Meher Sudhakar Abbireddi (via WPScan/Automattic),
182 // fixed in 3.5.5.
183 $filename = sanitize_file_name( basename( $filename ) );
184 }
185
186 $image_data = $this->download_image( $url );
187 if ( !$image_data ) {
188 throw new Exception( 'Could not download the image.' );
189 }
190 $upload_dir = wp_upload_dir();
191
192 // Filename handling including 'generated_' prefix scenario
193 if ( empty( $filename ) ) {
194 $filename = sanitize_file_name( $url_filename );
195 if ( empty( $extension ) ) { // This condition might now be redundant
196 $extension = $file_type['ext'];
197 }
198 // Filename length check and prepend if conditions met
199 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
200 $filename = uniqid( 'ai_', true ) . '.' . $extension;
201 }
202 if ( strpos( $filename, '.' ) === false ) {
203 $filename .= '.' . $extension;
204 }
205 }
206
207 // Directory and file path handling
208 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
209 $file = $upload_dir['path'] . '/' . $filename;
210 }
211 else {
212 $file = $upload_dir['basedir'] . '/' . $filename;
213 }
214
215 // Ensure file name uniqueness in the directory
216 $i = 1;
217 $parts = pathinfo( $file );
218 while ( file_exists( $file ) ) {
219 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
220 $i++;
221 }
222
223 // Write file to filesystem
224 file_put_contents( $file, $image_data );
225
226 // Prepare and insert attachment
227 $wp_filetype = wp_check_filetype( basename( $file ), null );
228 $attachment = [
229 'post_mime_type' => $wp_filetype['type'],
230 'post_title' => !is_null( $title ) ? $title : preg_replace( '/\.[^.]+$/', '', basename( $file ) ),
231 'post_content' => !is_null( $description ) ? $description : '',
232 'post_status' => $post_status,
233 'post_excerpt' => !is_null( $caption ) ? $caption : '',
234 'post_type' => $post_type,
235 ];
236
237 // Use wp_insert_post instead of wp_insert_attachment to allow custom post types
238 $attach_id = wp_insert_post( $attachment );
239
240 // Set the attached file manually since we're not using wp_insert_attachment
241 update_attached_file( $attach_id, $file );
242 require_once( ABSPATH . 'wp-admin/includes/image.php' );
243 $attach_data = wp_generate_attachment_metadata( $attach_id, $file );
244 wp_update_attachment_metadata( $attach_id, $attach_data );
245 if ( !is_null( $alt ) ) {
246 update_post_meta( $attach_id, '_wp_attachment_image_alt', $alt );
247 }
248
249 // Store AI-related metadata
250 if ( !empty( $ai_metadata['model'] ) ) {
251 update_post_meta( $attach_id, 'mwai_model', sanitize_text_field( $ai_metadata['model'] ) );
252 }
253 if ( !empty( $ai_metadata['latency'] ) ) {
254 update_post_meta( $attach_id, 'mwai_latency', floatval( $ai_metadata['latency'] ) );
255 }
256 if ( !empty( $ai_metadata['env_id'] ) ) {
257 update_post_meta( $attach_id, 'mwai_env_id', sanitize_text_field( $ai_metadata['env_id'] ) );
258 }
259
260 return $attach_id;
261 }
262 }
263