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 / integrations / media-library.php

media-library.php in Media Cloud Sync trunk, at includes/integrations/media-library.php

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