PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.5
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.5
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 11 months ago discussions.php 11 months ago files.php 11 months ago gdpr.php 1 year ago search.php 1 year ago security.php 1 year ago tasks.php 1 year ago wand.php 1 year ago
files.php
799 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->core->get_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 $files = $this->list( $userId, $purpose, $metadata, $envId, $limit, $offset );
654 return new WP_REST_Response( [ 'success' => true, 'data' => $files ], 200 );
655 }
656
657 public function rest_delete( $request ) {
658 $params = $request->get_json_params();
659 $fileIds = empty( $params['files'] ) ? [] : $params['files'];
660 $this->delete_files( $fileIds );
661 return new WP_REST_Response( [ 'success' => true ], 200 );
662 }
663
664 public function delete_files( $fileIds ) {
665 $query = "SELECT refId, path FROM $this->table_files WHERE id IN (";
666 $params = [];
667 foreach ( $fileIds as $fileId ) {
668 $query .= '%s,';
669 $params[] = $fileId;
670 }
671 $query = rtrim( $query, ',' );
672 $query .= ')';
673 $files = $this->wpdb->get_results( $this->wpdb->prepare( $query, $params ), ARRAY_A );
674 $refIds = apply_filters( 'mwai_files_delete', array_column( $files, 'refId' ) );
675 foreach ( $files as $file ) {
676 if ( in_array( $file['refId'], $refIds ) ) {
677 $this->wpdb->delete( $this->table_files, [ 'refId' => $file['refId'] ] );
678 if ( file_exists( $file['path'] ) ) {
679 unlink( $file['path'] );
680 }
681 }
682 }
683 }
684
685 public function rest_upload() {
686 if ( empty( $_FILES['file'] ) ) {
687 return new WP_REST_Response( [ 'success' => false, 'message' => 'No file provided.' ], 400 );
688 }
689 $file = $_FILES['file'];
690 $purpose = empty( $_POST['purpose'] ) ? null : $_POST['purpose'];
691 $metadata = empty( $_POST['metadata'] ) ? null : json_decode( $_POST['metadata'], true );
692 $envId = empty( $_POST['envId'] ) ? null : $_POST['envId'];
693 if ( !$purpose ) {
694 return new WP_REST_Response( [ 'success' => false, 'message' => 'Purpose is required.' ], 400 );
695 }
696 $fileTypeCheck = wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] );
697 if ( !$fileTypeCheck['type'] ) {
698 return new WP_REST_Response( [ 'success' => false, 'message' => 'Invalid file type.' ], 400 );
699 }
700
701 try {
702 $refId = $this->upload_file( $file['tmp_name'], $file['name'], $purpose, $metadata, $envId );
703 $url = $this->get_url( $refId );
704 return new WP_REST_Response( [
705 'success' => true,
706 'data' => [ 'id' => $refId, 'url' => $url ]
707 ], 200 );
708 }
709 catch ( Exception $e ) {
710 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
711 }
712 }
713
714 #endregion
715
716 #region Database functions
717
718 public function create_db() {
719 $charset_collate = $this->wpdb->get_charset_collate();
720 $sql = "CREATE TABLE $this->table_files (
721 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
722 refId VARCHAR(64) NOT NULL,
723 envId VARCHAR(128) NULL,
724 userId BIGINT(20) UNSIGNED NULL,
725 type VARCHAR(32) NULL,
726 status VARCHAR(32) NULL,
727 purpose VARCHAR(32) NULL,
728 created DATETIME NOT NULL,
729 updated DATETIME NOT NULL,
730 expires DATETIME NULL,
731 path TEXT NULL,
732 url TEXT NULL,
733 PRIMARY KEY (id),
734 UNIQUE KEY unique_file_id (refId)
735 ) $charset_collate;";
736
737 $sqlFileMeta = "CREATE TABLE $this->table_filemeta (
738 meta_id BIGINT(20) NOT NULL AUTO_INCREMENT,
739 file_id BIGINT(20) NOT NULL,
740 meta_key varchar(255) NULL,
741 meta_value longtext NULL,
742 PRIMARY KEY (meta_id)
743 ) $charset_collate;";
744
745 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
746 dbDelta( $sql );
747 dbDelta( $sqlFileMeta );
748 }
749
750 public function check_db() {
751 if ( $this->db_check ) {
752 return true;
753 }
754
755 // Check if table_files exists
756 $sql = $this->wpdb->prepare( 'SHOW TABLES LIKE %s', $this->table_files );
757 $table_files_exists = strtolower( $this->wpdb->get_var( $sql ) ) === strtolower( $this->table_files );
758
759 // Check if table_filemeta exists
760 $sqlMeta = $this->wpdb->prepare( 'SHOW TABLES LIKE %s', $this->table_filemeta );
761 $table_filemeta_exists = strtolower( $this->wpdb->get_var( $sqlMeta ) ) === strtolower( $this->table_filemeta );
762
763 // If either table does not exist, create them
764 if ( !$table_files_exists || !$table_filemeta_exists ) {
765 $this->create_db();
766 }
767
768 // Update db_check for both tables
769 $this->db_check = $table_files_exists && $table_filemeta_exists;
770
771 // LATER: REMOVE THIS AFTER MARCH 2024
772 // $this->db_check = $this->db_check && $this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_files LIKE 'userId'" );
773 // if ( !$this->db_check ) {
774 // $this->wpdb->query( "ALTER TABLE $this->table_files ADD COLUMN userId BIGINT(20) UNSIGNED NULL" );
775 // $this->wpdb->query( "ALTER TABLE $this->table_files ADD COLUMN purpose VARCHAR(32) NULL" );
776 // $this->wpdb->query( "ALTER TABLE $this->table_files MODIFY COLUMN path TEXT NULL" );
777 // $this->wpdb->query( "ALTER TABLE $this->table_files DROP COLUMN metadata" );
778 // $this->db_check = true;
779 // }
780 // // LATER: REMOVE THIS AFTER MARCH 2024
781 // $this->db_check = $this->db_check && !$this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_files LIKE 'fileId'" );
782 // if ( !$this->db_check ) {
783 // $this->wpdb->query( "ALTER TABLE $this->table_files ADD COLUMN refId VARCHAR(64) NOT NULL" );
784 // $this->wpdb->query( "ALTER TABLE $this->table_files DROP COLUMN fileId" );
785 // $this->db_check = true;
786 // }
787 // // LATER: REMOVE THIS AFTER MARCH 2024
788 // $this->db_check = $this->db_check && $this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_files LIKE 'envId'" );
789 // if ( !$this->db_check ) {
790 // $this->wpdb->query( "ALTER TABLE $this->table_files ADD COLUMN envId VARCHAR(128) NULL" );
791 // $this->db_check = true;
792 // }
793
794 return $this->db_check;
795 }
796
797 #endregion
798 }
799