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/integrations/media-library.php +949 -250 1.2.131.4.1 View file →
@@ -2,8 +2,18 @@
2 2 namespace Dudlewebs\WPMCS;
3 3
4 4 defined('ABSPATH') || exit;
5 5
6 +use Exception;
7 +use WP_Error;
8 +
9 +/**
10 + * Media Library integration.
11 + *
12 + * Loaded by the autoloader; activation is gated via `is_installed()` in Integration::init().
13 + *
14 + * @since 1.0.0
15 + */
6 16 class MediaLibrary {
7 17 private static $instance = null;
8 18 private $assets_url;
9 19 private $version;
@@ -8,15 +18,26 @@
8 18 private $assets_url;
9 19 private $version;
10 20 private $token;
11 21
22 + protected $config;
23 + protected $bucketConfig;
12 24 protected $settings;
25 + protected $credentials;
26 + protected $service;
27 +
28 + protected $bucket_name;
29 + protected $region = '';
30 +
13 31 private $deleting_attachment = false;
14 32
15 33
16 34 public static $source_type_prefix = "media";
17 - public static $label = 'Media Library';
18 35
36 + public static function get_label() {
37 + return __('Media Library', 'media-cloud-sync');
38 + }
39 +
19 40 // Map which meta to be updated
20 41 public static $source_types = [
21 42 "media_library" => [
22 43 'table' => 'posts',
@@ -33,8 +54,26 @@
33 54 $this->version = WPMCS_VERSION;
34 55 $this->token = WPMCS_TOKEN;
35 56 $this->settings = Utils::get_settings();
36 57
58 + $this->credentials = Utils::get_credentials();
59 + $this->config = isset($this->credentials['config']) && !empty($this->credentials['config'])
60 + ? $this->credentials['config']
61 + : [];
62 + $this->bucketConfig = isset($this->credentials['bucketConfig']) && !empty($this->credentials['bucketConfig'])
63 + ? $this->credentials['bucketConfig']
64 + : [];
65 + $this->service = isset($this->credentials['service']) && !empty($this->credentials['service'])
66 + ? $this->credentials['service']
67 + : '';
68 +
69 + if (isset($this->bucketConfig['bucket_name'])) {
70 + $this->bucket_name = $this->bucketConfig['bucket_name'];
71 + }
72 + if (isset($this->config['region'])) {
73 + $this->region = $this->config['region'];
74 + }
75 +
37 76 // Initialize setup
38 77 $this->registerActions();
39 78 }
40 79
@@ -49,14 +88,30 @@
49 88
50 89 /** URL RE-WRITING HOOKS */
51 90 add_filter( 'wp_get_attachment_url', [ $this, 'wp_get_attachment_url' ], 99, 2 );
52 91 add_filter( 'wp_get_attachment_image_attributes', [ $this, 'wp_get_attachment_image_attributes' ], 99, 3 );
53 - add_filter( 'wp_calculate_image_srcset', [ $this, 'wp_calculate_image_srcset' ], 99, 5 );
92 + add_filter( 'get_image_tag', array( $this, 'get_image_tag' ), 99, 6 );
93 + add_filter( 'wp_get_attachment_image_src', array( $this, 'wp_get_attachment_image_src' ), 99, 4 );
94 + add_filter( 'wp_prepare_attachment_for_js', array( $this, 'wp_prepare_attachment_for_js' ), 99, 3 );
95 + add_filter( 'image_get_intermediate_size', array( $this, 'image_get_intermediate_size' ), 99, 3 );
54 96 add_filter( 'get_attached_file', [ $this, 'get_attached_file' ], 10, 2 );
55 97 add_filter( 'wp_get_original_image_path', [ $this, 'get_attached_file' ], 10, 2 );
56 98 add_filter( 'wp_video_shortcode', [ $this, 'wp_media_shortcode' ], 100, 5 );
57 99 add_filter( 'wp_audio_shortcode', [ $this, 'wp_media_shortcode' ], 100, 5 );
100 +
101 + /*
102 + * Responsive Images WP 4.4
103 + */
104 + add_filter( 'wp_calculate_image_srcset', array( $this, 'wp_calculate_image_srcset' ), 10, 5 );
58 105
106 + // Srcset handling
107 + add_filter( 'wp_image_file_matches_image_meta', array( $this, 'wp_image_file_matches_image_meta' ), 10, 4 );
108 +
109 + if ( self::wp_check_filetype_broken() ) {
110 + add_filter( 'shortcode_atts_audio', array( $this, 'filter_shortcode_atts' ), 10, 4 );
111 + add_filter( 'shortcode_atts_video', array( $this, 'filter_shortcode_atts' ), 10, 4 );
112 + }
113 +
59 114 /** FILE MANAGEMENT HOOKS */
60 115 add_filter( 'wp_unique_filename', [ $this, 'wp_unique_filename' ], 10, 3 );
61 116 add_filter( 'wp_update_attachment_metadata', [ $this, 'update_attachment_metadata' ], 110, 2 );
62 117 add_filter( 'pre_delete_attachment', [ $this, 'pre_delete_attachment' ], 20 );
@@ -61,38 +116,29 @@
61 116 add_filter( 'wp_update_attachment_metadata', [ $this, 'update_attachment_metadata' ], 110, 2 );
62 117 add_filter( 'pre_delete_attachment', [ $this, 'pre_delete_attachment' ], 20 );
63 118 add_filter( 'delete_attachment', [ $this, 'delete_attachment' ], 20 );
64 119 add_action( 'delete_post', [$this, 'delete_post'] );
120 + // Safety net for code that deletes an attachment bypassing wp_delete_attachment().
121 + add_action( 'deleted_post', [ $this, 'deleted_post' ], 10, 2 );
65 122 add_filter( 'update_attached_file', [ $this, 'update_attached_file' ], 100, 2 );
66 - add_filter( 'load_image_to_edit_path', [ $this, 'load_image_to_edit_path' ], 10, 3 );
123 +
124 + add_action( 'wpmcs_do_update_attachment_metadata', [ $this, 'update_attachment_metadata' ], 10, 2 );
67 125 }
68 126
69 127
70 128 /**
71 - * Function to execute on load_image_to_edit_path
129 + * Upload a single media item from the local server to the provider.
130 + *
131 + * This function will trigger the `wpmcs_do_update_attachment_metadata` action
132 + * with the `false` parameter, which will cause the attachment metadata
133 + * to be updated on the provider.
134 + *
135 + * @param int $id The media item ID.
136 + * @param string $source_type The source type of the media item.
72 137 * @since 1.0.0
73 - * @return string
74 - *
75 138 */
76 - public function load_image_to_edit_path($path, $attachment_id, $size) {
77 - $wpmcsItem = Item::instance();
78 - if (!Utils::is_ok_to_serve($attachment_id) && !$wpmcsItem->is_available_from_provider($attachment, true, 'media_library') ) {
79 - return $path;
80 - }
81 -
82 -
83 - $server_file = false;
84 - //If operation is Image editor Image Save
85 - if(
86 - isset($_REQUEST['do']) &&
87 - isset($_REQUEST['action']) &&
88 - $_REQUEST['action'] === 'image-editor' &&
89 - $_REQUEST['do']==='save'
90 - ) {
91 - $server_file = $wpmcsItem->moveToServer($attachment_id, $size, false, 'media_library');
92 - }
93 -
94 - return $server_file ? $server_file : $path;
139 + public function upload_single_media($id, $source_type = 'media_library') {
140 + do_action( 'wpmcs_do_update_attachment_metadata', false, $id );
95 141 }
96 142
97 143
98 144 /**
@@ -125,21 +171,17 @@
125 171 * @since 1.0.0
126 172 *
127 173 */
128 174 public function delete_attachment($attachment_id){
129 - if (!Utils::is_service_enabled()) {
130 - return $attachment_id;
131 - }
132 -
133 175 $wpmcsItem = Item::instance();
134 176 $item = $wpmcsItem->get($attachment_id, 'media_library');
135 177
136 178 if (Utils::is_empty($item)) {
179 + // Remove Log if exists
180 + Logger::instance()->remove_log('sync_to_cloud', $attachment_id, 'media_library');
137 181 return $attachment_id;
138 182 }
139 183
140 - $wpmcsItem->delete_attachments_by_item($item);
141 -
142 184 $wpmcsItem->delete($attachment_id, 'media_library');
143 185
144 186 return $attachment_id;
145 187 }
@@ -151,40 +193,59 @@
151 193 * @param int $attachment_id
152 194 * @return array|WP_Error
153 195 */
154 196 public function update_attachment_metadata($attachment_meta, $attachment_id) {
155 - if (!Utils::is_ok_to_upload($attachment_id) || is_wp_error($attachment_meta)) {
156 - return $attachment_meta;
157 - }
197 + // Remove Logs
198 + Logger::instance()->remove_log('sync_to_cloud', $attachment_id, 'media_library');
158 199
159 - $attachment_id = (int) $attachment_id;
160 - $is_image = wp_attachment_is_image($attachment_id);
200 + // Reachable from three independent, uncoordinated triggers (WP's own
201 + // wp_update_attachment_metadata hook, this plugin's bulk sync, Imagify's re-sync).
202 + // No lock here — Item::add()'s upsert is what keeps a race safe; skipping instead
203 + // would risk dropping a real re-upload request.
204 + try {
205 + // Ensure attachment is eligible for upload
206 + $attachment_meta = is_array($attachment_meta) && !empty($attachment_meta)
207 + ? $attachment_meta
208 + : wp_get_attachment_metadata($attachment_id);
161 209
162 - if ($is_image && $this->should_wait_for_subsizes($attachment_meta, $attachment_id)) {
163 - return $attachment_meta;
164 - }
210 + if (!Utils::is_ok_to_upload($attachment_id) || is_wp_error($attachment_meta)) {
211 + return $attachment_meta;
212 + }
165 213
166 - $attachment_meta = $attachment_meta ?: wp_get_attachment_metadata($attachment_id);
214 + $attachment_id = (int) $attachment_id;
215 + $is_image = wp_attachment_is_image($attachment_id);
167 216
168 - $file = $this->get_attachment_file($attachment_meta, $attachment_id);
169 - $source_path = Utils::get_attachment_source_path($file);
217 + if ($is_image && $this->should_wait_for_subsizes($attachment_meta, $attachment_id)) {
218 + return $attachment_meta;
219 + }
170 220
171 - if (empty($source_path) || !is_string($source_path)) {
172 - return $attachment_meta;
173 - }
221 + $file = $this->get_attachment_file($attachment_meta, $attachment_id);
222 + $source_path = Utils::get_attachment_source_path($file);
174 223
175 - $existing = Item::instance()->get($attachment_id, 'media_library');
176 - $backup = Item::instance()->get_backup($attachment_id, 'media_library');
224 + if (empty($source_path) || !is_string($source_path)) {
225 + return $attachment_meta;
226 + }
177 227
178 - if (Utils::is_empty($existing)) {
179 - $this->handle_media_first_upload($attachment_meta, $attachment_id, $source_path);
180 - } elseif (Utils::is_empty($backup)) {
181 - $this->handle_media_with_no_backup($attachment_meta, $attachment_id, $source_path, $existing);
182 - } else {
183 - $this->handle_media_with_backup($attachment_meta, $attachment_id, $source_path, $existing, $backup);
228 + $existing = Item::instance()->get($attachment_id, 'media_library');
229 + $backup = Item::instance()->get_backup($attachment_id, 'media_library');
230 +
231 + if (Utils::is_empty($existing)) {
232 + $this->handle_media_first_upload($attachment_meta, $attachment_id, $source_path);
233 + } elseif (Utils::is_empty($backup)) {
234 + $this->handle_media_with_no_backup($attachment_meta, $attachment_id, $source_path, $existing);
235 + } else {
236 + $this->handle_media_with_backup($attachment_meta, $attachment_id, $source_path, $existing, $backup);
237 + }
238 +
239 + return $attachment_meta;
240 + } catch (Exception $e) {
241 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
242 + 'message' => $e->getMessage() ?? 'Currupted attachment metadata.',
243 + 'file' => method_exists($e, 'getFile') ? $e->getFile() : 'N/A',
244 + 'code' => 415
245 + ]);
246 + return $attachment_meta;
184 247 }
185 -
186 - return $attachment_meta;
187 248 }
188 249
189 250 /**
190 251 * Checks if we should wait for WordPress to generate subsizes.
@@ -189,9 +250,13 @@
189 250 /**
190 251 * Checks if we should wait for WordPress to generate subsizes.
191 252 */
192 253 private function should_wait_for_subsizes($attachment_meta, $attachment_id) {
193 - if (empty($attachment_meta) || !function_exists('wp_get_registered_image_subsizes')) {
254 + if (
255 + empty($attachment_meta) ||
256 + !function_exists('wp_get_registered_image_subsizes') ||
257 + !function_exists('wp_get_missing_image_subsizes')
258 + ) {
194 259 return false;
195 260 }
196 261
197 262 // Wait for generate_attachment_metadata if the filter is set to true.
@@ -213,20 +278,15 @@
213 278 // Also, some images, particularly SVGs, don't create thumbnails but do have
214 279 // metadata for them. At the time `wp_get_missing_image_subsizes()` checks
215 280 // the saved metadata, it isn't there, but we already have it.
216 281 $handle_post_meta = function ($value, $object_id, $meta_key, $single, $meta_type) use ($attachment_id, $attachment_meta) {
217 - if (
218 - !empty($value['image_meta']['orientation']) ||
219 - !empty($attachment_meta['image_meta']['orientation'])
220 - ) {
221 - if (!empty($value['image_meta']['orientation'])) {
222 - unset($value['image_meta']['orientation']);
223 - }
224 - if (!empty($attachment_meta['image_meta']['orientation'])) {
225 - unset($attachment_meta['image_meta']['orientation']);
226 - }
227 - unset($value['original_image'], $attachment_meta['original_image']);
282 + if(is_array($attachment_meta) && isset($attachment_meta['image_meta']) && !empty($attachment_meta['image_meta']['orientation'])) {
283 + unset($attachment_meta['original_image']);
228 284 }
285 + if(is_array($value) && isset($value['image_meta']) && !empty($value['image_meta']['orientation'])) {
286 + unset($value['original_image']);
287 + }
288 +
229 289 if (
230 290 is_null($value) &&
231 291 $object_id === $attachment_id &&
232 292 '_wp_attachment_metadata' === $meta_key &&
@@ -231,8 +291,10 @@
231 291 $object_id === $attachment_id &&
232 292 '_wp_attachment_metadata' === $meta_key &&
233 293 $single && $meta_type === 'post'
234 294 ) {
295 + // For some reason the filter is expected return an array of values
296 + // as if not doing a single record.
235 297 return [$attachment_meta];
236 298 }
237 299 return $value;
238 300 };
@@ -269,8 +331,10 @@
269 331 $attachment_id,
270 332 $uploaded_data['file']['url'],
271 333 $uploaded_data['file']['key'],
272 334 $uploaded_data['file']['source_path'],
335 + $uploaded_data['file']['original_source_path'] ?? '',
336 + $uploaded_data['file']['original_key'] ?? '',
273 337 $uploaded_data['extra'],
274 338 'media_library'
275 339 );
276 340 }
@@ -287,9 +351,9 @@
287 351 private function handle_media_with_no_backup($attachment_meta, $attachment_id, $source_path, $existing) {
288 352 $uploaded_data = $this->uploadMedia($attachment_meta, $attachment_id, $source_path);
289 353
290 354 if ($existing['source_path'] != $source_path) {
291 - $uploaded_data['extra']['backup'] = serialize($existing);
355 + $uploaded_data['extra']['backup'] = Utils::maybe_serialize($existing);
292 356 }
293 357
294 358 $this->update_item_if_uploaded($attachment_id, $uploaded_data);
295 359 }
@@ -307,9 +371,9 @@
307 371 $delete_existing = false;
308 372
309 373 if ($backup['source_path'] != $source_path) {
310 374 $uploaded_data = $this->uploadMedia($attachment_meta, $attachment_id, $source_path);
311 - $uploaded_data['extra']['backup'] = maybe_serialize($backup);
375 + $uploaded_data['extra']['backup'] = Utils::maybe_serialize($backup);
312 376
313 377 if ($existing['source_path'] != $source_path) {
314 378 $delete_existing = true;
315 379 }
@@ -318,19 +382,21 @@
318 382
319 383 // construct upload data from backup
320 384 $uploaded_data = [
321 385 'file' => [
322 - 'source_path' => $backup['source_path'],
323 - 'url' => $backup['url'],
324 - 'key' => $backup['key'],
386 + 'source_path' => $backup['source_path'],
387 + 'url' => $backup['url'],
388 + 'key' => $backup['key'],
389 + 'original_source_path' => $backup['original_source_path'],
390 + 'original_key' => $backup['original_key'],
325 391 ],
326 - 'extra' => maybe_unserialize($backup['extra']),
392 + 'extra' => Utils::maybe_unserialize($backup['extra']),
327 393 ];
328 394 }
329 395
330 396 if ($delete_existing) {
331 397 Item::instance()->delete_attachments_by_item($existing, false);
332 - Item::instance()->may_be_delete_server_files_by_item($existing, true);
398 + Item::instance()->delete_server_files_by_item($existing, true);
333 399 }
334 400
335 401 $this->update_item_if_uploaded($attachment_id, $uploaded_data);
336 402 }
@@ -346,12 +412,14 @@
346 412 if (!empty($uploaded_data['file'])) {
347 413 Item::instance()->update(
348 414 $attachment_id,
349 415 [
350 - 'source_path' => $uploaded_data['file']['source_path'],
351 - 'url' => $uploaded_data['file']['url'],
352 - 'key' => $uploaded_data['file']['key'],
353 - 'extra' => maybe_serialize($uploaded_data['extra']),
416 + 'source_path' => $uploaded_data['file']['source_path'],
417 + 'url' => $uploaded_data['file']['url'],
418 + 'key' => $uploaded_data['file']['key'],
419 + 'original_source_path' => $uploaded_data['file']['original_source_path'] ?? '',
420 + 'original_key' => $uploaded_data['file']['original_key'] ?? '',
421 + 'extra' => Utils::maybe_serialize($uploaded_data['extra']),
354 422 ],
355 423 'media_library'
356 424 );
357 425 }
@@ -377,12 +445,8 @@
377 445 * @since 1.0.0
378 446 * @return string
379 447 */
380 448 private function filter_unique_filename($filename, $ext, $dir, $post_id = null) {
381 - if (!Utils::is_service_enabled()) {
382 - return $filename;
383 - }
384 -
385 449 // sanitize the file name before we begin processing
386 450 $filename = sanitize_file_name($filename);
387 451 $ext = strtolower($ext);
388 452 $name = wp_basename($filename, $ext);
@@ -497,23 +561,12 @@
497 561 if ( $this->deleting_attachment ) {
498 562 return $file;
499 563 }
500 564
501 - if (!Utils::is_ok_to_serve($attachment_id)) {
565 + if (!Utils::should_serve_from_cloud($attachment_id)) {
502 566 return $file;
503 567 }
504 568
505 - if( isset($_REQUEST['action']) && $_REQUEST['action'] === 'image-editor' ) {
506 - // Avoid rewriting URL when image restore in image editor
507 - if(isset($_REQUEST['do']) && $_REQUEST['do']==='restore') {
508 - return $file;
509 - }
510 - // Avoid rewriting URL when image save in image editor
511 - if(isset($_REQUEST['do']) && $_REQUEST['do'] === 'save') {
512 - return $file;
513 - }
514 - }
515 -
516 569 $wpmcsItem = Item::instance();
517 570
518 571 $item = $wpmcsItem->get($attachment_id, 'media_library');
519 572
@@ -557,49 +610,72 @@
557 610 */
558 611
559 612 public function wp_calculate_image_srcset($sources, $size_array, $image_src, $attachment_meta, $attachment_id = 0) {
560 613 $attachment_id = (int)$attachment_id;
614 + if ( ! is_array( $sources ) ) {
615 + // Sources corrupt
616 + return $sources;
617 + }
561 618
562 619 // Must need $attachment_id other wise not possible to get data from the table
563 - if (!Utils::is_ok_to_serve($attachment_id)) {
620 + if (!Utils::should_serve_from_cloud($attachment_id)) {
564 621 return $sources;
565 622 }
566 623
567 624 $wpmcsItem = Item::instance();
568 625
569 - $item = $wpmcsItem->get($attachment_id, 'media_library');
570 -
571 - if (Utils::is_empty($item)) {
626 + if (Utils::is_empty($wpmcsItem->get($attachment_id, 'media_library'))) {
572 627 return $sources;
573 628 }
574 629
575 - $item_extra = $wpmcsItem->get_extras($attachment_id, false, 'media_library');
630 + foreach ( $sources as $width => $source ) {
631 + $filename = wp_basename( $source['url'] );
632 + $size = $this->find_image_size_from_width( $attachment_meta, $width, $filename );
633 + $provider_url = $wpmcsItem->get_url( $attachment_id, $size, 'media_library' );
576 634
577 - if (isset($item_extra['width']) && !empty($item_extra['width'])) {
578 - $sources[$item_extra['width']]=[
579 - 'url' => $wpmcsItem->get_url($attachment_id, 'full', 'media_library'),
580 - 'descriptor' => 'w',
581 - 'value' => $item_extra['width']
582 - ];
583 - }
635 + if ( false === $provider_url || is_wp_error( $provider_url ) ) {
636 + // Skip URLs not uploaded to cloud
637 + continue;
638 + }
584 639
585 - if ($item_extra) {
586 - if (isset($item_extra['sizes']) && !empty($item_extra['sizes'])) {
587 - foreach ($item_extra['sizes'] as $size => $size_array) {
588 - if (isset($size_array['width']) && !empty($size_array['width'])) {
589 - $w = $size_array['width'];
590 - if (isset($sources[$w]) && !empty($sources[$w])) {
591 - $sources[$w]['url'] = $wpmcsItem->get_url($attachment_id, $size, 'media_library');
592 - }
593 - }
594 - }
595 - }
596 - }
597 -
640 + $sources[ $width ]['url'] = $provider_url;
641 + }
598 642 return $sources;
599 643 }
600 644
601 645 /**
646 + * Helper function to find size name from width and filename
647 + *
648 + * @param array $sizes
649 + * @param string $width
650 + * @param string $filename
651 + *
652 + * @return null|string
653 + */
654 + protected function find_image_size_from_width( $meta, $width, $filename ) {
655 + if ( ! is_array( $meta ) ) {
656 + return null;
657 + }
658 +
659 + if ( ! empty( $meta['sizes'] ) && is_array( $meta['sizes'] ) ) {
660 + foreach ( $meta['sizes'] as $name => $size ) {
661 + if ( $width === absint( $size['width'] ?? 0 ) && isset( $size['file'] ) && $size['file'] === $filename ) {
662 + return $name;
663 + }
664 + }
665 + }
666 +
667 + if ( isset( $meta['width'], $meta['file'] ) && $width === absint( $meta['width'] ) ) {
668 + $full_file = wp_basename( $meta['file'] );
669 + if ( $full_file === $filename ) {
670 + return 'full';
671 + }
672 + }
673 +
674 + return null;
675 + }
676 +
677 + /**
602 678 * Filters the list of attachment image attributes.
603 679 *
604 680 * @since 1.0.0
605 681 * @param array $attr Attributes for the image markup.
@@ -609,10 +685,10 @@
609 685 * @return array
610 686 */
611 687 public function wp_get_attachment_image_attributes($attr, $attachment, $size='thumbnail') {
612 688 if (
613 - !$attachment ||
614 - !Utils::is_ok_to_serve($attachment->ID)
689 + !$attachment ||
690 + !Utils::should_serve_from_cloud($attachment->ID)
615 691 ) {
616 692 return $attr;
617 693 }
618 694
@@ -659,9 +735,9 @@
659 735 *
660 736 * @return bool|mixed|WP_Error
661 737 */
662 738 public function wp_get_attachment_url($url, $attachment_id) {
663 - if (!Utils::is_ok_to_serve($attachment_id)) {
739 + if (!Utils::should_serve_from_cloud($attachment_id)) {
664 740 return $url;
665 741 }
666 742
667 743 $new_url = Item::instance()->get_url($attachment_id, 'full', 'media_library');
@@ -675,172 +751,626 @@
675 751 return $new_url;
676 752 }
677 753
678 754 /**
755 + * Maybe replace attachment URLs when retrieving the image tag
756 + *
757 + * @param string $html
758 + * @param int $id
759 + * @param string $alt
760 + * @param string $title
761 + * @param string $align
762 + * @param string $size
763 + *
764 + * @return string
765 + */
766 + public function get_image_tag( $html, $id, $alt = '', $title = '', $align = '', $size = '' ) {
767 + if ( ! is_string( $html ) ) {
768 + return $html;
769 + }
770 +
771 + if (!Utils::should_serve_from_cloud($id)) {
772 + return $html;
773 + }
774 +
775 + preg_match( '@\ssrc=[\'\"]([^\'\"]*)[\'\"]@', $html, $matches );
776 +
777 + if ( ! isset( $matches[1] ) ) {
778 + // Can't establish img src
779 + return $html;
780 + }
781 +
782 + if ( empty( $size ) && preg_match( '/\bsize-([^\s"\']+)/', $html, $size_match ) ) {
783 + $size = $size_match[1];
784 + }
785 +
786 + if ( empty( $size ) ) {
787 + $size = 'full';
788 + }
789 +
790 + $img_src = $matches[1];
791 + $size = Utils::maybe_convert_size_to_string( $id, $size );
792 + $new_url = Item::instance()->get_url($id, $size, 'media_library');
793 +
794 + if (Utils::is_empty($new_url)) {
795 + return $html;
796 + }
797 +
798 + return str_replace( $img_src, $new_url, $html );
799 + }
800 +
801 +
802 + /**
803 + * Relace URLs for images that represent an attachment
804 + *
805 + * @param array|bool $image
806 + * @param int $attachment_id
807 + * @param string|array $size
808 + * @param bool $icon
809 + *
810 + * @return array
811 + */
812 + public function wp_get_attachment_image_src( $image, $attachment_id, $size, $icon ) {
813 + if (!Utils::should_serve_from_cloud($attachment_id)) {
814 + return $image;
815 + }
816 +
817 + if ( isset( $image[0] ) ) {
818 + $size = Utils::maybe_convert_size_to_string( $attachment_id, $size );
819 + $new_url = Item::instance()->get_url($attachment_id, $size, 'media_library');
820 +
821 + if (Utils::is_empty($new_url)) {
822 + return $image;
823 + }
824 +
825 + $image[0] = $new_url;
826 + }
827 +
828 + return $image;
829 + }
830 +
831 + /**
832 + * Replace URLs when outputting attachments in the media grid
833 + *
834 + * @param array $response
835 + * @param int|object $attachment
836 + * @param array $meta
837 + *
838 + * @return array
839 + */
840 + public function wp_prepare_attachment_for_js( $response, $attachment, $meta ) {
841 + if (!Utils::should_serve_from_cloud($attachment->ID)) {
842 + return $response;
843 + }
844 +
845 + if ( isset( $response['sizes'] ) && is_array( $response['sizes'] ) ) {
846 + foreach ( $response['sizes'] as $size => $value ) {
847 + $url = Item::instance()->get_url($attachment->ID, $size, 'media_library');
848 + if (Utils::is_empty($url)) {
849 + continue;
850 + }
851 + $response['sizes'][ $size ]['url'] = $url;
852 + }
853 + }
854 +
855 + return $response;
856 + }
857 +
858 +
859 + /**
860 + * Replace URLs when retrieving intermediate sizes.
861 + *
862 + * @param array $data
863 + * @param int $post_id
864 + * @param string|array $size
865 + *
866 + * @return array
867 + */
868 + public function image_get_intermediate_size( $data, $post_id, $size ) {
869 + if (!Utils::should_serve_from_cloud($post_id)) {
870 + return $data;
871 + }
872 + if ( isset( $data['url'] ) ) {
873 + $size = Utils::maybe_convert_size_to_string( $post_id, $size );
874 + $url = Item::instance()->get_url($post_id, $size, 'media_library');
875 +
876 + if (Utils::is_empty($url)) {
877 + return $data;
878 + }
879 +
880 + $data['url'] = $url;
881 + }
882 +
883 + return $data;
884 + }
885 +
886 + /**
887 + * Determines if the image metadata is for the image source file.
888 + *
889 + * @handles wp_image_file_matches_image_meta
890 + *
891 + * @param bool $match
892 + * @param string $image_location
893 + * @param array $image_meta
894 + * @param int $source_id
895 + *
896 + * @return bool
897 + */
898 + public function wp_image_file_matches_image_meta( $match, $image_location, $image_meta, $source_id ) {
899 + // If already matched or the URL is local, there's nothing for us to do.
900 + if ( $match || FilterContent::url_needs_replacing( $image_location ) ) {
901 + return $match;
902 + }
903 +
904 + $item = array(
905 + 'id' => $source_id,
906 + 'source_type' => 'media_library',
907 + );
908 +
909 + return FilterContent::instance()->item_matches_src( $item, $image_location );
910 + }
911 +
912 +
913 + /**
914 + * Filters shortcode attributes to temporarily add file extension to end of URL params.
915 + *
916 + * The temporary extension is removed once wp_check_filetype has been used.
917 + *
918 + * The function compensates for when query args or fragments are included in the URL,
919 + * which makes wp_check_filetype fail to see the extension of the file.
920 + *
921 + * @see https://core.trac.wordpress.org/ticket/30377
922 + * @see https://github.com/aaemnnosttv/fix-wp-media-shortcodes-with-params/blob/master/fix-wp-media-shortcodes-with-params.php
923 + *
924 + * @param array $out The output array of shortcode attributes.
925 + * @param array $pairs The supported attributes and their defaults.
926 + * @param array $atts The user defined shortcode attributes.
927 + * @param string $shortcode The shortcode name.
928 + *
929 + * @return array
930 + */
931 + public function filter_shortcode_atts( $out, $pairs, $atts, $shortcode ) {
932 + $get_media_extensions = "wp_get_{$shortcode}_extensions";
933 +
934 + if ( ! function_exists( $get_media_extensions ) ) {
935 + return $out;
936 + }
937 +
938 + $default_types = $get_media_extensions();
939 +
940 + if ( empty( $default_types ) || ! is_array( $default_types ) ) {
941 + return $out;
942 + }
943 +
944 + // URLs can be in src or type specific fallback attributes.
945 + array_unshift( $default_types, 'src' );
946 +
947 + $fixes = array();
948 +
949 + foreach ( $default_types as $type ) {
950 + if ( empty( $out[ $type ] ) ) {
951 + continue;
952 + }
953 +
954 + if ( false !== strpos( $out[ $type ], '&'.$this->token.'-fix-wp-check-file-type-ext=.' ) ) {
955 + continue;
956 + }
957 +
958 + if ( Utils::is_url( $out[ $type ] ) ) {
959 + $url = $out[ $type ];
960 + $parts = wp_parse_url( $url );
961 +
962 + if (
963 + empty( $parts['path'] ) ||
964 + ( empty( $parts['query'] ) && empty( $parts['fragment'] ) )
965 + ) {
966 + continue;
967 + }
968 +
969 + $ext = pathinfo( $parts['path'], PATHINFO_EXTENSION );
970 +
971 + if ( empty( $ext ) ) {
972 + continue;
973 + }
974 +
975 + $scheme = empty( $parts['scheme'] ) ? '' : $parts['scheme'] . '://';
976 + $user = empty( $parts['user'] ) ? '' : $parts['user'];
977 + $pass = ! empty( $user ) && ! empty( $parts['pass'] ) ? ':' . $parts['pass'] : '';
978 + $auth = ! empty( $user ) ? $user . $pass . '@' : '';
979 + $host = empty( $parts['host'] ) ? '' : $parts['host'];
980 + $port = ! empty( $host ) && ! empty( $parts['port'] ) ? ':' . $parts['port'] : '';
981 + $path = $parts['path'];
982 + $query = empty( $parts['query'] ) ? '?'.$this->token.'-fix-wp-check-file-type=true' : '?' . $parts['query'];
983 +
984 + if ( ! empty( $parts['fragment'] ) ) {
985 + $query .= '&'.$this->token.'-fix-wp-check-file-type-fragment=' . $parts['fragment'];
986 + }
987 +
988 + $query .= '&'.$this->token.'-fix-wp-check-file-type-ext=.' . $ext;
989 +
990 + $out[ $type ] = $scheme . $auth . $host . $port . $path . $query;
991 + $fixes[] = $ext;
992 + }
993 + }
994 +
995 + if ( $fixes ) {
996 + add_filter( "wp_{$shortcode}_shortcode", function ( $html ) use ( $fixes ) {
997 + $html = str_replace( '?'.$this->token.'-fix-wp-check-file-type=true', '', $html );
998 +
999 + foreach ( $fixes as $ext ) {
1000 + $html = str_replace( '&'.$this->token.'-fix-wp-check-file-type-ext=.' . $ext, '', $html );
1001 + }
1002 +
1003 + return str_replace( '&'.$this->token.'-fix-wp-check-file-type-fragment=', '#', $html );
1004 + } );
1005 + }
1006 +
1007 + return $out;
1008 + }
1009 +
1010 +
1011 + /**
679 1012 * Upload media item
680 1013 */
681 1014 public function uploadMedia($attachment_meta, $attachment_id, $source_path) {
682 - $wpmcsItem = Item::instance();
683 - $upload_dir = wp_get_upload_dir();
684 - $is_image = wp_attachment_is_image($attachment_id);
685 - $existing = $wpmcsItem->get($attachment_id, 'media_library');
686 - $existing_extras = $wpmcsItem->get_extras($attachment_id, false, 'media_library');
687 - $has_existing = !Utils::is_empty($existing);
688 - $sizes = [];
689 - $uploaded = [];
690 - $extras = [];
691 - $prefix = '';
692 - $file_path = '';
693 - $file_dir = '';
694 - $original_file_path = '';
695 - $original_file_source_path = '';
1015 + $wpmcsItem = Item::instance();
1016 + $upload_dir = wp_get_upload_dir();
1017 + $is_image = wp_attachment_is_image($attachment_id);
1018 + $existing = $wpmcsItem->get($attachment_id, 'media_library');
1019 + $existing_extras = $wpmcsItem->get_extras($attachment_id, false, 'media_library');
1020 + $has_existing = !Utils::is_empty($existing);
1021 + // A reupload of an already-private item must keep writing under the private path —
1022 + // never true for a genuinely new item, which is exactly right (new uploads stay public).
1023 + $is_private = $has_existing && !empty($existing['is_private']);
1024 + $sizes = [];
1025 + $uploaded = [];
1026 + $extras = [];
1027 + $prefix = '';
1028 + $file_path = trailingslashit($upload_dir['basedir']) . $source_path;
1029 + $file_dir = isset(pathinfo($file_path)['dirname']) ? pathinfo($file_path)['dirname'] : '';
1030 + $original_file = isset($attachment_meta['original_image']) ? $attachment_meta['original_image'] : '';
696 1031
1032 + $do_reupload = apply_filters('wpmcs_do_reupload_media', false, $attachment_id, 'media_library');
697 1033
1034 + // Force reupload when sizes/dimensions changed
1035 + if (!$do_reupload && $has_existing) {
1036 + if($this->shouldForceReupload($attachment_meta, $attachment_id, $existing_extras)) {
1037 + $do_reupload = true;
1038 + }
1039 + }
1040 +
698 1041 // Add prefix if object versioning is ON
699 1042 if (isset($this->settings['object_versioning']) && $this->settings['object_versioning']) {
700 - $prefix = ($existing_extras && isset($existing_extras['prefix']) && !empty($existing_extras['prefix']))
701 - ? $existing_extras['prefix']
1043 + $prefix = ($has_existing && isset($existing_extras['prefix']) && !empty($existing_extras['prefix']))
1044 + ? $existing_extras['prefix']
702 1045 : Utils::generate_object_versioning_prefix();
703 - $extras['prefix'] = $prefix;
1046 + $extras['prefix'] = $prefix;
704 1047 }
705 1048
706 - // Get width and height from image meta
1049 + // Width/height
707 1050 if (isset($attachment_meta) && !empty($attachment_meta)) {
708 1051 $extras['width'] = (isset($attachment_meta['width']) && !empty($attachment_meta['width'])) ? $attachment_meta['width'] : 0;
709 1052 $extras['height'] = (isset($attachment_meta['height']) && !empty($attachment_meta['height'])) ? $attachment_meta['height'] : 0;
710 1053 }
711 1054
712 -
713 - // Get Original File Name/Path from Image meta
714 - $original_file = isset($attachment_meta) && !empty($attachment_meta) &&
715 - isset($attachment_meta['original_image']) && !empty($attachment_meta['original_image'])
716 - ? $attachment_meta['original_image'] : '';
717 -
718 - $file_path = trailingslashit($upload_dir['basedir']) . $source_path;
719 - $file_dir = isset(pathinfo($file_path)['dirname']) ? pathinfo($file_path)['dirname'] : '';
720 -
721 - // Check whether the extension is enabled for uploading
1055 + // Excluded by extension settings — an intentional skip, not a sync failure, so no log entry.
722 1056 if (!Utils::is_extension_available($file_path)) {
723 - return $attachment_meta;
1057 + return false;
724 1058 }
725 1059
726 - // Upload the Full Size file
727 - if( $has_existing && ( $existing['source_path'] == $source_path ) ) {
728 - $uploaded = [
729 - 'success' => true,
730 - 'file_url' => $existing['url'],
731 - 'key' => $existing['key']
732 - ];
733 - } else if(file_exists($file_path)){
734 - $uploaded = Service::instance()->uploadSingle($file_path, $source_path, $prefix);
735 - }
1060 + // Upload main file first, if that fails no need to attempt the rest
1061 + $uploaded = $this->uploadFullFile($file_path, $source_path, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private);
736 1062
737 - if(
738 - isset($uploaded) && !empty($uploaded) &&
739 - isset($uploaded['success']) && $uploaded['success']
740 - ) {
741 - if(isset($original_file) && !empty($original_file)){
742 - // Find original image relative and absolute path
743 - $original_file_path = trailingslashit($file_dir) . $original_file;
744 - $original_file_source_path = Utils::get_attachment_source_path($original_file_path);
745 - $uploaded_original = [];
1063 + if ($uploaded && isset($uploaded['success']) && $uploaded['success']) {
1064 + // Attempt to upload original file if present. Not critical if this fails so we don't check the result before proceeding.
1065 + $original = $this->uploadOriginalFile($original_file, $file_dir, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private);
746 1066
747 - // Upload Original File
748 - if(
749 - !Utils::is_empty($existing_extras) &&
750 - isset($existing_extras['original']) && !Utils::is_empty($existing_extras['original']) &&
751 - $existing_extras['original']['source_path'] == $original_file_source_path
752 - ) {
753 - $uploaded_original = [
754 - 'success' => true,
755 - 'file_url' => $existing_extras['original']['url'],
756 - 'key' => $existing_extras['original']['key']
757 - ];
758 - } else if(file_exists($original_file_path)) {
759 - $uploaded_original = Service::instance()->uploadSingle($original_file_path, $original_file_source_path, $prefix);
1067 + if ($is_image && isset($file_dir) && !empty($file_dir)) {
1068 + $sizes_to_upload = $this->get_attachment_image_sizes_for_upload($attachment_meta, $attachment_id);
1069 + if (!empty($sizes_to_upload)) {
1070 + // Upload image sizes. Again, not critical if this fails so we don't check the result before proceeding.
1071 + $sizes = $this->uploadImageSizes($sizes_to_upload, $file_dir, $do_reupload, $existing_extras, $prefix, $attachment_id, $is_private);
760 1072 }
761 -
762 - if(
763 - isset($uploaded_original) && !empty($uploaded_original) &&
764 - isset($uploaded_original['success']) && $uploaded_original['success']
765 - ) {
766 - $extras['original'] = array(
767 - 'source_path' => $original_file_source_path,
768 - 'url' => $uploaded_original['file_url'],
769 - 'key' => $uploaded_original['key']
770 - );
771 - }
772 1073 }
773 1074
774 - if (
775 - $is_image &&
776 - isset($attachment_meta['sizes']) && !empty($attachment_meta['sizes']) &&
777 - isset($file_dir) && !empty($file_dir)
778 - ) {
779 - foreach ($attachment_meta['sizes'] as $size => $sub_image) {
780 - $sub_size = [];
781 - $sub_file = isset($sub_image['file']) ? $sub_image['file'] : false;
782 - $sub_file_path = '';
783 - $sub_file_source_path = '';
784 - $uploaded_sub_image = [];
785 -
786 - if ($sub_file) {
787 - $sub_file_path = $file_dir . '/' . $sub_file;
788 - $sub_file_source_path = Utils::get_attachment_source_path($sub_file_path);
789 - }
1075 + if (!empty($sizes)) {
1076 + // Merge over existing sizes rather than replacing wholesale — a size that
1077 + // came back empty from uploadImageSizes() (its own fallback included) must
1078 + // never erase a previously-good, unrelated size's record.
1079 + $existing_sizes = isset($existing_extras['sizes']) && is_array($existing_extras['sizes'])
1080 + ? $existing_extras['sizes']
1081 + : [];
790 1082
791 - // Upload Image size
792 - if(
793 - !Utils::is_empty($existing_extras) &&
794 - isset($existing_extras['sizes']) && !Utils::is_empty($existing_extras['sizes']) &&
795 - isset($existing_extras['sizes'][$size]) && !Utils::is_empty($existing_extras['sizes'][$size]) &&
796 - $existing_extras['sizes'][$size]['source_path'] == $sub_file_source_path
797 - ) {
798 - $uploaded_sub_image = [
799 - 'success' => true,
800 - 'file_url' => $existing_extras['sizes'][$size]['url'],
801 - 'key' => $existing_extras['sizes'][$size]['key']
802 - ];
803 - } else if(file_exists($sub_file_path)) {
804 - $uploaded_sub_image = Service::instance()->uploadSingle($sub_file_path, $sub_file_source_path, $prefix);
1083 + foreach ($sizes as $size_name => $size_data) {
1084 + if (Utils::is_empty($size_data) && isset($existing_sizes[$size_name])) {
1085 + unset($sizes[$size_name]);
805 1086 }
1087 + }
806 1088
807 - if (
808 - isset($uploaded_sub_image) && !empty($uploaded_sub_image) &&
809 - isset($uploaded_sub_image['success']) && $uploaded_sub_image['success']
810 - ) {
811 - $sub_size['source_path'] = $sub_file_source_path;
812 - $sub_size['url'] = $uploaded_sub_image['file_url'];
813 - $sub_size['key'] = $uploaded_sub_image['key'];
814 - $sub_size['width'] = isset($sub_image['width']) ? $sub_image['width']: 0;
815 - $sub_size['height'] = isset($sub_image['height']) ? $sub_image['height']: 0;
816 - }
817 -
818 - $sizes[$size] = $sub_size;
819 - }
1089 + $extras['sizes'] = array_merge($existing_sizes, $sizes);
820 1090 }
821 -
822 -
823 - if (isset($sizes) && !empty($sizes)) {
824 - $extras['sizes'] = $sizes;
825 - }
826 1091
827 1092 return [
828 1093 'file' => [
829 - 'source_path' => $source_path,
830 - 'url' => $uploaded['file_url'],
831 - 'key' => $uploaded['key'],
1094 + 'source_path' => $source_path,
1095 + 'url' => $uploaded['file_url'],
1096 + 'key' => $uploaded['key'],
1097 + 'original_source_path' => isset($original['source_path']) ? $original['source_path'] : '',
1098 + 'original_key' => isset($original['key']) ? $original['key'] : ''
832 1099 ],
833 1100 'extra' => $extras
834 1101 ];
1102 + }
835 1103
1104 + if (isset($uploaded['success']) && !$uploaded['success']) {
1105 + // Prefer the service's own error message/code when it provided one.
1106 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1107 + 'message' => $uploaded['message'] ?? __('File upload failed', 'media-cloud-sync'),
1108 + 'file' => $file_path,
1109 + 'code' => $uploaded['code'] ?? 500
1110 + ]);
836 1111 }
837 1112
838 1113 return false;
839 1114 }
840 1115
1116 + /**
1117 + * Image subsizes to sync to cloud (after filters). Empty means no subsizes are uploaded.
1118 + *
1119 + * `wpmcs_skip_upload_image_sizes` — (bool) When true, no subsizes are uploaded.
1120 + * `wpmcs_upload_attachment_image_sizes` — (array) Subset of `$attachment_meta['sizes']` to upload; empty skips all.
1121 + *
1122 + * @param array $attachment_meta Full attachment metadata.
1123 + * @param int $attachment_id Attachment post ID.
1124 + * @return array Same shape as attachment meta `sizes` entries.
1125 + */
1126 + private function get_attachment_image_sizes_for_upload($attachment_meta, $attachment_id) {
1127 + $sizes = [];
1128 + if (isset($attachment_meta['sizes']) && is_array($attachment_meta['sizes'])) {
1129 + $sizes = $attachment_meta['sizes'];
1130 + }
841 1131
1132 + if (apply_filters('wpmcs_skip_upload_image_sizes', false, $attachment_id, $attachment_meta)) {
1133 + return [];
1134 + }
1135 +
1136 + $sizes = apply_filters('wpmcs_upload_attachment_image_sizes', $sizes, $attachment_id, $attachment_meta);
1137 +
1138 + return is_array($sizes) ? $sizes : [];
1139 + }
1140 +
842 1141 /**
1142 + * Decide if we should force a reupload based on dims/sizes differences.
1143 + * @since 1.3.6
1144 + * @param array $attachment_meta
1145 + * @param int $attachment_id
1146 + * @param array $existing_extras
1147 + */
1148 + private function shouldForceReupload($attachment_meta, $attachment_id, $existing_extras) {
1149 + $existing_extras = $existing_extras ?? Item::instance()->get_extras($attachment_id, false, 'media_library');
1150 +
1151 + $current_width = isset($attachment_meta['width']) ? (int)$attachment_meta['width'] : 0;
1152 + $current_height = isset($attachment_meta['height']) ? (int)$attachment_meta['height'] : 0;
1153 +
1154 + $existing_width = isset($existing_extras['width']) ? (int)$existing_extras['width'] : 0;
1155 + $existing_height = isset($existing_extras['height']) ? (int)$existing_extras['height'] : 0;
1156 +
1157 + if ($current_width !== $existing_width || $current_height !== $existing_height) {
1158 + return true;
1159 + }
1160 +
1161 + $filtered_sizes = $this->get_attachment_image_sizes_for_upload($attachment_meta, $attachment_id);
1162 + if (empty($filtered_sizes)) {
1163 + return false;
1164 + }
1165 +
1166 + $existing_sizes = isset($existing_extras['sizes']) && is_array($existing_extras['sizes']) ? $existing_extras['sizes'] : [];
1167 + $current_sizes = [];
1168 + foreach ($filtered_sizes as $sname => $smeta) {
1169 + $current_sizes[$sname] = [
1170 + 'width' => isset($smeta['width']) ? (int)$smeta['width'] : 0,
1171 + 'height' => isset($smeta['height']) ? (int)$smeta['height'] : 0,
1172 + ];
1173 + }
1174 +
1175 + $existing_relevant = [];
1176 + foreach (array_keys($current_sizes) as $sname) {
1177 + if (isset($existing_sizes[$sname])) {
1178 + $existing_relevant[$sname] = $existing_sizes[$sname];
1179 + }
1180 + }
1181 +
1182 + if (array_keys($existing_relevant) !== array_keys($current_sizes)) {
1183 + return true;
1184 + }
1185 +
1186 + foreach ($current_sizes as $sname => $dims) {
1187 + if (!isset($existing_sizes[$sname])) {
1188 + return true;
1189 + }
1190 + $ex_w = isset($existing_sizes[$sname]['width']) ? (int)$existing_sizes[$sname]['width'] : 0;
1191 + $ex_h = isset($existing_sizes[$sname]['height']) ? (int)$existing_sizes[$sname]['height'] : 0;
1192 + if ($ex_w !== $dims['width'] || $ex_h !== $dims['height']) {
1193 + return true;
1194 + }
1195 + }
1196 +
1197 + return false;
1198 + }
1199 +
1200 + /**
1201 + * Upload the main/full file.
1202 + * @since 1.3.6
1203 + * @param string $file_path
1204 + * @param string $source_path
1205 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1206 + * @param bool $has_existing
1207 + * @param array $existing
1208 + * @param string $prefix
1209 + * @param int $attachment_id
1210 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1211 + */
1212 + private function uploadFullFile($file_path, $source_path, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private = false) {
1213 + // $do_reupload may be an array of size names (force just those sizes) — that
1214 + // shouldn't force the full file too, only a literal true does.
1215 + if ($do_reupload !== true && $has_existing && ($existing['source_path'] == $source_path)) {
1216 + return [
1217 + 'success' => true,
1218 + 'file_url' => $existing['url'],
1219 + 'key' => $existing['key']
1220 + ];
1221 + }
1222 +
1223 + if (file_exists($file_path)) {
1224 + return Service::instance()->uploadSingle($file_path, $source_path, $prefix, $is_private);
1225 + }
1226 +
1227 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1228 + 'message' => __('File not found', 'media-cloud-sync'),
1229 + 'file' => $file_path,
1230 + 'code' => 404
1231 + ]);
1232 +
1233 + return false;
1234 + }
1235 +
1236 + /**
1237 + * Upload original file when present.
1238 + * @since 1.3.6
1239 + * @param string $original_file
1240 + * @param string $file_dir
1241 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1242 + * @param bool $has_existing
1243 + * @param array $existing
1244 + * @param string $prefix
1245 + * @param int $attachment_id
1246 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1247 + */
1248 + private function uploadOriginalFile($original_file, $file_dir, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private = false) {
1249 + $original = [];
1250 + if (empty($original_file)) {
1251 + return $original;
1252 + }
1253 +
1254 + $original_file_path = trailingslashit($file_dir) . $original_file;
1255 + $original_file_source_path = Utils::get_attachment_source_path($original_file_path);
1256 +
1257 + // $do_reupload may be an array of size names (force just those sizes) — that
1258 + // shouldn't force the original file too, only a literal true does.
1259 + if ($do_reupload !== true && $has_existing && isset($existing['original_source_path']) && !Utils::is_empty($existing['original_source_path']) && $existing['original_source_path'] === $original_file_source_path) {
1260 + return [ 'source_path' => $original_file_source_path, 'key' => $existing['original_key'] ];
1261 + }
1262 +
1263 + if (file_exists($original_file_path)) {
1264 + $uploaded_original = Service::instance()->uploadSingle($original_file_path, $original_file_source_path, $prefix, $is_private);
1265 + if (isset($uploaded_original['success']) && $uploaded_original['success']) {
1266 + return [ 'source_path' => $original_file_source_path, 'key' => $uploaded_original['key'] ];
1267 + }
1268 +
1269 + if (isset($uploaded_original['success']) && !$uploaded_original['success']) {
1270 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1271 + 'message' => $uploaded_original['message'] ?? __('Original File upload failed', 'media-cloud-sync'),
1272 + 'file' => $original_file_path,
1273 + 'code' => $uploaded_original['code'] ?? 500
1274 + ]);
1275 + }
1276 + } else {
1277 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1278 + 'message' => __('Original File not found', 'media-cloud-sync'),
1279 + 'file' => $original_file_path,
1280 + 'code' => 404
1281 + ]);
1282 + }
1283 +
1284 + return $original;
1285 + }
1286 +
1287 + /**
1288 + * Upload intermediate image sizes.
1289 + * @since 1.3.6
1290 + * @param array $attachment_sizes
1291 + * @param string $file_dir
1292 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1293 + * @param array $existing_extras
1294 + * @param string $prefix
1295 + * @param int $attachment_id
1296 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1297 + * @return array
1298 + */
1299 + private function uploadImageSizes($attachment_sizes, $file_dir, $do_reupload, $existing_extras, $prefix, $attachment_id, $is_private = false) {
1300 + $sizes = [];
1301 +
1302 + foreach ($attachment_sizes as $size => $sub_image) {
1303 + $sub_size = [];
1304 + $sub_file = isset($sub_image['file']) ? $sub_image['file'] : false;
1305 + $sub_file_path = '';
1306 + $sub_file_source_path = '';
1307 +
1308 + if ($sub_file) {
1309 + $sub_file_path = $file_dir . '/' . $sub_file;
1310 + $sub_file_source_path = Utils::get_attachment_source_path($sub_file_path);
1311 + }
1312 +
1313 + $uploaded_sub_image = [];
1314 +
1315 + // $do_reupload may be an array naming specific sizes to force (e.g. a
1316 + // WooCommerce on-the-fly regen of one size only) rather than a plain bool.
1317 + $force_this_size = ($do_reupload === true) || (is_array($do_reupload) && in_array($size, $do_reupload, true));
1318 +
1319 + if (
1320 + !$force_this_size &&
1321 + !Utils::is_empty($existing_extras) &&
1322 + isset($existing_extras['sizes']) && !Utils::is_empty($existing_extras['sizes']) &&
1323 + isset($existing_extras['sizes'][$size]) && !Utils::is_empty($existing_extras['sizes'][$size]) &&
1324 + $existing_extras['sizes'][$size]['source_path'] == $sub_file_source_path
1325 + ) {
1326 + $uploaded_sub_image = [
1327 + 'success' => true,
1328 + 'file_url' => $existing_extras['sizes'][$size]['url'],
1329 + 'key' => $existing_extras['sizes'][$size]['key']
1330 + ];
1331 + } else if (file_exists($sub_file_path)) {
1332 + $uploaded_sub_image = Service::instance()->uploadSingle($sub_file_path, $sub_file_source_path, $prefix, $is_private);
1333 + } else {
1334 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1335 + /* translators: %1$s: Image size name (e.g., thumbnail, medium, large). */
1336 + 'message' => sprintf(__('Image size %1$s not found', 'media-cloud-sync'), $size),
1337 + 'file' => $sub_file_path,
1338 + 'code' => 404
1339 + ]);
1340 + }
1341 +
1342 + if (isset($uploaded_sub_image) && !empty($uploaded_sub_image) && isset($uploaded_sub_image['success']) && $uploaded_sub_image['success']) {
1343 + $sub_size['source_path'] = $sub_file_source_path;
1344 + $sub_size['url'] = $uploaded_sub_image['file_url'];
1345 + $sub_size['key'] = $uploaded_sub_image['key'];
1346 + $sub_size['width'] = isset($sub_image['width']) ? $sub_image['width'] : 0;
1347 + $sub_size['height'] = isset($sub_image['height']) ? $sub_image['height'] : 0;
1348 + } else {
1349 + if (isset($uploaded_sub_image['success']) && !$uploaded_sub_image['success']) {
1350 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1351 + /* translators: %1$s: Image size name (e.g., thumbnail, medium, large). */
1352 + 'message' => sprintf(__('Image size %1$s upload failed', 'media-cloud-sync'), $size),
1353 + 'file' => $sub_file_path,
1354 + 'code' => 500
1355 + ]);
1356 + }
1357 +
1358 + // A failed (re)upload attempt shouldn't erase a previously-good record —
1359 + // fall back to what's already stored rather than overwriting it with empty data.
1360 + if (isset($existing_extras['sizes'][$size]) && !Utils::is_empty($existing_extras['sizes'][$size])) {
1361 + $sub_size = $existing_extras['sizes'][$size];
1362 + }
1363 + }
1364 +
1365 + $sizes[$size] = $sub_size;
1366 + }
1367 +
1368 + return $sizes;
1369 + }
1370 +
1371 +
1372 + /**
843 1373 * Get Total Media Count (queried)
844 1374 *
845 1375 * Param added because need a generic function name,
846 1376 * if the function calls by source_type
@@ -861,8 +1391,129 @@
861 1391 }
862 1392
863 1393
864 1394 /**
1395 + * Whether an attachment is still waiting on WordPress-generated subsizes — same check
1396 + * update_attachment_metadata() uses, so callers can tell that apart from a real failure.
1397 + *
1398 + * @param int $attachment_id
1399 + * @return bool
1400 + */
1401 + private function is_attachment_not_yet_ready( $attachment_id ) {
1402 + if ( ! wp_attachment_is_image( $attachment_id ) ) {
1403 + return false;
1404 + }
1405 +
1406 + $attachment_meta = wp_get_attachment_metadata( $attachment_id );
1407 +
1408 + return $this->should_wait_for_subsizes( $attachment_meta, $attachment_id );
1409 + }
1410 +
1411 + /**
1412 + * Upload pending media files.
1413 + *
1414 + * Processes media attachments that are pending and haven't been synced yet.
1415 + *
1416 + * Paginates by `posts.ID` (a stable cursor) rather than a raw SQL OFFSET,
1417 + * so a concurrent single-item retry (which can remove an arbitrary item
1418 + * from the middle of the pending set, not just the front) can't desync
1419 + * which row gets picked up next.
1420 + *
1421 + * @param string $source_type Source type for identifying which source..
1422 + * @param int $limit Number of media to process. Default is 50.
1423 + * @param int $after_id Only consider attachments with ID greater than this cursor. Default is 0.
1424 + * @return array Status array with success, failed count, and the last attachment ID considered.
1425 + */
1426 + public function upload_pending_media( $source_type, $limit = 50, $after_id = 0 ) {
1427 + global $wpdb;
1428 +
1429 + $failed = 0;
1430 +
1431 + $source_type_data = self::$source_types[ $source_type ] ?? false;
1432 + if ( ! $source_type_data ) {
1433 + return [ 'success' => true, 'failed' => $limit, 'last_id' => $after_id ];
1434 + }
1435 +
1436 + $posts = $wpdb->prefix . $source_type_data['table'];
1437 + $items = Db::get_table_name();
1438 +
1439 + $join = "
1440 + items.source_id = posts.ID
1441 + AND items.source_type = %s
1442 + AND items.provider = %s
1443 + AND items.storage = %s
1444 + ";
1445 +
1446 + $params = [
1447 + $source_type,
1448 + $this->service,
1449 + $this->bucket_name,
1450 + ];
1451 +
1452 + if ( ! empty( $this->region ) ) {
1453 + $join .= " AND items.region = %s";
1454 + $params[] = $this->region;
1455 + }
1456 +
1457 + $after_id_sql = '';
1458 + if ( $after_id > 0 ) {
1459 + $after_id_sql = 'AND posts.ID > %d';
1460 + $params[] = (int) $after_id;
1461 + }
1462 +
1463 + $sql = "
1464 + SELECT posts.ID
1465 + FROM {$posts} AS posts
1466 + LEFT JOIN {$items} AS items USE INDEX (idx_item_lookup)
1467 + ON {$join}
1468 + WHERE posts.post_type = 'attachment'
1469 + AND items.source_id IS NULL
1470 + {$after_id_sql}
1471 + ORDER BY posts.ID ASC
1472 + LIMIT %d
1473 + ";
1474 +
1475 + $params[] = (int) $limit;
1476 +
1477 + $rows = $wpdb->get_results( $wpdb->prepare( $sql, $params ) );
1478 +
1479 + if ( empty( $rows ) ) {
1480 + return [ 'success' => true, 'failed' => 0, 'last_id' => $after_id ];
1481 + }
1482 +
1483 + $last_id = $after_id;
1484 + foreach ( $rows as $row ) {
1485 + $attachment_id = (int) $row->ID;
1486 +
1487 + // Still waiting on WordPress to finish generating subsizes — not a failure,
1488 + // just not ready yet. Leave $last_id alone so it's reconsidered next tick.
1489 + if ( $this->is_attachment_not_yet_ready( $attachment_id ) ) {
1490 + break;
1491 + }
1492 +
1493 + $last_id = $attachment_id;
1494 +
1495 + try {
1496 + $this->upload_single_media( $attachment_id, $source_type );
1497 + } catch ( \Throwable $e ) {
1498 + $failed++;
1499 + continue;
1500 + }
1501 +
1502 + if ( ! Item::instance()->get( $attachment_id, $source_type ) ) {
1503 + $failed++;
1504 + }
1505 + }
1506 +
1507 + return [
1508 + 'success' => true,
1509 + 'failed' => $failed,
1510 + 'last_id' => $last_id,
1511 + ];
1512 + }
1513 +
1514 +
1515 + /**
865 1516 * Edit Image Meta In View Image
866 1517 * @since 1.0.0
867 1518 */
868 1519 function attachment_submitbox_metadata( ) {
@@ -884,9 +1535,9 @@
884 1535 $provider = $wpmcsItem->get_field($attachment_id, 'provider', 'media_library');
885 1536 if($provider) {
886 1537 $label = Schema::getServiceLabels($provider); ?>
887 1538 <div class="misc-pub-section misc-pub-provider">
888 - <?php esc_html_e( 'Provider :', 'media-cloud-sync' ); ?> <strong><?php echo esc_textarea(!empty($label) ? $label : $provider); ?></strong></a>
1539 + <?php esc_html_e( 'Provider :', 'media-cloud-sync' ); ?> <strong><?php echo esc_textarea(!empty($label) ? $label : $provider); ?></strong>
889 1540 </div>
890 1541 <?php
891 1542 }
892 1543
@@ -892,9 +1543,9 @@
892 1543
893 1544 $region = $wpmcsItem->get_field($attachment_id, 'region', 'media_library');
894 1545 if($region) { ?>
895 1546 <div class="misc-pub-section misc-pub-provider">
896 - <?php esc_html_e( 'Region :', 'media-cloud-sync' ); ?> <strong><?php echo esc_textarea($region); ?></strong></a>
1547 + <?php esc_html_e( 'Region :', 'media-cloud-sync' ); ?> <strong><?php echo esc_textarea($region); ?></strong>
897 1548 </div>
898 1549 <?php
899 1550 }
900 1551 $private = (int)$wpmcsItem->get_field($attachment_id, 'is_private', 'media_library');
@@ -899,9 +1550,9 @@
899 1550 }
900 1551 $private = (int)$wpmcsItem->get_field($attachment_id, 'is_private', 'media_library');
901 1552 ?>
902 1553 <div class="misc-pub-section misc-pub-provider">
903 - <?php esc_html_e( 'Access :', 'media-cloud-sync' ); ?> <strong><?php $private ? esc_html_e( 'Private', 'media-cloud-sync' ) : esc_html_e( 'Public', 'media-cloud-sync' ); ?></strong></a>
1554 + <?php esc_html_e( 'Access :', 'media-cloud-sync' ); ?> <strong><?php $private ? esc_html_e( 'Private', 'media-cloud-sync' ) : esc_html_e( 'Public', 'media-cloud-sync' ); ?></strong>
904 1555 </div>
905 1556 <?php
906 1557 }
907 1558
@@ -907,8 +1558,10 @@
907 1558
908 1559
909 1560 /**
910 1561 * Function to get attachment details by ID
1562 + *
1563 + * @since 1.3.12 Added capability checks for attachment access.
911 1564 */
912 1565 public function ajax_get_attachment_details() {
913 1566 $result = array(
914 1567 'status' => false,
@@ -915,8 +1568,12 @@
915 1568 'data' => array(),
916 1569 'exclude' => false
917 1570 );
918 1571
1572 + if(!Utils::is_service_enabled()) {
1573 + wp_send_json_success( $result );
1574 + }
1575 +
919 1576 if ( ! isset( $_POST['id'] ) ) {
920 1577 wp_send_json_success( $result );
921 1578 }
922 1579
@@ -921,9 +1578,18 @@
921 1578 }
922 1579
923 1580 check_ajax_referer( 'get_media_provider_details', '_nonce' );
924 1581
925 - $id= intval( sanitize_text_field( $_POST['id'] ) );
1582 + if ( ! current_user_can( 'upload_files' ) ) {
1583 + wp_send_json_error();
1584 + }
1585 +
1586 + $id = absint( wp_unslash( $_POST['id'] ) );
1587 + $post = get_post( $id );
1588 +
1589 + if ( ! $post || 'attachment' !== $post->post_type || ! current_user_can( 'edit_post', $id ) ) {
1590 + wp_send_json_success( $result );
1591 + }
926 1592
927 1593 // Return if extension not allowed
928 1594 $path = get_attached_file( $id );
929 1595 if(!Utils::is_extension_available($path)) {
@@ -951,9 +1617,9 @@
951 1617 $region = $wpmcsItem->get_field($id, 'region', 'media_library');
952 1618 if($region){
953 1619 $item['region'] = $region;
954 1620 }
955 - $item['private'] = $wpmcsItem->get_field($id, 'private', 'media_library');
1621 + $item['private'] = (int) $wpmcsItem->get_field($id, 'is_private', 'media_library');
956 1622 if($item) {
957 1623 $result= array(
958 1624 'status' => true,
959 1625 'data' => $item
@@ -988,8 +1654,41 @@
988 1654 * Note: delete_post is used as there is a potential that deleted_post is not reached.
989 1655 */
990 1656 public function delete_post() {
991 1657 $this->deleting_attachment = false;
1658 + }
1659 +
1660 + /**
1661 + * Safety net for deletion that bypasses wp_delete_attachment().
1662 + *
1663 + * @handles deleted_post
1664 + */
1665 + public function deleted_post( $post_id, $post ) {
1666 + if ( ! $post || 'attachment' !== $post->post_type ) {
1667 + return;
1668 + }
1669 +
1670 + $item = Item::instance()->get( $post_id, 'media_library' );
1671 + if ( Utils::is_empty( $item ) ) {
1672 + return;
1673 + }
1674 +
1675 + Item::instance()->delete( $post_id, 'media_library' );
1676 + }
1677 +
1678 + /**
1679 + * Has WP Core fixed wp_check_filetype when URL has params yet?
1680 + *
1681 + * @see https://core.trac.wordpress.org/ticket/30377
1682 + * @see https://github.com/aaemnnosttv/fix-wp-media-shortcodes-with-params/blob/master/fix-wp-media-shortcodes-with-params.php
1683 + *
1684 + * @return bool
1685 + */
1686 + public static function wp_check_filetype_broken() {
1687 + $normal_file = wp_check_filetype( 'file.mp4', array( 'mp4' => 'video/mp4' ) );
1688 + $querys_file = wp_check_filetype( 'file.mp4?param=1', array( 'mp4' => 'video/mp4' ) );
1689 +
1690 + return $normal_file !== $querys_file;
992 1691 }
993 1692
994 1693 /**
995 1694 * Is installed?