PluginProbe
Media Cloud Sync / trunk
Media Cloud Sync vtrunk
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 1.3.0 All 34 releases
media-cloud-sync / includes / base / item.php

item.php in Media Cloud Sync trunk, at includes/base/item.php

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