PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.5.5
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.5.5
3.5.9 3.5.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 / services / image.php
ai-engine / classes / services Last commit date
image.php 2 weeks ago message-builder.php 3 weeks ago model-environment.php 7 months ago response-id-manager.php 3 months ago session.php 11 months ago usage-stats.php 1 month ago
image.php
261 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 $filename = sanitize_file_name( basename( $filename ) );
182 }
183
184 $image_data = $this->download_image( $url );
185 if ( !$image_data ) {
186 throw new Exception( 'Could not download the image.' );
187 }
188 $upload_dir = wp_upload_dir();
189
190 // Filename handling including 'generated_' prefix scenario
191 if ( empty( $filename ) ) {
192 $filename = sanitize_file_name( $url_filename );
193 if ( empty( $extension ) ) { // This condition might now be redundant
194 $extension = $file_type['ext'];
195 }
196 // Filename length check and prepend if conditions met
197 if ( strlen( $filename ) > 32 || strlen( $filename ) < 4 || strpos( $filename, 'generated_' ) === 0 ) {
198 $filename = uniqid( 'ai_', true ) . '.' . $extension;
199 }
200 if ( strpos( $filename, '.' ) === false ) {
201 $filename .= '.' . $extension;
202 }
203 }
204
205 // Directory and file path handling
206 if ( wp_mkdir_p( $upload_dir['path'] ) ) {
207 $file = $upload_dir['path'] . '/' . $filename;
208 }
209 else {
210 $file = $upload_dir['basedir'] . '/' . $filename;
211 }
212
213 // Ensure file name uniqueness in the directory
214 $i = 1;
215 $parts = pathinfo( $file );
216 while ( file_exists( $file ) ) {
217 $file = $parts['dirname'] . '/' . $parts['filename'] . '-' . $i . '.' . $parts['extension'];
218 $i++;
219 }
220
221 // Write file to filesystem
222 file_put_contents( $file, $image_data );
223
224 // Prepare and insert attachment
225 $wp_filetype = wp_check_filetype( basename( $file ), null );
226 $attachment = [
227 'post_mime_type' => $wp_filetype['type'],
228 'post_title' => !is_null( $title ) ? $title : preg_replace( '/\.[^.]+$/', '', basename( $file ) ),
229 'post_content' => !is_null( $description ) ? $description : '',
230 'post_status' => $post_status,
231 'post_excerpt' => !is_null( $caption ) ? $caption : '',
232 'post_type' => $post_type,
233 ];
234
235 // Use wp_insert_post instead of wp_insert_attachment to allow custom post types
236 $attach_id = wp_insert_post( $attachment );
237
238 // Set the attached file manually since we're not using wp_insert_attachment
239 update_attached_file( $attach_id, $file );
240 require_once( ABSPATH . 'wp-admin/includes/image.php' );
241 $attach_data = wp_generate_attachment_metadata( $attach_id, $file );
242 wp_update_attachment_metadata( $attach_id, $attach_data );
243 if ( !is_null( $alt ) ) {
244 update_post_meta( $attach_id, '_wp_attachment_image_alt', $alt );
245 }
246
247 // Store AI-related metadata
248 if ( !empty( $ai_metadata['model'] ) ) {
249 update_post_meta( $attach_id, 'mwai_model', sanitize_text_field( $ai_metadata['model'] ) );
250 }
251 if ( !empty( $ai_metadata['latency'] ) ) {
252 update_post_meta( $attach_id, 'mwai_latency', floatval( $ai_metadata['latency'] ) );
253 }
254 if ( !empty( $ai_metadata['env_id'] ) ) {
255 update_post_meta( $attach_id, 'mwai_env_id', sanitize_text_field( $ai_metadata['env_id'] ) );
256 }
257
258 return $attach_id;
259 }
260 }
261