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 +993 -195 1.2.101.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,35 +69,75 @@
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 - 'provider' => $this->service,
77 - 'region' => $this->region,
78 - 'storage' => $this->bucket_name,
79 - 'source_id' => $source_id,
80 - 'source_path' => $source_path,
81 - 'source_type' => $source_type,
82 - 'url' => $url,
83 - 'key' => $key,
84 - 'is_private' => $is_private,
85 - 'extra' => maybe_serialize($meta),
82 + 'provider' => $this->service,
83 + 'region' => $this->region,
84 + 'storage' => $this->bucket_name,
85 + 'source_id' => $source_id,
86 + 'source_path' => $source_path,
87 + 'source_type' => $source_type,
88 + 'url' => $url,
89 + 'key' => $key,
90 + 'original_source_path' => $original_source_path,
91 + 'original_key' => $original_key,
92 + 'is_private' => $is_private,
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 - Utils::update_meta($source_id, 'item', $data);
91 - Cache::update_item_cache($source_id.'_item', $data);
92 - Counter::add( 'uploaded' );
128 + Integration::update_meta($source_id, 'item', $data, false, false, $source_type);
129 + Cache::update_item_cache($source_id.'_item_'.$source_type, $data);
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
@@ -100,27 +145,47 @@
100 145 * Get Item From Db
101 146 */
102 147 public function get($source_id, $source_type = 'media_library') {
103 148 global $wpdb;
104 - $item = Cache::get_item_cache($source_id.'_item', false);
149 + $item = Cache::get_item_cache($source_id.'_item_'.$source_type, false);
105 150 if($item === false) {
106 - $item = Utils::get_meta($source_id, 'item', false);
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 - Cache::update_item_cache($source_id.'_item', '');
170 + Cache::update_item_cache($source_id.'_item_'.$source_type, '');
113 171 return false;
114 172 }
115 173
116 - Utils::update_meta($source_id, 'item', $item);
174 + Integration::update_meta($source_id, 'item', $item, false, false, $source_type);
117 175 }
118 -
119 - Cache::update_item_cache($source_id.'_item', $item == false ? '' : $item);
176 + // Update cache
177 + Cache::update_item_cache($source_id.'_item_'.$source_type, $item == false ? '' : $item);
120 178 }
121 179
122 - return $item;
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
@@ -125,18 +190,35 @@
125 190 /**
126 191 * Function to delete item from data base
127 192 * @since 1.0.0
128 193 */
129 - public function delete($source_id){
194 + public function delete($source_id, $source_type = 'media_library'){
130 195 global $wpdb;
131 - if (isset($source_id) && !empty($source_id)) {
132 - $rows = $wpdb->delete(Db::get_table_name(), array('source_id' => $source_id));
196 + if (isset($source_id) && !Utils::is_empty($source_id)) {
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 - Utils::delete_meta($source_id, 'item');
137 - Cache::delete_item_cache($source_id.'_item');
138 - Counter::remove( 'uploaded' );
214 + Integration::delete_meta($source_id, 'item', false, $source_type);
215 + Cache::delete_item_cache($source_id.'_item_'.$source_type);
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,27 +226,85 @@
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){
236 + public function update($source_id, $data, $source_type = 'media_library', $target_identity = null) {
150 237 global $wpdb;
151 - if (isset($source_id) && !empty($source_id)) {
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));
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
162 - Utils::delete_meta($source_id, 'item');
163 - Cache::delete_item_cache($source_id.'_item');
295 + Integration::delete_meta($source_id, 'item', false, $source_type);
296 + Cache::delete_item_cache($source_id.'_item_'.$source_type);
164 297
165 - // Reset cache
166 - $current_data = $this->get($source_id);
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 }
@@ -173,19 +313,19 @@
173 313 * Get extra values of item from database
174 314 * @since 1.0.0
175 315 * @param
176 316 */
177 - public function get_extras($source_id, $field = false){
178 - $data = $this->get($source_id);
317 + public function get_extras($source_id, $field = false, $source_type = 'media_library'){
318 + $data = $this->get($source_id, $source_type);
179 319 if ($data) {
180 320 if (isset($data['extra']) && !empty($data['extra'])) {
181 321
182 322 if(!Utils::is_empty($field)) {
183 - $extras = unserialize($data['extra']);
323 + $extras = Utils::maybe_unserialize($data['extra']);
184 324 return isset($extras[$field]) && !empty($extras[$field]) ? $extras[$field] : false;
185 325 }
186 326
187 - return unserialize($data['extra']);
327 + return Utils::maybe_unserialize($data['extra']);
188 328 }
189 329 }
190 330 return false;
191 331 }
@@ -195,10 +335,10 @@
195 335 * Get column of item from database
196 336 * @since 1.0.0
197 337 * @param
198 338 */
199 - public function get_field($source_id, $field){
200 - $data = $this->get($source_id);
339 + public function get_field($source_id, $field, $source_type = 'media_library'){
340 + $data = $this->get($source_id, $source_type);
201 341 if ($data) {
202 342 if (isset($data[$field]) && !empty($data[$field])) {
203 343 return $data[$field];
204 344 }
@@ -211,13 +351,13 @@
211 351 * Get backup values of item from database
212 352 * @since 1.0.0
213 353 * @param
214 354 */
215 - public function get_backup($source_id){
216 - $data = $this->get_extras($source_id);
355 + public function get_backup($source_id, $source_type = 'media_library'){
356 + $data = $this->get_extras($source_id, false, $source_type);
217 357 if ($data) {
218 358 if (isset($data['backup']) && !empty($data['backup'])) {
219 - return unserialize($data['backup']);
359 + return Utils::maybe_unserialize($data['backup']);
220 360 }
221 361 }
222 362 return false;
223 363 }
@@ -227,85 +367,223 @@
227 367 * Checking Item that is served by provider And Is rewrite URL is enabled
228 368 * @since 1.0.0
229 369 * @param
230 370 */
231 - public function is_available_from_provider($attachment_id, $check_rewrite = true){
232 - $item = $this->get($attachment_id);
371 + public function is_available_from_provider($attachment_id, $check_rewrite = true, $source_type = 'media_library') {
372 + $item = $this->get($attachment_id, $source_type);
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 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']);
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 /**
@@ -312,27 +590,23 @@
312 590 * Get service url of item from database
313 591 * @since 1.0.0
314 592 * @param
315 593 */
316 - public function get_url($source_id, $size = 'full'){
317 - if ($data = $this->get($source_id)) {
318 - $extras = $this->get_extras($source_id);
319 - $key = '';
320 - $url = '';
594 + public function get_url($source_id, $size = 'full', $source_type = 'media_library'){
595 + if ($data = $this->get($source_id, $source_type)) {
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 = Utils::get_meta( $source_id, 'presigned_url_'.$size );
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 - Utils::update_meta($source_id, 'presigned_url_'.$size, $preSignedUrl, false, $expireSeconds);
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,93 +646,615 @@
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){
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)) {
386 - $wpmcsService = Service::instance();
387 - if($all) {
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;
687 + }
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;
748 + }
749 + }
750 +
751 + return $results;
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 +
765 + return false;
766 + }
767 +
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 + /**
818 + * Get service path of item from database by source url
819 + * @since 1.0.0
820 + * @param int $source_id
821 + * @param string $file
822 + * @param string $source_type
823 + * @return bool
824 + */
825 + public function moveToServerBySourcePath( $source_id, $file, $source_type = 'media_library' ) {
826 + $source_id = (int) $source_id;
827 +
828 + $item = $this->get( $source_id, $source_type );
829 + if ( Utils::is_empty( $item ) ) {
830 + return false;
831 + }
832 +
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 ) {
388 873 if (
389 - isset($data['source_path']) && !empty($data['source_path']) &&
390 - isset($data['key']) && !empty($data['key'])
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 + )
391 883 ) {
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;
884 + return true;
885 + }
886 + }
887 + }
888 +
889 + return false;
890 + }
891 +
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 + /**
958 + * Delete media item
959 + */
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 +
966 + $upload_dir = wp_get_upload_dir();
967 +
968 + if (isset($item['extra']) && !empty($item['extra'])) {
969 + $extras = Utils::maybe_unserialize($item['extra']);
970 + if (
971 + isset($extras) && !empty($extras) &&
972 + isset($extras['sizes']) && !empty($extras['sizes'])
973 + ) {
974 + foreach ($extras['sizes'] as $sub_image) {
975 + if (isset($sub_image['key']) && !empty($sub_image['key'])) {
976 + Service::instance()->deleteSingle($sub_image['key']);
395 977 }
396 978 }
397 - $extras = $this->get_extras($source_id, false) ?: [];
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 - }
979 + }
980 +
981 + if (
982 + isset($extras) && !empty($extras) &&
983 + isset($extras['backup']) && !empty($extras['backup']) &&
984 + $delete_backup
985 + ) {
986 + $backup = Utils::maybe_unserialize($extras['backup']);
987 + if (isset($backup) && !empty($backup)) {
988 + $this->delete_attachments_by_item($backup, false);
406 989 }
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 - }
990 + }
991 + }
992 +
993 + if (
994 + isset($item['original_key']) && !empty($item['original_key'])
995 + ) {
996 + Service::instance()->deleteSingle($item['original_key']);
997 + }
998 +
999 + if (isset($item['key']) && !empty($item['key'])) {
1000 + Service::instance()->deleteSingle($item['key']);
1001 + }
1002 + }
1003 +
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 + /**
1135 + * Function to remove media from server by id
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.
1159 + */
1160 + public function may_be_delete_server_files_by_id($attachment_id, $source_type = 'media_library', $delete_main_file=false, $delete_backup = false) {
1161 + $item = $this->get($attachment_id, $source_type);
1162 + if(Utils::is_empty($item)) {
1163 + return false;
1164 + }
1165 +
1166 + if( !( isset($this->settings['remove_from_server']) && $this->settings['remove_from_server'] ) ) {
1167 + return false;
1168 + }
1169 +
1170 + if (isset($item['extra']) && !empty($item['extra'])) {
1171 + $extras = Utils::maybe_unserialize($item['extra']);
1172 + if (
1173 + isset($extras) && !empty($extras) &&
1174 + isset($extras['backup']) && !empty($extras['backup']) &&
1175 + $delete_backup
1176 + ) {
1177 + $backup = Utils::maybe_unserialize($extras['backup']);
1178 + if (isset($backup) && !empty($backup)) {
1179 + $this->may_be_delete_server_files_by_item($backup, $delete_main_file);
1180 + }
1181 + }
1182 + }
1183 +
1184 + $this->may_be_delete_server_files_by_item($item, $delete_main_file);
1185 +
1186 + return true;
1187 + }
1188 +
1189 + /**
1190 + * Function to remove media from server by item
1191 + */
1192 + public function may_be_delete_server_files_by_item( $item, $delete_main_file=false ) {
1193 + if(Utils::is_empty($item)) {
1194 + return false;
1195 + }
1196 +
1197 + if( !( isset($this->settings['remove_from_server']) && $this->settings['remove_from_server'] ) ) {
1198 + return false;
1199 + }
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 ) {
1209 + $upload_dir = wp_get_upload_dir();
1210 + $has_original = false;
1211 + $files_to_remove = array();
1212 +
1213 + $file_path = trailingslashit($upload_dir['basedir']) . $item['source_path'];
1214 +
1215 + if (isset($item['extra']) && !empty($item['extra'])) {
1216 + $extras = Utils::maybe_unserialize($item['extra']);
1217 + if (
1218 + isset($extras) && !empty($extras) &&
1219 + isset($extras['sizes']) && !empty($extras['sizes'])
1220 + ) {
1221 + foreach ($extras['sizes'] as $sub_image) {
1222 + if (isset($sub_image['source_path']) && !empty($sub_image['source_path'])) {
1223 + $file = trailingslashit($upload_dir['basedir']) . $sub_image['source_path'];
1224 + if(file_exists($file)) {
1225 + $files_to_remove[] = $file;
415 1226 }
416 1227 }
417 1228 }
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) ? $this->get_extras($source_id) : [];
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) ? $this->get_extras($source_id) : [];
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 1229 }
455 1230 }
456 - return false;
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;
1238 + }
1239 + }
1240 + if(file_exists($file_path)) {
1241 + if ($has_original || (!$has_original && $delete_main_file)) {
1242 + $files_to_remove[] = $file_path;
1243 + }
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 +
1255 + return true;
457 1256 }
458 -
459 1257
460 1258 /**
461 1259 * Ensures only one instance of Class is loaded or can be loaded.
462 1260 *