PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / trunk
AI Engine – The Chatbot, AI Framework & MCP for WordPress vtrunk
3.7.3 3.7.2 3.7.1 3.7.0 3.6.9 3.6.8 3.6.7 3.6.6 3.6.4 3.6.5 3.6.3 3.6.2 3.6.1 3.6.0 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 / query / dropped-file.php
ai-engine / classes / query Last commit date
assist-feedback.php 1 year ago assistant.php 4 months ago base.php 2 months ago dropped-file.php 19 hours ago edit-image.php 5 months ago embed.php 5 months ago feedback.php 6 days ago function.php 4 weeks ago image.php 3 months ago parameter.php 1 year ago text.php 2 months ago transcribe.php 10 months ago
dropped-file.php
313 lines
1 <?php
2
3 class Meow_MWAI_Query_DroppedFile {
4 private $data;
5 private $rawData;
6 private $type; // Defines what the data is about ('refId', 'url', or 'data')
7 private $purpose; // 'analysis' or 'generated'
8 private $mimeType; // 'image/jpeg' or any other mime type
9 private $fileId; // The ID of the file in the database
10 public $originalPath; // The original file path (for files loaded from disk)
11
12 /**
13 * Fetch content from a URL, handling internal vs external URLs differently.
14 * Internal URLs (same site) use wp_remote_get to avoid SSRF blocking issues.
15 * External URLs use wp_safe_remote_get for SSRF protection.
16 */
17 private static function fetch_url_content( $url ) {
18 $parts = wp_parse_url( $url );
19 if ( !isset( $parts['scheme'] ) || !in_array( $parts['scheme'], [ 'http', 'https' ], true ) ) {
20 throw new Exception( 'Invalid URL scheme; only HTTP/HTTPS allowed.' );
21 }
22
23 // Check if internal URL by comparing hostnames (handles http/https mismatch)
24 $site_host = wp_parse_url( get_site_url(), PHP_URL_HOST );
25 $url_host = wp_parse_url( $url, PHP_URL_HOST );
26 $is_internal = ( $site_host === $url_host );
27
28 if ( $is_internal ) {
29 $response = wp_remote_get( $url, [ 'timeout' => 60, 'sslverify' => false ] );
30 }
31 else {
32 // SSRF protection for external URLs
33 $response = wp_safe_remote_get( $url, [ 'timeout' => 60, 'redirection' => 0 ] );
34 }
35
36 if ( is_wp_error( $response ) ) {
37 throw new Exception( 'AI Engine: Failed to download file: ' . $response->get_error_message() );
38 }
39
40 $data = wp_remote_retrieve_body( $response );
41 if ( empty( $data ) ) {
42 throw new Exception( 'AI Engine: Failed to download file contents from URL.' );
43 }
44
45 return $data;
46 }
47
48 public static function from_url( $url, $purpose, $mimeType = null, $fileId = null ) {
49 if ( empty( $mimeType ) ) {
50 $mimeType = Meow_MWAI_Core::get_mime_type( $url );
51 }
52 return new Meow_MWAI_Query_DroppedFile( $url, 'url', $purpose, $mimeType, $fileId );
53 }
54
55 public static function from_data( $data, $purpose, $mimeType = null ) {
56 return new Meow_MWAI_Query_DroppedFile( $data, 'data', $purpose, $mimeType );
57 }
58
59 public static function from_path( $path, $purpose, $mimeType = null ) {
60 // Sanitize path to prevent PHAR deserialization attacks
61 $path = Meow_MWAI_Core::sanitize_file_path( $path );
62 $data = file_get_contents( $path );
63 if ( empty( $mimeType ) ) {
64 $mimeType = Meow_MWAI_Core::get_mime_type( $path );
65 }
66 $droppedFile = new Meow_MWAI_Query_DroppedFile( $data, 'data', $purpose, $mimeType );
67 // Store the original path for filename extraction
68 $droppedFile->originalPath = $path;
69 return $droppedFile;
70 }
71
72 public static function from_refId( $refId, $purpose, $mimeType = null ) {
73 return new Meow_MWAI_Query_DroppedFile( $refId, 'refId', $purpose, $mimeType );
74 }
75
76 /**
77 * Create DroppedFile from provider file_id reference (OpenAI, Anthropic, etc.)
78 *
79 * For PDFs uploaded to provider Files APIs, we only store the file_id
80 * Examples: OpenAI 'file-xxx', Anthropic 'file_xxx'
81 * These are reference-only - the file data lives on the provider's servers
82 * Do NOT try to load file data from these - use get_refId() to get the file_id
83 */
84 public static function from_provider_file_id( $fileId, $purpose, $mimeType = null ) {
85 return new Meow_MWAI_Query_DroppedFile( $fileId, 'provider_file_id', $purpose, $mimeType );
86 }
87
88 public function __construct( $data, $type, $purpose, $mimeType = null, $fileId = null ) {
89 if ( !empty( $type ) && $type !== 'refId' && $type !== 'url' && $type !== 'data' && $type !== 'provider_file_id' ) {
90 throw new Exception( 'AI Engine: The file type can only be refId, url, data, or provider_file_id.' );
91 }
92 if ( !empty( $purpose ) && $purpose !== 'analysis' && $purpose !== 'generated' ) {
93 throw new Exception( 'AI Engine: The file purpose can only be analysis or generated.' );
94 }
95 $this->data = $data;
96 $this->type = $type;
97 $this->purpose = $purpose;
98 $this->mimeType = $mimeType;
99 $this->fileId = $fileId;
100 }
101
102 public function get_url() {
103 if ( $this->type === 'url' ) {
104 return $this->data;
105 }
106 throw new Exception( 'AI Engine: The file is not an URL.' );
107 }
108
109 private function get_raw_data() {
110 if ( !empty( $this->rawData ) ) {
111 return $this->rawData;
112 }
113 if ( $this->type === 'provider_file_id' ) {
114 // Provider file IDs are reference-only (file data lives on provider's servers)
115 // Common mistake: trying to load file data for PDFs in conversation history
116 // Fix: Check file mime type before calling get_data()/get_base64()/get_inline_base64_url()
117 // For PDFs: use get_refId() to get the file_id string instead
118 throw new Exception( 'AI Engine: Cannot get raw data for provider file ID (file_id: ' . $this->data . '). Use get_refId() instead.' );
119 }
120 if ( $this->type === 'refId' ) {
121 global $mwai_core;
122
123 // Prefer loading from disk to avoid HTTP rewrites or CDN issues
124 $path = $mwai_core->files->get_path( $this->data );
125 if ( !empty( $path ) ) {
126 $path = Meow_MWAI_Core::sanitize_file_path( $path );
127 if ( file_exists( $path ) && is_readable( $path ) ) {
128 $data = file_get_contents( $path );
129 if ( $data === false ) {
130 throw new Exception( 'AI Engine: Failed to read file contents for refId: ' . $this->data );
131 }
132 $this->rawData = $data;
133 return $this->rawData;
134 }
135 }
136
137 // Fallback to the public URL if the local path is unavailable
138 $url = $mwai_core->files->get_url( $this->data );
139 if ( empty( $url ) ) {
140 throw new Exception( 'AI Engine: Could not find file URL for refId: ' . $this->data );
141 }
142 $this->rawData = self::fetch_url_content( $url );
143 return $this->rawData;
144 }
145 else if ( $this->type === 'url' ) {
146 // For internal URLs, try to read from disk first (more efficient)
147 $site_host = wp_parse_url( get_site_url(), PHP_URL_HOST );
148 $url_host = wp_parse_url( $this->data, PHP_URL_HOST );
149 if ( $site_host === $url_host ) {
150 $upload_dir = wp_upload_dir();
151 // Normalize protocols for comparison (http vs https)
152 $normalized_url = preg_replace( '/^https?:/', '', $this->data );
153 $normalized_upload_url = preg_replace( '/^https?:/', '', $upload_dir['baseurl'] );
154 if ( strpos( $normalized_url, $normalized_upload_url ) === 0 ) {
155 $local_path = str_replace( $normalized_upload_url, $upload_dir['basedir'], $normalized_url );
156 $local_path = Meow_MWAI_Core::sanitize_file_path( $local_path );
157
158 // The mapping above is pure string substitution, so the URL decides the rest of
159 // the path: "/uploads/../../wp-config.php" mapped straight out of the uploads
160 // folder and the bytes were then forwarded to the configured provider under the
161 // real filename. sanitize_file_path() cannot catch this - it only matches stream
162 // wrappers at offset 0, and after the substitution the string always begins with
163 // the uploads basedir, so it can never fire here.
164 //
165 // Resolve the "../" segments and require the result to stay under uploads. A
166 // traversal that stays inside (".../2024/../2023/x.jpg") still resolves fine, so
167 // no legitimate URL is affected. Reject rather than fall through to the HTTP
168 // fetch below, otherwise a blocked traversal is simply retried as a request.
169 $local_path = Meow_MWAI_Core::normalize_path_lexically( $local_path );
170 if ( !Meow_MWAI_Core::is_path_within( $local_path, $upload_dir['basedir'] ) ) {
171 throw new Exception( 'AI Engine: This file cannot be accessed.' );
172 }
173
174 // A missing file is NOT an error here: media offloaded to S3/CDN keeps the local
175 // uploads baseurl while no local copy exists. Those must keep falling through to
176 // the HTTP fetch below, which is why only the containment failure throws.
177 if ( file_exists( $local_path ) && is_readable( $local_path ) ) {
178 $this->rawData = file_get_contents( $local_path );
179 if ( $this->rawData !== false ) {
180 return $this->rawData;
181 }
182 }
183 }
184 }
185
186 // Fetch via HTTP (handles internal vs external URLs with SSRF protection)
187 $this->rawData = self::fetch_url_content( $this->data );
188 return $this->rawData;
189 }
190 else if ( $this->type === 'data' ) {
191 return $this->data;
192 }
193 throw new Exception( 'AI Engine: The file is not data or an URL.' );
194 }
195
196 public function get_data() {
197 if ( $this->type === 'provider_file_id' ) {
198 // Provider file IDs are just references, no data loading needed
199 throw new Exception( 'AI Engine: Cannot get data for provider file ID. Use get_refId() instead.' );
200 }
201 if ( $this->type === 'refId' || $this->type === 'url' ) {
202 return $this->get_raw_data();
203 }
204 else if ( $this->type === 'data' ) {
205 return $this->data;
206 }
207 throw new Exception( 'AI Engine: The file is not data or an URL.' );
208 }
209
210 public function get_base64() {
211 $data = $this->get_raw_data();
212 return base64_encode( $data );
213 }
214
215 /**
216 * Detect an image MIME type from the raw bytes' magic signature.
217 * Returns null when the bytes are not a recognised image format.
218 */
219 private static function detect_image_mime_from_bytes( $data ) {
220 if ( !is_string( $data ) || strlen( $data ) < 12 ) {
221 return null;
222 }
223 if ( substr( $data, 0, 3 ) === "\xFF\xD8\xFF" ) {
224 return 'image/jpeg';
225 }
226 if ( substr( $data, 0, 8 ) === "\x89PNG\r\n\x1A\n" ) {
227 return 'image/png';
228 }
229 if ( substr( $data, 0, 4 ) === 'GIF8' ) {
230 return 'image/gif';
231 }
232 if ( substr( $data, 0, 4 ) === 'RIFF' && substr( $data, 8, 4 ) === 'WEBP' ) {
233 return 'image/webp';
234 }
235 return null;
236 }
237
238 // Will return something like "data:image/jpeg;base64,{data}"
239 public function get_inline_base64_url() {
240 $data = $this->get_raw_data();
241 $mimeType = $this->mimeType;
242
243 // A CDN can transcode an image on delivery (Jetpack/Photon serves WebP uploads as
244 // JPEG, Cloudflare Polish does the same), so the URL extension we derived the MIME
245 // from can disagree with the actual bytes. Sending "data:image/webp" with JPEG bytes
246 // makes the vision model receive an undecodable image and reply as if none was sent.
247 // Trust the bytes for images.
248 if ( strpos( (string) $mimeType, 'image/' ) === 0 ) {
249 $detected = self::detect_image_mime_from_bytes( $data );
250 if ( !empty( $detected ) && $detected !== $mimeType ) {
251 $mimeType = $detected;
252 }
253 }
254
255 $b64 = base64_encode( $data );
256 return "data:{$mimeType};base64,{$b64}";
257 }
258
259 public function get_type() {
260 return $this->type;
261 }
262
263 public function get_purpose() {
264 return $this->purpose;
265 }
266
267 public function get_mimeType() {
268 return $this->mimeType;
269 }
270
271 public function is_image() {
272 return strpos( $this->mimeType, 'image' ) !== false;
273 }
274
275 public function get_fileId() {
276 return $this->fileId;
277 }
278
279 public function get_refId() {
280 if ( $this->type === 'refId' || $this->type === 'provider_file_id' ) {
281 return $this->data;
282 }
283 return null;
284 }
285
286 // Return a filename for this file. If the file is an URL, use the basename of
287 // its path. If the file is raw data, generate a generic name based on the mime type.
288 public function get_filename() {
289 // If we have an original path (from from_path), use its basename
290 if ( !empty( $this->originalPath ) ) {
291 return basename( $this->originalPath );
292 }
293 if ( $this->type === 'refId' ) {
294 global $mwai_core;
295 $path = $mwai_core->files->get_path( $this->data );
296 return basename( $path );
297 }
298 if ( $this->type === 'url' ) {
299 $path = parse_url( $this->data, PHP_URL_PATH );
300 return basename( $path );
301 }
302 if ( $this->type === 'data' ) {
303 if ( !empty( $this->mimeType ) ) {
304 $parts = explode( '/', $this->mimeType );
305 $ext = end( $parts );
306 return 'file.' . $ext;
307 }
308 return 'file.bin';
309 }
310 return 'file';
311 }
312 }
313