PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.0.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.0.6
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 / modules / files.php
ai-engine / classes / modules Last commit date
advisor.php 1 year ago chatbot.php 10 months ago discussions.php 10 months ago files.php 11 months ago forms-manager.php 10 months ago gdpr.php 11 months ago search.php 1 year ago security.php 1 year ago tasks.php 1 year ago wand.php 10 months ago
files.php
893 lines
1 <?php
2
3 class Meow_MWAI_Modules_Files {
4 private $core = null;
5 private $wpdb = null;
6 private $namespace = 'mwai-ui/v1';
7 private $db_check = false;
8 private $table_files = null;
9 private $table_filemeta = null;
10
11 public function __construct( $core ) {
12 global $wpdb;
13 $this->core = $core;
14 $this->wpdb = $wpdb;
15 $this->table_files = $this->wpdb->prefix . 'mwai_files';
16 $this->table_filemeta = $this->wpdb->prefix . 'mwai_filemeta';
17 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
18 if ( !wp_next_scheduled( 'mwai_files_cleanup' ) ) {
19 wp_schedule_event( time(), 'hourly', 'mwai_files_cleanup' );
20 }
21 add_action( 'mwai_files_cleanup', [ $this, 'cleanup_expired_files' ] );
22 }
23
24 public function cleanup_expired_files() {
25 $current_time = current_time( 'mysql' );
26 $expired_files = [];
27 if ( $this->check_db() ) {
28 $expired_files = $this->wpdb->get_results(
29 "SELECT * FROM $this->table_files WHERE expires IS NOT NULL AND expires < '{$current_time}'"
30 );
31 }
32 $expired_posts = get_posts( [
33 'post_type' => 'attachment',
34 'meta_key' => '_mwai_file_expires',
35 'meta_value' => $current_time,
36 'meta_compare' => '<'
37 ] );
38 $fileRefs = [];
39 foreach ( $expired_files as $file ) {
40 $fileRefs[] = $file->refId;
41 }
42 foreach ( $expired_posts as $post ) {
43 $fileRefs[] = get_post_meta( $post->ID, '_mwai_file_id', true );
44 }
45 $this->delete_expired_files( $fileRefs );
46 }
47
48 public function delete_expired_files( $fileRefs ) {
49
50 // Give a chance to other process to delete the files (for example, in the case of files hosted by Assistants)
51 $fileRefs = apply_filters( 'mwai_files_delete', $fileRefs );
52
53 if ( !is_array( $fileRefs ) ) {
54 $fileRefs = [ $fileRefs ];
55 }
56 foreach ( $fileRefs as $refId ) {
57 $file = null;
58 if ( $this->check_db() ) {
59 $file = $this->wpdb->get_row( $this->wpdb->prepare(
60 "SELECT *
61 FROM $this->table_files
62 WHERE refId = %s",
63 $refId
64 ) );
65 }
66 if ( $file ) {
67 $this->wpdb->delete( $this->table_files, [ 'refId' => $refId ] );
68 $this->wpdb->delete( $this->table_filemeta, [ 'file_id' => $file->id ] );
69 if ( file_exists( $file->path ) ) {
70 unlink( $file->path );
71 }
72 }
73 else {
74 $posts = get_posts( [ 'post_type' => 'attachment', 'meta_key' => '_mwai_file_id', 'meta_value' => $refId ] );
75 if ( $posts ) {
76 foreach ( $posts as $post ) {
77 wp_delete_attachment( $post->ID, true );
78 }
79 }
80 }
81 }
82 }
83
84 public function get_path( $refId ) {
85 $file = null;
86 if ( $this->check_db() ) {
87 $file = $this->wpdb->get_row( $this->wpdb->prepare(
88 "SELECT *
89 FROM $this->table_files
90 WHERE refId = %s",
91 $refId
92 ) );
93 }
94 if ( $file ) {
95 return $file->path;
96 }
97 else {
98 $posts = get_posts( [ 'post_type' => 'attachment', 'meta_key' => '_mwai_file_id', 'meta_value' => $refId ] );
99 if ( $posts ) {
100 foreach ( $posts as $post ) {
101 return get_attached_file( $post->ID );
102 }
103 }
104 }
105 return null;
106 }
107
108 public function get_base64_data( $refId ) {
109 $path = $this->get_path( $refId );
110 if ( $path ) {
111 $content = file_get_contents( $path );
112 $data = base64_encode( $content );
113 return $data;
114 }
115 return null;
116 }
117
118 public function is_image( $refId ) {
119 $info = $this->get_info( $refId );
120 return $info['type'] === 'image';
121 }
122
123 public function get_mime_type( $refId ) {
124 $path = $this->get_path( $refId );
125 if ( $path ) {
126 return Meow_MWAI_Core::get_mime_type( $path );
127 }
128 $url = $this->get_url( $refId );
129 if ( $url ) {
130 return Meow_MWAI_Core::get_mime_type( $url );
131 }
132 return null;
133 }
134
135 public function get_data( $refId ) {
136 $path = $this->get_path( $refId );
137 if ( $path ) {
138 $content = file_get_contents( $path );
139 return $content;
140 }
141 return null;
142 }
143
144 public function get_info( $refId ) {
145 $info = null;
146 if ( $this->check_db() ) {
147 $info = $this->wpdb->get_row( $this->wpdb->prepare(
148 "SELECT *
149 FROM $this->table_files
150 WHERE refId = %s",
151 $refId
152 ), ARRAY_A );
153 }
154 if ( !$info ) {
155 $posts = get_posts( [ 'post_type' => 'attachment', 'meta_key' => '_mwai_file_id', 'meta_value' => $refId ] );
156 if ( $posts ) {
157 $post = $posts[0];
158 $info = [
159 'refId' => $refId,
160 'url' => wp_get_attachment_url( $post->ID ),
161 'path' => get_attached_file( $post->ID )
162 ];
163 }
164 }
165 if ( $info ) {
166 $info['metadata'] = $this->get_metadata( $refId );
167 }
168 return $info;
169 }
170
171 public function get_url( $refId ) {
172 $file = null;
173 if ( $this->check_db() ) {
174 $file = $this->wpdb->get_row( $this->wpdb->prepare(
175 "SELECT *
176 FROM $this->table_files
177 WHERE refId = %s",
178 $refId
179 ) );
180 }
181 if ( $file ) {
182 return $file->url;
183 }
184 else {
185 $posts = get_posts( [ 'post_type' => 'attachment', 'meta_key' => '_mwai_file_id', 'meta_value' => $refId ] );
186 if ( $posts ) {
187 foreach ( $posts as $post ) {
188 return wp_get_attachment_url( $post->ID );
189 }
190 }
191 }
192 return null;
193 }
194
195 /**
196 * Handle a base-64 PNG returned by gpt-image-1: save as a temp file,
197 * register it in the Files DB, and give back a public URL.
198 *
199 * @param string $b64_json Raw base-64 image payload from OpenAI.
200 * @param string $purpose Optional purpose flag. Default 'generated'.
201 * @param int $ttl Time-to-live in seconds. Default 1 hour.
202 * @param string $target Target location: 'uploads' or 'library'. Default 'uploads'.
203 * @param array $metadata Additional metadata to store with the file.
204 *
205 * @return string|WP_Error Public URL or WP_Error on failure.
206 */
207 public function save_temp_image_from_b64(
208 string $b64_json,
209 string $purpose = 'generated',
210 int $ttl = HOUR_IN_SECONDS,
211 string $target = 'uploads',
212 array $metadata = []
213 ) {
214 // 1) Decode → binary.
215 $binary = base64_decode( $b64_json );
216 if ( !$binary ) {
217 return new WP_Error( 'mwai_bad_b64', 'Invalid base-64 payload.' );
218 }
219
220 // 2) Make a transient file in the server tmp dir.
221 $tmp_path = wp_tempnam( 'mwai-image' ); // Creates an empty file.
222 $filename = 'mwai-generated-' . time() . '-' . wp_generate_password( 8, false ) . '.png';
223 file_put_contents( $tmp_path, $binary );
224
225 // 3) Reuse the normal upload flow (target based on user preference, expiry = $ttl).
226 try {
227 // Extract envId from metadata if available
228 $envId = isset( $metadata['query_envId'] ) ? $metadata['query_envId'] : null;
229
230 $refId = $this->upload_file(
231 $tmp_path, // path on disk
232 $filename, // desired filename
233 $purpose, // purpose
234 $metadata, // metadata (now includes query info)
235 $envId, // envId from query
236 $target, // target (uploads or library based on user settings)
237 $ttl // expiry in seconds
238 );
239 // 4) Clean up temp file if it was uploaded to library (but not if uploads)
240 // For uploads target, the temp file IS the final file
241 if ( $target === 'library' && file_exists( $tmp_path ) ) {
242 @unlink( $tmp_path );
243 }
244
245 // 5) Turn refId → URL.
246 return $this->get_url( $refId );
247 }
248 catch ( Exception $e ) {
249 // Clean up temp file on error
250 if ( file_exists( $tmp_path ) ) {
251 @unlink( $tmp_path );
252 }
253 return new WP_Error( 'mwai_upload_failed', $e->getMessage() );
254 }
255 }
256
257 #region REST endpoints
258
259 public function rest_api_init() {
260 register_rest_route( $this->namespace, '/files/upload', [
261 'methods' => 'POST',
262 'callback' => [ $this, 'rest_upload' ],
263 'permission_callback' => [ $this->core, 'check_rest_nonce' ]
264 ] );
265 register_rest_route( $this->namespace, '/files/list', [
266 'methods' => 'POST',
267 'callback' => [ $this, 'rest_list' ],
268 'permission_callback' => [ $this->core, 'check_rest_nonce' ]
269 ] );
270 register_rest_route( $this->namespace, '/files/delete', [
271 'methods' => 'POST',
272 'callback' => [ $this, 'rest_delete' ],
273 'permission_callback' => [ $this->core, 'check_rest_nonce' ]
274 ] );
275 }
276
277 /*
278 * Record a new file in the Files database.
279 * This doesn't handle the upload or anything.
280 */
281 public function commit_file( $fileInfo ) {
282 if ( !$this->check_db() ) {
283 throw new Exception( 'Could not create database table.' );
284 }
285 $now = date( 'Y-m-d H:i:s' );
286 if ( empty( $fileInfo['refId'] ) ) {
287 if ( !empty( $fileInfo['url'] ) ) {
288 $fileInfo['refId'] = $this->generate_refId( $fileInfo['url'] );
289 }
290 else {
291 throw new Exception( 'File ID (or URL) is required.' );
292 }
293 }
294 if ( empty( $fileInfo['type'] ) ) {
295 $fileInfo['type'] = Meow_MWAI_Core::is_image( $fileInfo['url'] ) ? 'image' : 'file';
296 }
297 $success = $this->wpdb->insert( $this->table_files, [
298 'refId' => $fileInfo['refId'],
299 'envId' => empty( $fileInfo['envId'] ) ? null : $fileInfo['envId'],
300 'userId' => empty( $fileInfo['userId'] ) ? $this->get_effective_user_id() : $fileInfo['userId'],
301 'purpose' => empty( $fileInfo['purpose'] ) ? null : $fileInfo['purpose'],
302 'type' => empty( $fileInfo['type'] ) ? null : $fileInfo['type'],
303 'status' => empty( $fileInfo['status'] ) ? null : $fileInfo['status'],
304 'created' => empty( $fileInfo['created'] ) ? $now : $fileInfo['created'],
305 'updated' => empty( $fileInfo['updated'] ) ? $now : $fileInfo['updated'],
306 'expires' => empty( $fileInfo['expires'] ) ? null : $fileInfo['expires'],
307 'path' => empty( $fileInfo['path'] ) ? null : $fileInfo['path'],
308 'url' => empty( $fileInfo['url'] ) ? null : $fileInfo['url']
309 ] );
310 // check for error
311 if ( !$success ) {
312 throw new Exception( 'Error while adding file in the DB (' . $this->wpdb->last_error . ')' );
313 }
314 return $this->wpdb->insert_id;
315 }
316
317 // Generate a refId from a URL or random, and make sure it's unique
318 public function generate_refId( $attempts = 0 ) {
319 $refId = md5( date( 'Y-m-d H:i:s' ) . '-' . $attempts );
320 $file = $this->wpdb->get_row( $this->wpdb->prepare(
321 "SELECT *
322 FROM $this->table_files
323 WHERE refId = %s",
324 $refId
325 ) );
326 if ( $file ) {
327 return $this->generate_refId( $attempts + 1 );
328 }
329 return $refId;
330 }
331
332 public function upload_file(
333 $path,
334 $filename = null,
335 $purpose = null,
336 $metadata = null,
337 $envId = null,
338 $target = null,
339 $expiry = null
340 ) {
341 require_once( ABSPATH . 'wp-admin/includes/image.php' );
342 require_once( ABSPATH . 'wp-admin/includes/file.php' );
343 require_once( ABSPATH . 'wp-admin/includes/media.php' );
344
345 $target = empty( $target ) ? $this->core->get_option( 'image_local_upload' ) : $target;
346 $expiry = empty( $expiry ) ? $this->core->get_option( 'image_expires' ) : $expiry;
347
348 $expires = ( $expiry === 'never' || empty( $expiry ) ) ? null : date( 'Y-m-d H:i:s', time() + intval( $expiry ) );
349 $refId = $this->generate_refId();
350 $url = null;
351 if ( empty( $filename ) ) {
352 $parsed_url = parse_url( $path, PHP_URL_PATH );
353 $filename = basename( $parsed_url );
354 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
355 }
356 else {
357 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
358 }
359
360 // Validate file type using WordPress built-in function
361 $validate = wp_check_filetype( $filename );
362 if ( $validate['type'] == false ) {
363 throw new Exception( 'File type is not allowed.' );
364 }
365 $newFilename = $refId . '.' . $extension;
366 $unique_filename = wp_unique_filename( wp_upload_dir()['path'], $newFilename );
367 $destination = wp_upload_dir()['path'] . '/' . $unique_filename;
368
369 if ( $target === 'uploads' ) {
370 if ( !$this->check_db() ) {
371 throw new Exception( 'Could not create database table.' );
372 }
373 if ( !copy( $path, $destination ) ) {
374 throw new Exception( 'Could not move the file.' );
375 }
376 $url = wp_upload_dir()['url'] . '/' . $unique_filename;
377
378 $now = date( 'Y-m-d H:i:s' );
379 $fileId = $this->commit_file( [
380 'refId' => $refId,
381 'envId' => $envId,
382 'purpose' => $purpose,
383 'type' => null,
384 'status' => 'uploaded',
385 'created' => $now,
386 'updated' => $now,
387 'expires' => $expires,
388 'path' => $destination,
389 'url' => $url
390 ] );
391 if ( $metadata && is_array( $metadata ) ) {
392 foreach ( $metadata as $metaKey => $metaValue ) {
393 $this->add_metadata( $fileId, $metaKey, $metaValue );
394 }
395 }
396
397 }
398 else if ( $target === 'library' ) {
399
400 if ( filter_var( $path, FILTER_VALIDATE_URL ) ) {
401 $tmp = download_url( $path );
402 if ( is_wp_error( $tmp ) ) {
403 throw new Exception( $tmp->get_error_message() );
404 }
405 $file_array = [ 'name' => $unique_filename, 'tmp_name' => $tmp ];
406 }
407 else {
408 $file_array = [ 'name' => $unique_filename, 'tmp_name' => $path ];
409 }
410
411 $id = media_handle_sideload( $file_array, 0 );
412 if ( is_wp_error( $id ) ) {
413 throw new Exception( $id->get_error_message() );
414 }
415
416 $url = wp_get_attachment_url( $id );
417 update_post_meta( $id, '_mwai_file_id', $refId );
418 update_post_meta( $id, '_mwai_file_expires', $expires );
419
420 // Store additional metadata
421 if ( $metadata && is_array( $metadata ) ) {
422 foreach ( $metadata as $metaKey => $metaValue ) {
423 update_post_meta( $id, '_mwai_' . $metaKey, $metaValue );
424 }
425 }
426
427 // Store purpose and envId as post meta
428 if ( $purpose ) {
429 update_post_meta( $id, '_mwai_purpose', $purpose );
430 }
431 if ( $envId ) {
432 update_post_meta( $id, '_mwai_envId', $envId );
433 }
434 }
435
436 return $refId;
437 }
438
439 public function add_metadata( $fileId, $metaKey, $metaValue ) {
440 $data = [
441 'file_id' => $fileId,
442 'meta_key' => $metaKey,
443 'meta_value' => $metaValue
444 ];
445 $res = $this->wpdb->insert( $this->table_filemeta, $data );
446 if ( $res === false ) {
447 Meow_MWAI_Logging::warn( 'Error while writing files metadata (' . $this->wpdb->last_error . ')' );
448 return false;
449 }
450 return $this->wpdb->insert_id;
451 }
452
453 public function update_refId( $fileId, $refId ) {
454 if ( $this->check_db() ) {
455 $this->wpdb->update( $this->table_files, [ 'refId' => $refId ], [ 'id' => $fileId ] );
456 }
457 }
458
459 public function update_purpose( $fileId, $purpose ) {
460 if ( $this->check_db() ) {
461 $this->wpdb->update( $this->table_files, [ 'purpose' => $purpose ], [ 'id' => $fileId ] );
462 }
463 }
464
465 public function update_envId( $fileId, $envId ) {
466 if ( $this->check_db() ) {
467 $this->wpdb->update( $this->table_files, [ 'envId' => $envId ], [ 'id' => $fileId ] );
468 }
469 }
470
471 public function get_metadata( $refId, $fileId = null ) {
472 if ( !$fileId ) {
473 $fileId = $this->get_id_from_refId( $refId );
474 }
475 if ( $fileId ) {
476 $sql = $this->wpdb->prepare( "SELECT * FROM $this->table_filemeta WHERE file_id = %d", $fileId );
477 $metadata = $this->wpdb->get_results( $sql, ARRAY_A );
478 $meta = [];
479 foreach ( $metadata as $metaItem ) {
480 $meta[$metaItem['meta_key']] = $metaItem['meta_value'];
481 }
482 return $meta;
483 }
484 return null;
485 }
486
487 public function search( $userId = null, $purpose = null, $metadata = [], $envId = null ) {
488 list( $sql, $params ) = $this->_buildQuery( $userId, $purpose, $metadata, $envId, true );
489 $finalQuery = $this->wpdb->prepare( $sql, $params );
490 $files = $this->wpdb->get_results( $finalQuery, ARRAY_A );
491 foreach ( $files as &$file ) {
492 $file['metadata'] = $this->get_metadata( $file['refId'] );
493 }
494 return $files;
495 }
496
497 public function list(
498 $userId = null,
499 $purpose = null,
500 $metadata = [],
501 $envId = null,
502 $limit = 10,
503 $offset = 0
504 ) {
505 list( $countSql, $countParams ) = $this->_buildQuery( $userId, $purpose, $metadata, $envId, false );
506 $total = $this->wpdb->get_var( $this->wpdb->prepare( $countSql, $countParams ) );
507
508 list( $fileSql, $fileParams ) = $this->_buildQuery( $userId, $purpose, $metadata, $envId, true );
509 if ( $limit ) {
510 $fileSql .= ' LIMIT %d';
511 $fileParams[] = $limit;
512 }
513 if ( $offset ) {
514 $fileSql .= ' OFFSET %d';
515 $fileParams[] = $offset;
516 }
517 $files = $this->wpdb->get_results( $this->wpdb->prepare( $fileSql, $fileParams ), ARRAY_A );
518 foreach ( $files as &$file ) {
519 $file['metadata'] = $this->get_metadata( $file['refId'] );
520 }
521 return [ 'files' => $files, 'total' => $total ];
522 }
523
524 private function _buildQuery( $userId, $purpose, $metadata, $envId, $selectStar ) {
525 $sql = $selectStar ? "SELECT * FROM $this->table_files WHERE 1=1" : "SELECT COUNT(*) FROM $this->table_files WHERE 1=1";
526 $params = [];
527
528 // Based on the old "search" function
529 $actualUserId = $this->core->get_user_id();
530 $canAdmin = $this->core->can_access_settings();
531 if ( $userId !== $actualUserId ) {
532 if ( !$canAdmin ) {
533 throw new Exception( 'You are not allowed to access files from another user.' );
534 }
535 }
536 if ( $userId ) {
537 $sql .= ' AND userId = %d';
538 $params[] = $userId;
539 }
540 if ( $purpose ) {
541 if ( is_array( $purpose ) ) {
542 $sql .= ' AND (';
543 foreach ( $purpose as $p ) {
544 $sql .= ' purpose = %s OR';
545 $params[] = $p;
546 }
547 $sql = rtrim( $sql, 'OR' );
548 $sql .= ')';
549 }
550 else {
551 $sql .= ' AND purpose = %s';
552 $params[] = $purpose;
553 }
554 }
555 if ( $metadata ) {
556 foreach ( $metadata as $metaKey => $metaValue ) {
557 $sql .= " AND EXISTS ( SELECT * FROM $this->table_filemeta
558 WHERE file_id = $this->table_files.id AND meta_key = %s AND meta_value = %s )";
559 $params[] = $metaKey;
560 $params[] = $metaValue;
561 }
562 }
563 if ( $envId ) {
564 $sql .= ' AND envId = %s';
565 $params[] = $envId;
566 }
567 $sql .= ' ORDER BY updated DESC';
568 return [ $sql, $params ];
569 }
570
571 // public function search( $userId = null, $purpose = null, $metadata = [], $limit = 10, $offset = 0 ) {
572 // $sql = "SELECT * FROM $this->table_files WHERE 1=1";
573 // $actualUserId = $this->core->get_user_id();
574 // $canAdmin = $this->core->can_access_settings();
575 // if ( $userId !== $actualUserId ) {
576 // if ( !$canAdmin ) {
577 // throw new Exception( 'You are not allowed to access files from another user.' );
578 // }
579 // }
580 // if ( $userId ) {
581 // $sql .= $this->wpdb->prepare( " AND userId = %d", $userId );
582 // }
583 // if ( $purpose ) {
584 // if ( is_array( $purpose ) ) {
585 // $sql .= " AND (";
586 // foreach ( $purpose as $p ) {
587 // $sql .= $this->wpdb->prepare( " purpose = %s OR", $p );
588 // }
589 // $sql = rtrim( $sql, 'OR' );
590 // $sql .= ")";
591 // }
592 // else {
593 // $sql .= $this->wpdb->prepare( " AND purpose = %s", $purpose );
594 // }
595 // }
596 // if ( $metadata ) {
597 // foreach ( $metadata as $metaKey => $metaValue ) {
598 // $sql .= $this->wpdb->prepare( " AND EXISTS ( SELECT * FROM $this->table_filemeta
599 // WHERE file_id = $this->table_files.id AND meta_key = %s AND meta_value = %s )",
600 // $metaKey, $metaValue
601 // );
602 // }
603 // }
604 // $sql .= " ORDER BY updated DESC";
605 // if ( $limit ) {
606 // $sql .= $this->wpdb->prepare( " LIMIT %d", $limit );
607 // }
608 // if ( $offset ) {
609 // $sql .= $this->wpdb->prepare( " OFFSET %d", $offset );
610 // }
611 // $files = $this->wpdb->get_results( $sql, ARRAY_A );
612
613 // // Add metadata
614 // foreach ( $files as &$file ) {
615 // $file['metadata'] = $this->get_metadata( $file['refId'] );
616 // }
617
618 // return $files;
619 // }
620
621 public function get_id_from_refId( $refId ) {
622 $file = null;
623 if ( $this->check_db() ) {
624 $file = $this->wpdb->get_row( $this->wpdb->prepare(
625 "SELECT *
626 FROM $this->table_files
627 WHERE refId = %s",
628 $refId
629 ) );
630 }
631 if ( $file ) {
632 return $file->id;
633 }
634 return null;
635 }
636
637 public function add_metadata_from_refId( $refId, $metaKey, $metaValue ) {
638 $fileId = $this->get_id_from_refId( $refId );
639 if ( $fileId ) {
640 return $this->add_metadata( $fileId, $metaKey, $metaValue );
641 }
642 return false;
643 }
644
645 public function rest_list( $request ) {
646 $params = $request->get_json_params();
647 $userId = empty( $params['userId'] ) ? null : $params['userId'];
648 $envId = empty( $params['envId'] ) ? null : $params['envId'];
649 $purpose = empty( $params['purpose'] ) ? null : $params['purpose'];
650 $metadata = empty( $params['metadata'] ) ? null : json_decode( $params['metadata'], true );
651 $limit = empty( $params['limit'] ) ? 10 : intval( $params['limit'] );
652 $offset = empty( $params['page'] ) ? 0 : ( intval( $params['page'] ) - 1 ) * $limit;
653
654 // Security fix: For unauthenticated users or users without explicit userId,
655 // restrict to their own files based on session
656 $currentUserId = $this->core->get_user_id();
657 if ( !$currentUserId || $currentUserId === 0 ) {
658 // For unauthenticated users, get session-based user ID
659 $sessionUserId = $this->core->get_session_user_id();
660 if ( !$sessionUserId ) {
661 return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized access' ], 403 );
662 }
663 $userId = $sessionUserId;
664 } else if ( empty( $userId ) ) {
665 // For authenticated users without specified userId, use their own ID
666 $userId = $currentUserId;
667 } else if ( $userId !== $currentUserId && !$this->core->can_access_settings() ) {
668 // Non-admin users can only access their own files
669 return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized access to other user files' ], 403 );
670 }
671
672 $files = $this->list( $userId, $purpose, $metadata, $envId, $limit, $offset );
673 return new WP_REST_Response( [ 'success' => true, 'data' => $files ], 200 );
674 }
675
676 public function rest_delete( $request ) {
677 $params = $request->get_json_params();
678 $fileIds = empty( $params['files'] ) ? [] : $params['files'];
679
680 // Security fix: Verify user can delete these files
681 $currentUserId = $this->core->get_user_id();
682 $sessionUserId = null;
683
684 if ( !$currentUserId || $currentUserId === 0 ) {
685 // For unauthenticated users, get session-based user ID
686 $sessionUserId = $this->core->get_session_user_id();
687 if ( !$sessionUserId ) {
688 return new WP_REST_Response( [ 'success' => false, 'message' => 'Unauthorized access' ], 403 );
689 }
690 }
691
692 // Verify ownership of files before deletion
693 $authorizedFileIds = $this->filter_authorized_files( $fileIds, $currentUserId ?: $sessionUserId );
694
695 if ( empty( $authorizedFileIds ) ) {
696 return new WP_REST_Response( [ 'success' => false, 'message' => 'No authorized files to delete' ], 403 );
697 }
698
699 $this->delete_files( $authorizedFileIds );
700 return new WP_REST_Response( [ 'success' => true, 'deleted' => count( $authorizedFileIds ) ], 200 );
701 }
702
703 public function delete_files( $fileIds ) {
704 $query = "SELECT refId, path FROM $this->table_files WHERE id IN (";
705 $params = [];
706 foreach ( $fileIds as $fileId ) {
707 $query .= '%s,';
708 $params[] = $fileId;
709 }
710 $query = rtrim( $query, ',' );
711 $query .= ')';
712 $files = $this->wpdb->get_results( $this->wpdb->prepare( $query, $params ), ARRAY_A );
713 $refIds = apply_filters( 'mwai_files_delete', array_column( $files, 'refId' ) );
714 foreach ( $files as $file ) {
715 if ( in_array( $file['refId'], $refIds ) ) {
716 $this->wpdb->delete( $this->table_files, [ 'refId' => $file['refId'] ] );
717 if ( file_exists( $file['path'] ) ) {
718 unlink( $file['path'] );
719 }
720 }
721 }
722 }
723
724 /**
725 * Get effective user ID for file ownership
726 * Returns actual user ID for logged-in users, or session-based ID for guests
727 *
728 * @return int|string User ID or session-based ID
729 */
730 private function get_effective_user_id() {
731 $userId = $this->core->get_user_id();
732 if ( !$userId || $userId === 0 ) {
733 // For guest users, use session-based ID
734 return $this->core->get_session_user_id();
735 }
736 return $userId;
737 }
738
739 /**
740 * Filter file IDs to only include those the user is authorized to access
741 *
742 * @param array $fileIds Array of file IDs to filter
743 * @param int|string $userId User ID (can be session-based string for guests)
744 * @return array Array of authorized file IDs
745 */
746 private function filter_authorized_files( $fileIds, $userId ) {
747 if ( empty( $fileIds ) || empty( $userId ) ) {
748 return [];
749 }
750
751 // Admins can access all files
752 if ( $this->core->can_access_settings() ) {
753 return $fileIds;
754 }
755
756 // Build query to check file ownership
757 $placeholders = array_fill( 0, count( $fileIds ), '%s' );
758 $query = $this->wpdb->prepare(
759 "SELECT id FROM $this->table_files
760 WHERE id IN (" . implode( ',', $placeholders ) . ")
761 AND userId = %s",
762 array_merge( $fileIds, [ $userId ] )
763 );
764
765 $authorizedIds = $this->wpdb->get_col( $query );
766 return array_map( 'intval', $authorizedIds );
767 }
768
769 public function rest_upload() {
770 if ( empty( $_FILES['file'] ) ) {
771 return new WP_REST_Response( [ 'success' => false, 'message' => 'No file provided.' ], 400 );
772 }
773 $file = $_FILES['file'];
774 $purpose = empty( $_POST['purpose'] ) ? null : $_POST['purpose'];
775 $metadata = empty( $_POST['metadata'] ) ? null : json_decode( $_POST['metadata'], true );
776 $envId = empty( $_POST['envId'] ) ? null : $_POST['envId'];
777 if ( !$purpose ) {
778 return new WP_REST_Response( [ 'success' => false, 'message' => 'Purpose is required.' ], 400 );
779 }
780 $fileTypeCheck = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] );
781 if ( !$fileTypeCheck['type'] ) {
782 return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid file type.' ], 400 );
783 }
784
785 try {
786 $refId = $this->upload_file( $file['tmp_name'], $file['name'], $purpose, $metadata, $envId );
787 $url = $this->get_url( $refId );
788 return new WP_REST_Response( [
789 'success' => true,
790 'data' => [ 'id' => $refId, 'url' => $url ]
791 ], 200 );
792 }
793 catch ( Exception $e ) {
794 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
795 }
796 }
797
798 #endregion
799
800 #region Database functions
801
802 public function create_db() {
803 $charset_collate = $this->wpdb->get_charset_collate();
804 $sql = "CREATE TABLE $this->table_files (
805 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
806 refId VARCHAR(64) NOT NULL,
807 envId VARCHAR(128) NULL,
808 userId VARCHAR(64) NULL,
809 type VARCHAR(32) NULL,
810 status VARCHAR(32) NULL,
811 purpose VARCHAR(32) NULL,
812 created DATETIME NOT NULL,
813 updated DATETIME NOT NULL,
814 expires DATETIME NULL,
815 path TEXT NULL,
816 url TEXT NULL,
817 PRIMARY KEY (id),
818 UNIQUE KEY unique_file_id (refId)
819 ) $charset_collate;";
820
821 $sqlFileMeta = "CREATE TABLE $this->table_filemeta (
822 meta_id BIGINT(20) NOT NULL AUTO_INCREMENT,
823 file_id BIGINT(20) NOT NULL,
824 meta_key varchar(255) NULL,
825 meta_value longtext NULL,
826 PRIMARY KEY (meta_id)
827 ) $charset_collate;";
828
829 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
830 dbDelta( $sql );
831 dbDelta( $sqlFileMeta );
832 }
833
834 public function check_db() {
835 if ( $this->db_check ) {
836 return true;
837 }
838
839 // Check if table_files exists
840 $sql = $this->wpdb->prepare( 'SHOW TABLES LIKE %s', $this->table_files );
841 $table_files_exists = strtolower( $this->wpdb->get_var( $sql ) ) === strtolower( $this->table_files );
842
843 // Check if table_filemeta exists
844 $sqlMeta = $this->wpdb->prepare( 'SHOW TABLES LIKE %s', $this->table_filemeta );
845 $table_filemeta_exists = strtolower( $this->wpdb->get_var( $sqlMeta ) ) === strtolower( $this->table_filemeta );
846
847 // If either table does not exist, create them
848 if ( !$table_files_exists || !$table_filemeta_exists ) {
849 $this->create_db();
850 }
851
852 // Update db_check for both tables
853 $this->db_check = $table_files_exists && $table_filemeta_exists;
854
855 // Check if userId column needs to be updated to VARCHAR for session support
856 // LATER: REMOVE THIS AFTER JANUARY 2026
857 if ( $this->db_check ) {
858 $column_info = $this->wpdb->get_row( "SHOW COLUMNS FROM $this->table_files WHERE Field = 'userId'" );
859 if ( $column_info && strpos( $column_info->Type, 'BIGINT' ) !== false ) {
860 // Update userId column from BIGINT to VARCHAR to support session-based IDs
861 $this->wpdb->query( "ALTER TABLE $this->table_files MODIFY COLUMN userId VARCHAR(64) NULL" );
862 }
863 }
864
865 // LATER: REMOVE THIS AFTER MARCH 2024
866 // $this->db_check = $this->db_check && $this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_files LIKE 'userId'" );
867 // if ( !$this->db_check ) {
868 // $this->wpdb->query( "ALTER TABLE $this->table_files ADD COLUMN userId BIGINT(20) UNSIGNED NULL" );
869 // $this->wpdb->query( "ALTER TABLE $this->table_files ADD COLUMN purpose VARCHAR(32) NULL" );
870 // $this->wpdb->query( "ALTER TABLE $this->table_files MODIFY COLUMN path TEXT NULL" );
871 // $this->wpdb->query( "ALTER TABLE $this->table_files DROP COLUMN metadata" );
872 // $this->db_check = true;
873 // }
874 // // LATER: REMOVE THIS AFTER MARCH 2024
875 // $this->db_check = $this->db_check && !$this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_files LIKE 'fileId'" );
876 // if ( !$this->db_check ) {
877 // $this->wpdb->query( "ALTER TABLE $this->table_files ADD COLUMN refId VARCHAR(64) NOT NULL" );
878 // $this->wpdb->query( "ALTER TABLE $this->table_files DROP COLUMN fileId" );
879 // $this->db_check = true;
880 // }
881 // // LATER: REMOVE THIS AFTER MARCH 2024
882 // $this->db_check = $this->db_check && $this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_files LIKE 'envId'" );
883 // if ( !$this->db_check ) {
884 // $this->wpdb->query( "ALTER TABLE $this->table_files ADD COLUMN envId VARCHAR(128) NULL" );
885 // $this->db_check = true;
886 // }
887
888 return $this->db_check;
889 }
890
891 #endregion
892 }
893