PluginProbe
Media Cloud Sync / 1.4.1
Media Cloud Sync v1.4.1
1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 All 35 releases
← All changes | includes/base/item.php +842 -207 1.2.121.4.1 View file →
@@ -18,8 +18,17 @@
18 18 protected $bucket_name;
19 19 protected $region = '';
20 20
21 21 /**
22 + * Absolute paths restored from cloud (by any integration) pending removal
23 + * again — fed into the pre-update pipeline if a save happens this request,
24 + * with a shutdown fallback otherwise. See track_restored_for_cleanup().
25 + * @since 1.4.0
26 + */
27 + protected $pending_restored_files = [];
28 + protected $pending_cleanup_hooked = false;
29 +
30 + /**
22 31 * Admin constructor.
23 32 * @since 1.0.0
24 33 */
25 34 public function __construct() {
@@ -44,16 +53,12 @@
44 53 ? $this->credentials['bucketConfig']
45 54 : [];
46 55 $this->service = isset($this->credentials['service']) && !empty($this->credentials['service'])
47 56 ? $this->credentials['service']
48 - : [];
57 + : '';
49 58
50 - if (isset($this->bucketConfig['bucket_name'])) {
51 - $this->bucket_name = $this->bucketConfig['bucket_name'];
52 - }
53 - if (isset($this->config['region'])) {
54 - $this->region = $this->config['region'];
55 - }
59 + $this->bucket_name = isset($this->bucketConfig['bucket_name']) ? $this->bucketConfig['bucket_name'] : '';
60 + $this->region = isset($this->config['region']) ? $this->config['region'] : '';
56 61 }
57 62
58 63 /**
59 64 * Add Item In database
@@ -64,15 +69,16 @@
64 69 $source_id,
65 70 $url,
66 71 $key,
67 72 $source_path,
68 - $meta,
73 + $original_source_path = '',
74 + $original_key = '',
75 + $meta = array(),
69 76 $source_type = 'media_library',
70 77 $is_private = 0
71 78 ) {
72 79 global $wpdb;
73 80 $item_id = false;
74 -
75 81 $data = array(
76 82 'provider' => $this->service,
77 83 'region' => $this->region,
78 84 'storage' => $this->bucket_name,
@@ -80,19 +86,58 @@
80 86 'source_path' => $source_path,
81 87 'source_type' => $source_type,
82 88 'url' => $url,
83 89 'key' => $key,
90 + 'original_source_path' => $original_source_path,
91 + 'original_key' => $original_key,
84 92 'is_private' => $is_private,
85 - 'extra' => maybe_serialize($meta),
93 + 'extra' => Utils::maybe_serialize($meta),
86 94 );
87 - if ($wpdb->insert(Db::get_table_name(), $data)) {
88 - $item_id = $wpdb->insert_id;
95 +
96 + // Do some pre-update actions
97 + $this->pre_update_item($source_id, $data, [], $source_type);
98 +
99 + // Upsert instead of a plain insert — two independent triggers (this plugin's bulk
100 + // sync, WordPress's own metadata hook, Imagify's re-sync) can land on the same
101 + // attachment around the same time; this converges on one row instead of erroring.
102 + $table = Db::get_table_name();
103 + $inserted = $wpdb->query($wpdb->prepare(
104 + "INSERT INTO {$table}
105 + (provider, region, storage, source_id, source_path, source_type, url, `key`, original_source_path, original_key, is_private, extra)
106 + VALUES (%s, %s, %s, %d, %s, %s, %s, %s, %s, %s, %d, %s)
107 + ON DUPLICATE KEY UPDATE
108 + id = LAST_INSERT_ID(id),
109 + source_path = VALUES(source_path),
110 + url = VALUES(url),
111 + `key` = VALUES(`key`),
112 + original_source_path = VALUES(original_source_path),
113 + original_key = VALUES(original_key),
114 + is_private = VALUES(is_private),
115 + extra = VALUES(extra)",
116 + $data['provider'], $data['region'], $data['storage'], $data['source_id'], $data['source_path'],
117 + $data['source_type'], $data['url'], $data['key'], $data['original_source_path'], $data['original_key'],
118 + $data['is_private'], $data['extra']
119 + ));
120 +
121 + if ($inserted !== false) {
122 + // rows_affected: 1 = new row, 2 = existing row updated. Capture before any
123 + // other query on $wpdb overwrites it.
124 + $is_new_row = (int) $wpdb->rows_affected === 1;
125 +
126 + $item_id = (int) $wpdb->insert_id;
89 127 $data['id'] = $item_id;
90 128 Integration::update_meta($source_id, 'item', $data, false, false, $source_type);
91 129 Cache::update_item_cache($source_id.'_item_'.$source_type, $data);
92 - Counter::add( 'uploaded', $source_type );
130 +
131 + // Only count a genuinely new item, not a duplicate-collision update.
132 + if ($is_new_row) {
133 + Counter::add( 'uploaded', $source_type );
134 + }
93 135 }
94 136
137 + // Do some post-update actions
138 + $this->post_update_item($source_id, $data, $source_type);
139 +
95 140 return $item_id;
96 141 }
97 142
98 143
@@ -105,9 +150,22 @@
105 150 if($item === false) {
106 151 $item = Integration::get_meta($source_id, 'item', false, false, false, $source_type);
107 152 if($item == false){
108 153 $item_table = Db::get_table_name();
109 - $item = $wpdb->get_row("SELECT * FROM ".$item_table." WHERE source_id = $source_id AND source_type='$source_type'", ARRAY_A);
154 + $query = "SELECT * FROM {$item_table}
155 + WHERE source_id = %d
156 + AND source_type = %s
157 + AND provider = %s
158 + AND storage = %s";
159 +
160 + if(!empty($this->region)) {
161 + $query .= " AND region = %s";
162 + $query = $wpdb->prepare($query, $source_id, $source_type, $this->service, $this->bucket_name, $this->region);
163 + } else {
164 + $query = $wpdb->prepare($query, $source_id, $source_type, $this->service, $this->bucket_name);
165 + }
166 +
167 + $item = $wpdb->get_row( $query, ARRAY_A);
110 168
111 169 if($wpdb->last_error || null === $item || !(isset($item) && !empty($item))) {
112 170 Cache::update_item_cache($source_id.'_item_'.$source_type, '');
113 171 return false;
@@ -118,9 +176,16 @@
118 176 // Update cache
119 177 Cache::update_item_cache($source_id.'_item_'.$source_type, $item == false ? '' : $item);
120 178 }
121 179
122 - return !empty($item) ? $item : false;
180 + /**
181 + * Filter to modify item data when retrieved
182 + * @param array|false $item
183 + * @param int $source_id
184 + * @param string $source_type
185 + * @since 1.3.5
186 + */
187 + return apply_filters( 'wpmcs_get_item' , !empty($item) ? $item : false, $source_id, $source_type );
123 188 }
124 189
125 190 /**
126 191 * Function to delete item from data base
@@ -128,9 +193,22 @@
128 193 */
129 194 public function delete($source_id, $source_type = 'media_library'){
130 195 global $wpdb;
131 196 if (isset($source_id) && !Utils::is_empty($source_id)) {
132 - $rows = $wpdb->delete(Db::get_table_name(), array('source_id' => $source_id, 'source_type' => $source_type));
197 + // Delete attachments by item from cloud
198 + $item = $this->get($source_id, $source_type);
199 + $this->delete_attachments_by_item($item);
200 +
201 + $where = array(
202 + 'source_id' => $source_id,
203 + 'source_type' => $source_type,
204 + 'provider' => $this->service,
205 + 'storage' => $this->bucket_name,
206 + );
207 + if(!empty($this->region)) {
208 + $where['region'] = $this->region;
209 + }
210 + $rows = $wpdb->delete( Db::get_table_name(), $where );
133 211 if ($wpdb->last_error || false === $rows) {
134 212 return false;
135 213 }
136 214 Integration::delete_meta($source_id, 'item', false, $source_type);
@@ -135,8 +213,12 @@
135 213 }
136 214 Integration::delete_meta($source_id, 'item', false, $source_type);
137 215 Cache::delete_item_cache($source_id.'_item_'.$source_type);
138 216 Counter::remove( 'uploaded', $source_type );
217 +
218 + // Remove logs by media ID & source type
219 + Logger::instance()->remove_log_by_media_id($source_id, $source_type);
220 +
139 221 return true;
140 222 }
141 223 return false;
142 224 }
@@ -144,18 +226,69 @@
144 226
145 227 /**
146 228 * Function to update item in data base
147 229 * @since 1.0.0
230 + * @param array|null $target_identity Optional ['provider'=>, 'storage'=>, 'region'=>] to
231 + * move the row to a different connection's identity.
232 + * The WHERE clause still uses this instance's own
233 + * (source) binding to locate the row — only the SET
234 + * values change. @since 1.4.0
148 235 */
149 - public function update($source_id, $data, $source_type = 'media_library') {
236 + public function update($source_id, $data, $source_type = 'media_library', $target_identity = null) {
150 237 global $wpdb;
151 238 if (isset($source_id) && !Utils::is_empty($source_id)) {
239 + $data['provider'] = $target_identity['provider'] ?? $this->service;
240 + $data['storage'] = $target_identity['storage'] ?? $this->bucket_name;
241 + $data['region'] = $target_identity['region'] ?? $this->region;
152 242
153 - $data['provider'] = $this->service;
154 - $data['region'] = $this->region;
155 - $data['storage'] = $this->bucket_name;
243 + $old_item = $this->get($source_id, $source_type);
156 244
157 - $rows = $wpdb->update(Db::get_table_name(), $data, array('source_id' => $source_id, 'source_type' => $source_type));
245 + // update data from $old_item if not exists in $data
246 + if (isset($old_item) && !empty($old_item)) {
247 + foreach ($old_item as $key => $value) {
248 + if (!isset($data[$key])) {
249 + $data[$key] = $value;
250 + }
251 + }
252 + }
253 +
254 + // Do some pre-update actions
255 + $this->pre_update_item($source_id, $data, $old_item, $source_type);
256 +
257 + // Moving to a different identity (e.g. bucket_to_bucket migration) can collide
258 + // with a stale row already sitting at that target identity — e.g. an interrupted
259 + // earlier migration attempt, or the same item independently tracked under a
260 + // connection this site used previously. uidx_item_source is UNIQUE on
261 + // (source_id, source_type, provider, storage, region), so the UPDATE below would
262 + // otherwise fail outright. The row being updated here is the current, live one;
263 + // a pre-existing row already at the target is stale by definition — clear it
264 + // first rather than letting the whole update silently fail.
265 + $is_identity_move = $target_identity !== null && (
266 + $data['provider'] !== $this->service ||
267 + $data['storage'] !== $this->bucket_name ||
268 + $data['region'] !== $this->region
269 + );
270 + if ($is_identity_move) {
271 + $target_where = array(
272 + 'source_id' => $source_id,
273 + 'source_type' => $source_type,
274 + 'provider' => $data['provider'],
275 + 'storage' => $data['storage'],
276 + 'region' => $data['region'],
277 + );
278 + $wpdb->delete(Db::get_table_name(), $target_where);
279 + }
280 +
281 + $where = array(
282 + 'source_id' => $source_id,
283 + 'source_type' => $source_type,
284 + 'provider' => $this->service,
285 + 'storage' => $this->bucket_name,
286 + );
287 + if(!empty($this->region)) {
288 + $where['region'] = $this->region;
289 + }
290 + $rows = $wpdb->update(Db::get_table_name(), $data, $where);
158 291 if ($wpdb->last_error || false === $rows) {
159 292 return false;
160 293 }
161 294
@@ -161,10 +294,17 @@
161 294
162 295 Integration::delete_meta($source_id, 'item', false, $source_type);
163 296 Cache::delete_item_cache($source_id.'_item_'.$source_type);
164 297
165 - // Reset cache
166 - $current_data = $this->get($source_id, $source_type);
298 + if ($target_identity === null) {
299 + // Reset cache — skipped when moving to a different identity: this instance's
300 + // get() would search under the now-stale source identity and cache a false
301 + // negative; the delete_item_cache() above is sufficient on its own there.
302 + $this->get($source_id, $source_type);
303 + }
304 + // Do some post-update actions
305 + $this->post_update_item($source_id, $data, $source_type);
306 +
167 307 return true;
168 308 }
169 309 return false;
170 310 }
@@ -233,79 +373,217 @@
233 373 if(Utils::is_empty($item)) {
234 374 return false;
235 375 }
236 376
237 - if ($item['provider'] == $this->service) {
377 + if (
378 + $item['provider'] == $this->service &&
379 + $item['storage'] == $this->bucket_name &&
380 + $item['source_type'] == $source_type
381 + ) {
238 382 if(
239 383 ($check_rewrite && (isset($this->settings['rewrite_url']) && $this->settings['rewrite_url'])) ||
240 384 !$check_rewrite
241 385 ) {
386 + if ($check_rewrite) {
387 + return (bool) apply_filters('wpmcs_is_available_from_provider', true, $attachment_id, $source_type);
388 + }
242 389 return true;
243 - }
390 + }
244 391 }
245 392 return false;
246 393 }
247 394
248 395 /**
249 - * Get items by paths
396 + * Get items by source paths
397 + *
398 + * @param array|string $paths
399 + * @param bool $exact_match Use exact paths or greedy match
400 + * @param bool $first_only Return only the first matched item
401 + *
402 + * @return array
250 403 */
251 - public function get_items_by_source_paths( $paths = [] ) {
404 + public function get_items_by_paths( $paths, $exact_match = true, $first_only = false, $type = 'source' ) {
252 405 global $wpdb;
253 406
254 - $items = [];
407 + if ( ! is_array( $paths ) && is_string( $paths ) && ! empty( $paths ) ) {
408 + $paths = [ $paths ];
409 + }
255 410
256 - if (!Utils::is_empty($paths)) {
257 - // Ensure paths are properly sanitized and ready for the query
258 - $like_conditions = array();
259 - foreach ($paths as $path) {
260 - // Prepare the SQL conditions for exact match in source_path and partial match in extras column
261 - $like_conditions[] = $wpdb->prepare("(source_path = %s OR extra LIKE %s)", $path, '%' . $wpdb->esc_like($path) . '%');
262 - }
411 + if ( Utils::is_empty( $paths ) ) {
412 + return [];
413 + }
263 414
264 - // Combine the conditions with OR
265 - $where_clause = implode(' OR ', $like_conditions);
415 + /**
416 + * Field => Index mapping
417 + * field_name => index_name
418 + */
419 + switch ( $type ) {
420 + case 'key':
421 + $fields = [
422 + 'key' => 'uidx_key',
423 + 'original_key' => 'uidx_original_key',
424 + ];
425 + break;
266 426
267 - // Execute the SQL query to fetch source_id instead of source_path
268 - $item_table = Db::get_table_name();
269 - $sql = "SELECT DISTINCT source_id, source_type FROM " . $item_table . " WHERE " . $where_clause;
270 - $results = $wpdb->get_results($sql, ARRAY_A);
427 + default:
428 + $fields = [
429 + 'source_path' => 'uidx_source_path',
430 + 'original_source_path' => 'uidx_original_source_path',
431 + ];
432 + break;
433 + }
271 434
272 - if ($wpdb->last_error || Utils::is_empty($results)) {
273 - return [];
435 + if ( empty( $fields ) ) {
436 + return [];
437 + }
438 +
439 + // Normalize & deduplicate
440 + $paths = array_unique( $paths );
441 +
442 + $table = Db::get_table_name();
443 +
444 + // Build USE INDEX clause from field map
445 + $index_list = implode( ', ', array_values( $fields ) );
446 +
447 + $sql = "
448 + SELECT DISTINCT source_id, source_type
449 + FROM {$table} USE INDEX ({$index_list})
450 + WHERE provider = %s
451 + AND storage = %s
452 + ";
453 +
454 + $params = [
455 + $this->service,
456 + $this->bucket_name,
457 + ];
458 +
459 + // Optional region
460 + if ( ! empty( $this->region ) ) {
461 + $sql .= " AND region = %s";
462 + $params[] = $this->region;
463 + }
464 +
465 + /**
466 + * Path conditions
467 + */
468 + $conditions = [];
469 +
470 + if ( $exact_match ) {
471 + $placeholders = implode( ',', array_fill( 0, count( $paths ), '%s' ) );
472 +
473 + foreach ( array_keys( $fields ) as $column ) {
474 + $conditions[] = "`{$column}` IN ({$placeholders})";
475 + foreach ( $paths as $path ) {
476 + $params[] = $path;
477 + }
274 478 }
479 + } else {
480 + foreach ( $paths as $path ) {
481 + $ext = pathinfo( $path, PATHINFO_EXTENSION );
482 + $base = $ext
483 + ? substr_replace( $path, '%', -strlen( $ext ) - 1 )
484 + : $path . '%';
275 485
276 - $source_ids = array();
277 - foreach ($results as $row) {
278 - $item = $this->get((int)$row['source_id'], $row['source_type']);
279 - if(!Utils::is_empty($item)) {
280 - $items[] = $item;
486 + foreach ( array_keys( $fields ) as $column ) {
487 + $conditions[] = "`{$column}` LIKE %s";
488 + $params[] = $base;
281 489 }
282 490 }
283 - return $items;
284 491 }
492 +
493 + if ( ! empty( $conditions ) ) {
494 + $sql .= " AND ( " . implode( ' OR ', $conditions ) . " )";
495 + }
496 +
497 + // First only
498 + if ( $first_only ) {
499 + $sql .= " ORDER BY source_id ASC LIMIT 1";
500 + }
501 +
502 + $prepared = $wpdb->prepare( $sql, $params );
503 + $results = $wpdb->get_results( $prepared, ARRAY_A );
504 +
505 + if ( $wpdb->last_error || empty( $results ) ) {
506 + return [];
507 + }
508 +
509 + // Hydration
510 + if ( $first_only ) {
511 + return $this->get(
512 + (int) $results[0]['source_id'],
513 + $results[0]['source_type']
514 + );
515 + }
516 +
517 + $items = [];
518 +
519 + foreach ( $results as $row ) {
520 + $item = $this->get(
521 + (int) $row['source_id'],
522 + $row['source_type']
523 + );
524 +
525 + if ( ! Utils::is_empty( $item ) ) {
526 + $items[] = $item;
527 + }
528 + }
529 +
285 530 return $items;
286 531 }
287 532
288 533
289 534 /**
290 - * Get similar existing files like origin path
535 + * Get similar existing files by source path prefix
536 + * Searches in source_path and original_source_path
537 + *
291 538 * @since 1.0.0
292 539 */
293 - public function get_similar_files_by_path($path){
540 + public function get_similar_files_by_path( $path ) {
294 541 global $wpdb;
295 - if (isset($path) && !empty($path)) {
296 - $item_table = Db::get_table_name();
297 - $results = $wpdb->get_results("SELECT source_path FROM " . $item_table . " WHERE source_path LIKE '$path%'", ARRAY_A);
298 - if ($wpdb->last_error || null === $results || !(isset($results) && !empty($results))) {
299 - return false;
300 - }
301 - $source_paths = array();
302 - foreach ($results as $row) {
303 - $source_paths[] = $row['source_path'];
304 - }
305 - return $source_paths;
542 +
543 + if ( Utils::is_empty( $path ) ) {
544 + return false;
306 545 }
307 - return false;
546 +
547 + $table = Db::get_table_name();
548 + $like = $wpdb->esc_like( $path ) . '%';
549 +
550 + $base_where = "
551 + provider = %s
552 + AND storage = %s
553 + " . ( ! empty( $this->region ) ? "AND region = %s" : '' );
554 +
555 + $params = [ $this->service, $this->bucket_name ];
556 + if ( ! empty( $this->region ) ) {
557 + $params[] = $this->region;
558 + }
559 +
560 + $sql = "
561 + (
562 + SELECT source_path
563 + FROM {$table} USE INDEX (idx_source_path_provider)
564 + WHERE {$base_where}
565 + AND source_path LIKE %s
566 + )
567 + UNION DISTINCT
568 + (
569 + SELECT original_source_path AS source_path
570 + FROM {$table} USE INDEX (uidx_original_source_path)
571 + WHERE {$base_where}
572 + AND original_source_path LIKE %s
573 + )
574 + ";
575 +
576 + $params = array_merge( $params, [ $like ], $params, [ $like ] );
577 +
578 + $results = $wpdb->get_results(
579 + $wpdb->prepare( $sql, $params ),
580 + ARRAY_A
581 + );
582 +
583 + return ( $wpdb->last_error || empty( $results ) )
584 + ? false
585 + : array_column( $results, 'source_path' );
308 586 }
309 587
310 588
311 589 /**
@@ -314,25 +592,21 @@
314 592 * @param
315 593 */
316 594 public function get_url($source_id, $size = 'full', $source_type = 'media_library'){
317 595 if ($data = $this->get($source_id, $source_type)) {
318 - $extras = $this->get_extras($source_id, false, $source_type) ?: [];
319 - $key = '';
320 - $url = '';
596 + $key = '';
321 597 switch($size) {
322 598 case 'full':
323 599 $key = $data['key'];
324 - $url = $data['url'];
325 600 break;
326 601 case 'original':
327 - if(
328 - isset($extras) && !empty($extras) &&
329 - isset($extras['original']) && !empty($extras['original'])
330 - ) {
331 - $key = $extras['original']['key'];
332 - $url = $extras['original']['url'];
602 + if( isset($data['original_key']) && !empty($data['original_key']) ) {
603 + $key = $data['original_key'];
333 604 }
605 + break;
334 606 default:
607 + // Only named sizes need extras — skip fetching them for 'full'/'original'.
608 + $extras = $this->get_extras($source_id, false, $source_type) ?: [];
335 609 if(
336 610 isset($extras) && !empty($extras) &&
337 611 isset($extras['sizes']) && !empty($extras['sizes']) &&
338 612 isset($extras['sizes'][$size]) && !empty($extras['sizes'][$size])
@@ -337,34 +611,36 @@
337 611 isset($extras['sizes']) && !empty($extras['sizes']) &&
338 612 isset($extras['sizes'][$size]) && !empty($extras['sizes'][$size])
339 613 ) {
340 614 $key = $extras['sizes'][$size]['key'];
341 - $url = $extras['sizes'][$size]['url'];
342 615 }
343 616 }
344 617
345 618 if(!empty($key)){
346 - if (
347 - isset($this->settings['enable_presigned']) && $this->settings['enable_presigned'] &&
348 - isset($this->settings['presigned_expire']) && !empty($this->settings['presigned_expire'])
349 - ) {
350 - $preSignedUrl = Integration::get_meta( $source_id, 'presigned_url_'.$size, false, false, false, $source_type );
351 - if ($preSignedUrl === false) {
352 - $new_url = Service::instance()->get_presigned_url($key);
619 + if (isset($data['is_private']) && $data['is_private']) {
620 + $privateUrl = Integration::get_meta( $source_id, 'private_url_'.$size, false, false, false, $source_type );
621 + if ($privateUrl === false) {
622 + $new_url = Service::instance()->get_private_url($key);
353 623
354 624 if (!Utils::is_empty($new_url)) {
355 - $preSignedUrl = Cdn::may_generate_cdn_url($new_url, $key);
356 - $expireMinutes = (int)(isset($this->settings['presigned_expire']) && !empty($this->settings['presigned_expire']))
357 - ? $this->settings['presigned_expire']
625 + // No hook (Pro inactive, or the current delivery provider hasn't
626 + // implemented one) means passthrough — same URL, unmodified. Real
627 + // per-CDN rewriting (e.g. CloudFront signed URLs) is a Pro concern.
628 + $privateUrl = apply_filters( 'wpmcs_generate_private_url', $new_url, $key );
629 + $expireMinutes = (int)(isset($this->settings['private_url_expire']) && !empty($this->settings['private_url_expire']))
630 + ? $this->settings['private_url_expire']
358 631 : 20;
359 632 $expireSeconds = $expireMinutes * 60;
360 633
361 - Integration::update_meta($source_id, 'presigned_url_'.$size, $preSignedUrl, false, $expireSeconds, $source_type);
634 + Integration::update_meta($source_id, 'private_url_'.$size, $privateUrl, false, $expireSeconds, $source_type);
362 635 }
363 636 }
364 - return $preSignedUrl;
637 + return $privateUrl;
365 638 } else {
366 - return Cdn::may_generate_cdn_url($url, $key);
639 + $url = Service::instance()->get_url($key);
640 + if(!Utils::is_empty($url)) {
641 + return Cdn::may_generate_cdn_url($url, $key);
642 + }
367 643 }
368 644 }
369 645 }
370 646 return false;
@@ -370,132 +646,324 @@
370 646 return false;
371 647 }
372 648
373 649
374 - /**
375 - * Get service path of item from database
376 - * @since 1.0.0
377 - * @param
650 +
651 + /**
652 + * Move file to server, given item id and size
653 + * If $all is true, it will move all files to server
654 + * If $backup is true, it will move backup file to server
655 + * @param int $source_id source id of item
656 + * @param string $size size of the file, default is full
657 + * @param string $source_type source type of item, default is media_library
658 + * @param bool $all if true, it will move all files to server
659 + * @param bool $backup if true, it will move backup file to server
660 + * @param string $log_type error-log bucket to write to on failure — lets a caller other
661 + * than the "Restore to Server" job (e.g. "Remove from Cloud",
662 + * which also restores as a safety step) attribute failures to
663 + * its own error list instead of Restore to Server's.
664 + * @return array an array of server file paths
378 665 */
379 - public function moveToServer($source_id, $size = 'full', $all = false, $source_type = 'media_library') {
380 - $server_files = array();
666 + public function moveToServer($source_id, $size = 'full', $source_type = 'media_library', $all = false, $backup = false, $log_type = 'restore_to_server'){
667 + $server_files = [];
381 668 $server_file = false;
382 - $upload_dir = wp_get_upload_dir();
383 669 $source_id = (int)$source_id;
670 + $item = $this->get($source_id, $source_type);
384 671
385 - if ($data = $this->get($source_id, $source_type)) {
386 - $wpmcsService = Service::instance();
387 - if($all) {
388 - if (
389 - isset($data['source_path']) && !empty($data['source_path']) &&
390 - isset($data['key']) && !empty($data['key'])
391 - ) {
392 - $file_path = trailingslashit($upload_dir['basedir']) . $data['source_path'];
393 - if(file_exists($file_path) || $wpmcsService->object_to_server($data['key'], $file_path)) {
394 - $server_files['full'] = $file_path;
395 - }
672 + // Remove log if exists before move to server
673 + Logger::instance()->remove_log($log_type, $source_id, $source_type);
674 +
675 + if ( isset($item) && !empty($item) ) {
676 + $files = $this->moveToServerByItem($item, $size, $all, $log_type);
677 + if (isset($files) && !empty($files)) {
678 + $server_files = $all ? array_merge($server_files, $files) : $files;
679 + }
680 + }
681 + if( $all && $backup ) {
682 + $backupItem = $this->get_backup($source_id, $source_type);
683 + if (isset($backupItem) && !empty($backupItem)) {
684 + $files = $this->moveToServerByItem($backupItem, $size, $all, $log_type);
685 + if (isset($files) && !empty($files)) {
686 + $server_files['backup'] = $files;
396 687 }
397 - $extras = $this->get_extras($source_id, false, $source_type) ?: [];
398 - if (isset($extras['original']) && !empty($extras['original'])) {
399 - $original = $extras['original'];
400 - if(!empty($original)) {
401 - $original_file_path = trailingslashit($upload_dir['basedir']) . $original['source_path'];
402 - if(file_exists($original_file_path) || $wpmcsService->object_to_server($original['key'], $original_file_path)) {
403 - $server_files['original'] = $original_file_path;
404 - }
405 - }
688 + }
689 + }
690 + return $server_files;
691 + }
692 +
693 +
694 + /**
695 + * Copy back an item from the service to the server
696 + *
697 + * @since 1.0.0
698 + * @param array $item
699 + * @param string $size
700 + * @param bool $all
701 + * @param string $log_type error-log bucket to write to on failure
702 + * @return array|string
703 + */
704 + public function moveToServerByItem( $item = [], $size = 'full', $all = false, $log_type = 'restore_to_server' ) {
705 + $source_id = (int) ( $item['source_id'] ?? 0 );
706 + // Validate source ID
707 + if( $source_id <= 0 ) {
708 + return false;
709 + }
710 +
711 + $source_type = $item['source_type'] ?? 'media_library';
712 + $extras = ! empty( $item['extra'] ) ? Utils::maybe_unserialize( $item['extra'] ) : [];
713 +
714 + // Build file map once
715 + $files = [
716 + 'full' => [
717 + 'key' => $item['key'] ?? null,
718 + 'path' => $item['source_path'] ?? null,
719 + ],
720 + 'original' => [
721 + 'key' => $item['original_key'] ?? null,
722 + 'path' => $item['original_source_path'] ?? null,
723 + ],
724 + ];
725 +
726 + if ( ! empty( $extras['sizes'] ) ) {
727 + foreach ( $extras['sizes'] as $name => $data ) {
728 + $files[ $name ] = [
729 + 'key' => $data['key'] ?? null,
730 + 'path' => $data['source_path'] ?? null,
731 + ];
732 + }
733 + }
734 +
735 + // ALL files
736 + if ( $all ) {
737 + $results = [];
738 +
739 + foreach ( $files as $label => $data ) {
740 + if ( $file = $this->move_to_server_by_key_and_path(
741 + $data['key'],
742 + $data['path'],
743 + $source_id,
744 + $source_type,
745 + $log_type
746 + ) ) {
747 + $results[ $label ] = $file;
406 748 }
407 - if (isset($extras['sizes']) && !empty($extras['sizes'])) {
408 - $sizes = $extras['sizes'];
409 - foreach($sizes as $sub_size => $sub_file) {
410 - if(!empty($sub_file)) {
411 - $sub_file_path = trailingslashit($upload_dir['basedir']) . $sub_file['source_path'];
412 - if(file_exists($sub_file_path) || $wpmcsService->object_to_server($sub_file['key'], $sub_file_path)) {
413 - $server_files[$sub_size] = $sub_file_path;
414 - }
415 - }
416 - }
417 - }
418 - return !empty($server_files) ? $server_files : false;
419 - } else {
420 - if($size === 'full') {
421 - if (
422 - isset($data['source_path']) && !empty($data['source_path']) &&
423 - isset($data['key']) && !empty($data['key'])
424 - ) {
425 - $file_path = trailingslashit($upload_dir['basedir']) . $data['source_path'];
426 - if(file_exists($file_path) || $wpmcsService->object_to_server($data['key'], $file_path)) {
427 - $server_file = $file_path;
428 - }
429 - }
430 - } else if($size === 'original') {
431 - $extras = $this->get_extras($source_id, false, $source_type) ?: [];
432 - if (isset($extras['original']) && !empty($extras['original'])) {
433 - $original = $extras['original'];
434 - if(!empty($original)) {
435 - $original_file_path = trailingslashit($upload_dir['basedir']) . $original['source_path'];
436 - if(file_exists($original_file_path) || $wpmcsService->object_to_server($original['key'], $original_file_path)) {
437 - $server_file = $original_file_path;
438 - }
439 - }
440 - }
441 - } else {
442 - $extras = $this->get_extras($source_id, false, $source_type) ?: [];
443 - if (isset($extras['sizes']) && !empty($extras['sizes'])) {
444 - $sizes = $extras['sizes'];
445 - if(isset($sizes[$size]) && !empty($sizes[$size])) {
446 - $sub_file_path = trailingslashit($upload_dir['basedir']) . $sizes[$size]['source_path'];
447 - if(file_exists($sub_file_path) || $wpmcsService->object_to_server($sizes[$size]['key'], $sub_file_path)) {
448 - $server_file = $sub_file_path;
449 - }
450 - }
451 - }
452 - }
453 - return $server_file;
454 749 }
750 +
751 + return $results;
455 752 }
753 +
754 + // SINGLE file
755 + if ( isset( $files[ $size ] ) ) {
756 + return $this->move_to_server_by_key_and_path(
757 + $files[ $size ]['key'],
758 + $files[ $size ]['path'],
759 + $source_id,
760 + $source_type,
761 + $log_type
762 + );
763 + }
764 +
456 765 return false;
457 766 }
458 -
459 767
460 768 /**
769 + * Whether a moveToServer(..., $all=true, $backup=true) result actually restored
770 + * everything this item is expected to have (every size, the original if present, and
771 + * the backup entry if one exists) — moveToServer()'s return silently drops any single
772 + * file that failed, so a plain non-empty check on it isn't enough to safely delete the
773 + * cloud copies afterward.
774 + * @since 1.4.1
775 + */
776 + public function verify_full_restore( $source_id, $source_type, $moved ) {
777 + $row = $this->get( $source_id, $source_type );
778 + if ( empty( $row ) ) {
779 + return false;
780 + }
781 +
782 + $expected = $this->expected_restore_labels( $row );
783 + $restored = array_diff( array_keys( (array) $moved ), [ 'backup' ] );
784 + if ( ! empty( array_diff( $expected, $restored ) ) ) {
785 + return false;
786 + }
787 +
788 + $backup_item = $this->get_backup( $source_id, $source_type );
789 + if ( empty( $backup_item ) ) {
790 + return true;
791 + }
792 +
793 + $expected_backup = $this->expected_restore_labels( $backup_item );
794 + $restored_backup = ! empty( $moved['backup'] ) ? array_keys( $moved['backup'] ) : [];
795 + return empty( array_diff( $expected_backup, $restored_backup ) );
796 + }
797 +
798 + /**
799 + * File labels (full, original, each named size) a given item row is expected to have.
800 + */
801 + private function expected_restore_labels( $item_row ) {
802 + $expected = [ 'full' ];
803 + if ( ! empty( $item_row['original_key'] ) || ! empty( $item_row['original_source_path'] ) ) {
804 + $expected[] = 'original';
805 + }
806 +
807 + $extras = ! empty( $item_row['extra'] ) ? Utils::maybe_unserialize( $item_row['extra'] ) : [];
808 + if ( ! empty( $extras['sizes'] ) ) {
809 + $expected = array_merge( $expected, array_keys( $extras['sizes'] ) );
810 + }
811 +
812 + return $expected;
813 + }
814 +
815 +
816 +
817 + /**
461 818 * Get service path of item from database by source url
462 819 * @since 1.0.0
463 - * @param
820 + * @param int $source_id
821 + * @param string $file
822 + * @param string $source_type
823 + * @return bool
464 824 */
465 - public function moveToServerBySourcePath($source_id, $file, $source_type = 'media_library'){
466 - $server_files = array();
467 - $server_file = false;
468 - $upload_dir = wp_get_upload_dir();
469 - $source_id = (int)$source_id;
825 + public function moveToServerBySourcePath( $source_id, $file, $source_type = 'media_library' ) {
826 + $source_id = (int) $source_id;
470 827
471 - if ($data = $this->get($source_id, $source_type)) {
472 - $source_path = Utils::get_attachment_source_path($file);
828 + $item = $this->get( $source_id, $source_type );
829 + if ( Utils::is_empty( $item ) ) {
830 + return false;
831 + }
473 832
474 - if( isset($data['source_path']) && !empty($data['source_path']) && $data['source_path'] == $source_path ) {
475 - $file_path = trailingslashit($upload_dir['basedir']) . $data['source_path'];
476 - if(file_exists($file_path) || Service::instance()->object_to_server($data['key'], $file_path)) {
833 + $source_path = Utils::get_attachment_source_path( $file );
834 + if ( empty( $source_path ) ) {
835 + return false;
836 + }
837 +
838 + // 1. Check main file
839 + if (
840 + isset( $item['source_path'] ) &&
841 + ! empty( $item['source_path'] ) &&
842 + $item['source_path'] === $source_path &&
843 + $this->move_to_server_by_key_and_path(
844 + $item['key'] ?? null,
845 + $item['source_path'],
846 + $source_id,
847 + $source_type
848 + )
849 + ) {
850 + return true;
851 + }
852 +
853 +
854 + // 2. Check original
855 + if (
856 + isset( $item['original_source_path'] ) && ! empty( $item['original_source_path'] ) &&
857 + $item['original_source_path'] === $source_path &&
858 + $this->move_to_server_by_key_and_path(
859 + $item['original_key'] ?? null,
860 + $item['original_source_path'],
861 + $source_id,
862 + $source_type
863 + )
864 + ) {
865 + return true;
866 + }
867 +
868 + $extras = $this->get_extras( $source_id, false, $source_type ) ?: [];
869 +
870 + // 3. Check sizes
871 + if ( ! empty( $extras['sizes'] ) ) {
872 + foreach ( $extras['sizes'] as $size ) {
873 + if (
874 + isset( $size['source_path'] ) &&
875 + ! empty( $size['source_path'] ) &&
876 + $size['source_path'] === $source_path &&
877 + $this->move_to_server_by_key_and_path(
878 + $size['key'] ?? null,
879 + $size['source_path'],
880 + $source_id,
881 + $source_type
882 + )
883 + ) {
477 884 return true;
478 885 }
479 - } else {
480 - $sizes = $this->get_extras($source_id, 'sizes', $source_type) ?: [];
481 - foreach($sizes as $sub_size => $sub_file) {
482 - if(!empty($sub_file) && $sub_file['source_path'] == $source_path) {
483 - $sub_file_path = trailingslashit($upload_dir['basedir']) . $sub_file['source_path'];
484 - if(file_exists($sub_file_path) || Service::instance()->object_to_server($sub_file['key'], $sub_file_path)) {
485 - return true;
486 - }
487 - }
488 - }
489 886 }
490 887 }
888 +
491 889 return false;
492 890 }
493 891
494 892 /**
893 + * Copy back a file from the service to the server
894 + *
895 + * @param string $key
896 + * @param string $relative_path
897 + * @param int $source_id
898 + * @param string $source_type
899 + * @param string $log_type error-log bucket to write to on failure
900 + *
901 + * @return string|false
902 + */
903 + protected function move_to_server_by_key_and_path( $key, $relative_path, $source_id = 0, $source_type = 'media_library', $log_type = 'restore_to_server' ) {
904 + if ( empty( $key ) || empty( $relative_path ) ) {
905 + return false;
906 + }
907 +
908 + $upload_dir = wp_get_upload_dir();
909 + $file = trailingslashit( $upload_dir['basedir'] ) . $relative_path;
910 +
911 + if ( file_exists( $file ) ) {
912 + return $file;
913 + }
914 +
915 + // Checked on disk rather than trusting the return value alone — at least one
916 + // provider (Cloudflare R2) has been observed writing the file successfully while
917 + // still reporting failure (an SDK-level error thrown after the save completes).
918 + Service::instance()->object_to_server( $key, $file );
919 +
920 + if ( file_exists( $file ) ) {
921 + return $file;
922 + }
923 +
924 + Logger::instance()->add_log( $log_type, $source_id, $source_type, [
925 + 'message' => __( 'The file could not be copied to the server. Please try again.', 'media-cloud-sync' ),
926 + 'file' => $key,
927 + 'code' => 404,
928 + ] );
929 +
930 + return false;
931 + }
932 +
933 +
934 + /**
935 + * Move original file to server
936 + *
937 + * @param int $source_id
938 + * @param string $source_type
939 + * @return bool
940 + */
941 + public function moveOriginalToServer($source_id, $source_type = 'media_library') {
942 + $data = $this->get($source_id, $source_type);
943 + if ($data) {
944 + $size = 'full';
945 + if (
946 + isset($data['original_source_path']) && !empty($data['original_source_path']) &&
947 + isset($data['original_key']) && !empty($data['original_key'])
948 + ) {
949 + $size = 'original';
950 + }
951 + return $this->moveToServer($source_id, $size, $source_type);
952 + }
953 + return false;
954 + }
955 +
956 +
957 + /**
495 958 * Delete media item
496 959 */
497 960 public function delete_attachments_by_item($item, $delete_backup = true) {
961 + // Lets an integration veto the delete when another row still relies on the same key.
962 + if (!apply_filters('wpmcs_should_delete_cloud_files', true, $item)) {
963 + return;
964 + }
965 +
498 966 $upload_dir = wp_get_upload_dir();
499 967
500 968 if (isset($item['extra']) && !empty($item['extra'])) {
501 969 $extras = Utils::maybe_unserialize($item['extra']);
@@ -511,15 +979,8 @@
511 979 }
512 980
513 981 if (
514 982 isset($extras) && !empty($extras) &&
515 - isset($extras['original']) && !empty($extras['original'])
516 - ) {
517 - Service::instance()->deleteSingle($extras['original']['key']);
518 - }
519 -
520 - if (
521 - isset($extras) && !empty($extras) &&
522 983 isset($extras['backup']) && !empty($extras['backup']) &&
523 984 $delete_backup
524 985 ) {
525 986 $backup = Utils::maybe_unserialize($extras['backup']);
@@ -527,17 +988,175 @@
527 988 $this->delete_attachments_by_item($backup, false);
528 989 }
529 990 }
530 991 }
992 +
993 + if (
994 + isset($item['original_key']) && !empty($item['original_key'])
995 + ) {
996 + Service::instance()->deleteSingle($item['original_key']);
997 + }
998 +
531 999 if (isset($item['key']) && !empty($item['key'])) {
532 1000 Service::instance()->deleteSingle($item['key']);
533 1001 }
534 1002 }
535 1003
536 - /**
1004 +
1005 + /**
1006 + * Delete Cloud Files by Keys
1007 + * @since 1.3.6
1008 + */
1009 + public function delete_cloud_files_by_keys( $keys = [] ) {
1010 + if (Utils::is_empty($keys) || !is_array($keys)) {
1011 + return false;
1012 + }
1013 +
1014 + foreach ($keys as $key) {
1015 + Service::instance()->deleteSingle( $key );
1016 + }
1017 + return true;
1018 + }
1019 +
1020 +
1021 +
1022 + /**
1023 + * Pre-update item actions
1024 + * @since 1.2.13
1025 + * @param int $source_id
1026 + * @param array $data
1027 + * @param string $source_type
1028 + */
1029 + public function pre_update_item($source_id, $new_item, $old_item = [], $source_type = 'media_library') {
1030 + // Hook for pre-update actions
1031 + do_action('wpmcs_pre_update_item', $source_id, $new_item, $old_item, $source_type);
1032 +
1033 + // Additional filter to modify files to be removed from server if needed
1034 + $files_to_remove = apply_filters('wpmcs_pre_update_item_additional_files_to_remove_from_server', [], $source_id, $new_item, $old_item, $source_type);
1035 +
1036 + // Delete files if any
1037 + if (!Utils::is_empty($files_to_remove)) {
1038 + $this->may_be_delete_server_files_by_source_paths($files_to_remove);
1039 + }
1040 + }
1041 +
1042 +
1043 + /**
1044 + * Post-update item actions
1045 + * @since 1.2.13
1046 + * @param int $source_id
1047 + * @param array $data
1048 + * @param string $source_type
1049 + * This function is called after an item has been updated in the database.
1050 + * It triggers a WordPress action hook 'wpmcs_post_update_item' to allow other functions to hook into this event.
1051 + * After that, it calls may_be_delete_server_files_by_id to potentially delete server files associated with the item.
1052 + *
1053 + * @example
1054 + * $item = Item::instance();
1055 + * $item->post_update_item(123, $data, 'media_library');
1056 + *
1057 + * This example will trigger the post-update actions for the item with ID 123.
1058 + * It will execute any functions hooked to 'wpmcs_post_update_item' and may delete server files if the settings allow it.
1059 + */
1060 + public function post_update_item($source_id, $data, $source_type = 'media_library') {
1061 + // Hook for post-update actions
1062 + do_action('wpmcs_post_update_item', $source_id, $data, $source_type);
1063 +
1064 + // May be delete server files
1065 + $this->may_be_delete_server_files_by_id($source_id, $source_type, true, true);
1066 + }
1067 +
1068 + /**
1069 + * Track paths restored from cloud (by any integration) so they get removed
1070 + * again later, honoring "Remove from server" the way the normal sync pipeline
1071 + * would. Fed into the pre-update pipeline if a save happens this request (fast
1072 + * path — matches how the item's own pending removals already work), with a
1073 + * shutdown fallback (priority 1, ahead of most other plugins' shutdown hooks)
1074 + * for requests where nothing ever triggers a save.
1075 + *
1076 + * @param string[] $paths Absolute paths of the restored files.
1077 + * @return void
1078 + * @since 1.4.0
1079 + */
1080 + public function track_restored_for_cleanup(array $paths) {
1081 + foreach ($paths as $path) {
1082 + if (!in_array($path, $this->pending_restored_files, true)) {
1083 + $this->pending_restored_files[] = $path;
1084 + }
1085 + }
1086 +
1087 + if ($this->pending_cleanup_hooked) {
1088 + return;
1089 + }
1090 + $this->pending_cleanup_hooked = true;
1091 +
1092 + add_filter('wpmcs_pre_update_item_additional_files_to_remove_from_server', function ($files_to_remove) {
1093 + $files_to_remove = array_merge((array) $files_to_remove, $this->pending_restored_files);
1094 + $this->pending_restored_files = [];
1095 + return $files_to_remove;
1096 + });
1097 +
1098 + add_action('shutdown', array($this, 'flush_pending_restored_files'), 1);
1099 + }
1100 +
1101 + /**
1102 + * Shutdown fallback for track_restored_for_cleanup() — removes anything the
1103 + * pre-update pipeline didn't already pick up this request.
1104 + *
1105 + * @return void
1106 + * @since 1.4.0
1107 + */
1108 + public function flush_pending_restored_files() {
1109 + if (empty($this->pending_restored_files)) {
1110 + return;
1111 + }
1112 + $this->may_be_delete_server_files_by_source_paths($this->pending_restored_files);
1113 + $this->pending_restored_files = [];
1114 + }
1115 +
1116 +
1117 + public function may_be_delete_server_files_by_source_paths($source_paths) {
1118 + if (Utils::is_empty($source_paths) || !is_array($source_paths)) {
1119 + return false;
1120 + }
1121 +
1122 + if( !( isset($this->settings['remove_from_server']) && $this->settings['remove_from_server'] ) ) {
1123 + return false;
1124 + }
1125 +
1126 + foreach ($source_paths as $path) {
1127 + if(file_exists($path)) {
1128 + wp_delete_file($path, true);
1129 + }
1130 + }
1131 + return true;
1132 + }
1133 +
1134 + /**
537 1135 * Function to remove media from server by id
538 - * @since 1.0.0
539 - *
1136 + * @param int $attachment_id
1137 + * @param string $source_type
1138 + * @param bool $delete_main_file
1139 + * @param bool $delete_backup
1140 + *
1141 + * This function checks if the item exists and if the setting to remove from server is enabled.
1142 + * If so, it deletes the main file and any backup files associated with the item.
1143 + * It also checks if the item has any extra data, and if so, it attempts to delete the backup files if specified.
1144 + * Finally, it deletes the main file associated with the item.
1145 + *
1146 + * @since 1.2.13
1147 + * @return bool Returns true if the deletion process was initiated, false otherwise.
1148 + *
1149 + * @throws \Exception If the item does not exist or if the removal from server setting is not enabled.
1150 + *
1151 + * @example
1152 + * $item = Item::instance();
1153 + * $item->may_be_delete_server_files_by_id(123, 'media_library', true, true);
1154 + *
1155 + * This example will attempt to delete the server files for the attachment with ID 123,
1156 + * including the main file and any backup files, if the settings allow it.
1157 + *
1158 + * @see Item::may_be_delete_server_files_by_item() for the function that actually performs the deletion.
540 1159 */
541 1160 public function may_be_delete_server_files_by_id($attachment_id, $source_type = 'media_library', $delete_main_file=false, $delete_backup = false) {
542 1161 $item = $this->get($attachment_id, $source_type);
543 1162 if(Utils::is_empty($item)) {
@@ -561,9 +1180,9 @@
561 1180 }
562 1181 }
563 1182 }
564 1183
565 - $this->may_be_delete_server_files_by_item($item, $delete_main_file, $delete_backup);
1184 + $this->may_be_delete_server_files_by_item($item, $delete_main_file);
566 1185
567 1186 return true;
568 1187 }
569 1188
@@ -578,10 +1197,19 @@
578 1197 if( !( isset($this->settings['remove_from_server']) && $this->settings['remove_from_server'] ) ) {
579 1198 return false;
580 1199 }
581 1200
1201 + return $this->delete_server_files_by_item( $item, $delete_main_file );
1202 + }
1203 +
1204 +
1205 + /**
1206 + * Function to remove media from server by item
1207 + */
1208 + public function delete_server_files_by_item( $item, $delete_main_file=false ) {
582 1209 $upload_dir = wp_get_upload_dir();
583 1210 $has_original = false;
1211 + $files_to_remove = array();
584 1212
585 1213 $file_path = trailingslashit($upload_dir['basedir']) . $item['source_path'];
586 1214
587 1215 if (isset($item['extra']) && !empty($item['extra'])) {
@@ -593,33 +1221,40 @@
593 1221 foreach ($extras['sizes'] as $sub_image) {
594 1222 if (isset($sub_image['source_path']) && !empty($sub_image['source_path'])) {
595 1223 $file = trailingslashit($upload_dir['basedir']) . $sub_image['source_path'];
596 1224 if(file_exists($file)) {
597 - wp_delete_file($file);
1225 + $files_to_remove[] = $file;
598 1226 }
599 1227 }
600 1228 }
601 1229 }
602 -
603 - if (
604 - isset($extras) && !empty($extras) &&
605 - isset($extras['original']) && !empty($extras['original'])
606 - ) {
607 - $has_original = true;
608 - $file = trailingslashit($upload_dir['basedir']).$extras['original']['source_path'];
609 - if(file_exists($file) && $delete_main_file) {
610 - wp_delete_file($file);
611 - }
1230 + }
1231 + if (
1232 + isset($item['original_source_path']) && !empty($item['original_source_path'])
1233 + ) {
1234 + $has_original = true;
1235 + $file = trailingslashit($upload_dir['basedir']).$item['original_source_path'];
1236 + if(file_exists($file) && $delete_main_file) {
1237 + $files_to_remove[] = $file;
612 1238 }
613 1239 }
614 1240 if(file_exists($file_path)) {
615 1241 if ($has_original || (!$has_original && $delete_main_file)) {
616 - wp_delete_file($file_path);
1242 + $files_to_remove[] = $file_path;
617 1243 }
618 1244 }
1245 +
1246 +
1247 + $files_to_remove = apply_filters('wpmcs_files_to_remove_from_server', array_unique($files_to_remove), $item['source_id'], $item);
1248 +
1249 + if (!Utils::is_empty($files_to_remove)) {
1250 + foreach ($files_to_remove as $file) {
1251 + wp_delete_file($file, true);
1252 + }
1253 + }
1254 +
619 1255 return true;
620 1256 }
621 -
622 1257
623 1258 /**
624 1259 * Ensures only one instance of Class is loaded or can be loaded.
625 1260 *