PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.7.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.7.2
3.7.4 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 1 week ago edit-image.php 5 months ago embed.php 5 months ago feedback.php 1 week ago function.php 1 month ago image.php 3 months ago parameter.php 1 year ago text.php 2 months ago transcribe.php 10 months ago
dropped-file.php
275 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 // Will return something like "data:image/jpeg;base64,{data}"
216 public function get_inline_base64_url() {
217 $b64 = $this->get_base64();
218 return "data:{$this->mimeType};base64,{$b64}";
219 }
220
221 public function get_type() {
222 return $this->type;
223 }
224
225 public function get_purpose() {
226 return $this->purpose;
227 }
228
229 public function get_mimeType() {
230 return $this->mimeType;
231 }
232
233 public function is_image() {
234 return strpos( $this->mimeType, 'image' ) !== false;
235 }
236
237 public function get_fileId() {
238 return $this->fileId;
239 }
240
241 public function get_refId() {
242 if ( $this->type === 'refId' || $this->type === 'provider_file_id' ) {
243 return $this->data;
244 }
245 return null;
246 }
247
248 // Return a filename for this file. If the file is an URL, use the basename of
249 // its path. If the file is raw data, generate a generic name based on the mime type.
250 public function get_filename() {
251 // If we have an original path (from from_path), use its basename
252 if ( !empty( $this->originalPath ) ) {
253 return basename( $this->originalPath );
254 }
255 if ( $this->type === 'refId' ) {
256 global $mwai_core;
257 $path = $mwai_core->files->get_path( $this->data );
258 return basename( $path );
259 }
260 if ( $this->type === 'url' ) {
261 $path = parse_url( $this->data, PHP_URL_PATH );
262 return basename( $path );
263 }
264 if ( $this->type === 'data' ) {
265 if ( !empty( $this->mimeType ) ) {
266 $parts = explode( '/', $this->mimeType );
267 $ext = end( $parts );
268 return 'file.' . $ext;
269 }
270 return 'file.bin';
271 }
272 return 'file';
273 }
274 }
275