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 +196 -73 1.3.101.4.1 View file →
@@ -5,8 +5,15 @@
5 5
6 6 use Exception;
7 7 use WP_Error;
8 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 + */
9 16 class MediaLibrary {
10 17 private static $instance = null;
11 18 private $assets_url;
12 19 private $version;
@@ -24,10 +31,13 @@
24 31 private $deleting_attachment = false;
25 32
26 33
27 34 public static $source_type_prefix = "media";
28 - public static $label = 'Media Library';
29 35
36 + public static function get_label() {
37 + return __('Media Library', 'media-cloud-sync');
38 + }
39 +
30 40 // Map which meta to be updated
31 41 public static $source_types = [
32 42 "media_library" => [
33 43 'table' => 'posts',
@@ -106,8 +116,10 @@
106 116 add_filter( 'wp_update_attachment_metadata', [ $this, 'update_attachment_metadata' ], 110, 2 );
107 117 add_filter( 'pre_delete_attachment', [ $this, 'pre_delete_attachment' ], 20 );
108 118 add_filter( 'delete_attachment', [ $this, 'delete_attachment' ], 20 );
109 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 );
110 122 add_filter( 'update_attached_file', [ $this, 'update_attached_file' ], 100, 2 );
111 123
112 124 add_action( 'wpmcs_do_update_attachment_metadata', [ $this, 'update_attachment_metadata' ], 10, 2 );
113 125 }
@@ -159,12 +171,8 @@
159 171 * @since 1.0.0
160 172 *
161 173 */
162 174 public function delete_attachment($attachment_id){
163 - if (!Utils::is_service_enabled()) {
164 - return $attachment_id;
165 - }
166 -
167 175 $wpmcsItem = Item::instance();
168 176 $item = $wpmcsItem->get($attachment_id, 'media_library');
169 177
170 178 if (Utils::is_empty($item)) {
@@ -187,8 +195,13 @@
187 195 */
188 196 public function update_attachment_metadata($attachment_meta, $attachment_id) {
189 197 // Remove Logs
190 198 Logger::instance()->remove_log('sync_to_cloud', $attachment_id, 'media_library');
199 +
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.
191 204 try {
192 205 // Ensure attachment is eligible for upload
193 206 $attachment_meta = is_array($attachment_meta) && !empty($attachment_meta)
194 207 ? $attachment_meta
@@ -375,9 +388,9 @@
375 388 'key' => $backup['key'],
376 389 'original_source_path' => $backup['original_source_path'],
377 390 'original_key' => $backup['original_key'],
378 391 ],
379 - 'extra' => Utilsmaybe_unserialize($backup['extra']),
392 + 'extra' => Utils::maybe_unserialize($backup['extra']),
380 393 ];
381 394 }
382 395
383 396 if ($delete_existing) {
@@ -432,12 +445,8 @@
432 445 * @since 1.0.0
433 446 * @return string
434 447 */
435 448 private function filter_unique_filename($filename, $ext, $dir, $post_id = null) {
436 - if (!Utils::is_service_enabled()) {
437 - return $filename;
438 - }
439 -
440 449 // sanitize the file name before we begin processing
441 450 $filename = sanitize_file_name($filename);
442 451 $ext = strtolower($ext);
443 452 $name = wp_basename($filename, $ext);
@@ -552,9 +561,9 @@
552 561 if ( $this->deleting_attachment ) {
553 562 return $file;
554 563 }
555 564
556 - if (!Utils::is_ok_to_serve($attachment_id)) {
565 + if (!Utils::should_serve_from_cloud($attachment_id)) {
557 566 return $file;
558 567 }
559 568
560 569 $wpmcsItem = Item::instance();
@@ -607,9 +616,9 @@
607 616 return $sources;
608 617 }
609 618
610 619 // Must need $attachment_id other wise not possible to get data from the table
611 - if (!Utils::is_ok_to_serve($attachment_id)) {
620 + if (!Utils::should_serve_from_cloud($attachment_id)) {
612 621 return $sources;
613 622 }
614 623
615 624 $wpmcsItem = Item::instance();
@@ -642,22 +651,26 @@
642 651 *
643 652 * @return null|string
644 653 */
645 654 protected function find_image_size_from_width( $meta, $width, $filename ) {
646 - $sizes = $meta['sizes'];
647 - foreach ( $sizes as $name => $size ) {
648 - if ( $width === absint( $size['width'] ) && $size['file'] === $filename ) {
649 - return $name;
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 + }
650 664 }
651 665 }
652 666
653 - // Check for full size
654 - if($width === $meta['width']) {
655 - $full_file = wp_basename( $meta['file'] );
656 - if($full_file === $filename) {
657 - return 'full';
658 - }
659 - }
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 + }
660 673
661 674 return null;
662 675 }
663 676
@@ -672,10 +685,10 @@
672 685 * @return array
673 686 */
674 687 public function wp_get_attachment_image_attributes($attr, $attachment, $size='thumbnail') {
675 688 if (
676 - !$attachment ||
677 - !Utils::is_ok_to_serve($attachment->ID)
689 + !$attachment ||
690 + !Utils::should_serve_from_cloud($attachment->ID)
678 691 ) {
679 692 return $attr;
680 693 }
681 694
@@ -722,9 +735,9 @@
722 735 *
723 736 * @return bool|mixed|WP_Error
724 737 */
725 738 public function wp_get_attachment_url($url, $attachment_id) {
726 - if (!Utils::is_ok_to_serve($attachment_id)) {
739 + if (!Utils::should_serve_from_cloud($attachment_id)) {
727 740 return $url;
728 741 }
729 742
730 743 $new_url = Item::instance()->get_url($attachment_id, 'full', 'media_library');
@@ -749,14 +762,14 @@
749 762 * @param string $size
750 763 *
751 764 * @return string
752 765 */
753 - public function get_image_tag( $html, $id, $alt, $title, $align, $size ) {
766 + public function get_image_tag( $html, $id, $alt = '', $title = '', $align = '', $size = '' ) {
754 767 if ( ! is_string( $html ) ) {
755 768 return $html;
756 769 }
757 770
758 - if (!Utils::is_ok_to_serve($id)) {
771 + if (!Utils::should_serve_from_cloud($id)) {
759 772 return $html;
760 773 }
761 774
762 775 preg_match( '@\ssrc=[\'\"]([^\'\"]*)[\'\"]@', $html, $matches );
@@ -765,8 +778,16 @@
765 778 // Can't establish img src
766 779 return $html;
767 780 }
768 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 +
769 790 $img_src = $matches[1];
770 791 $size = Utils::maybe_convert_size_to_string( $id, $size );
771 792 $new_url = Item::instance()->get_url($id, $size, 'media_library');
772 793
@@ -788,9 +809,9 @@
788 809 *
789 810 * @return array
790 811 */
791 812 public function wp_get_attachment_image_src( $image, $attachment_id, $size, $icon ) {
792 - if (!Utils::is_ok_to_serve($attachment_id)) {
813 + if (!Utils::should_serve_from_cloud($attachment_id)) {
793 814 return $image;
794 815 }
795 816
796 817 if ( isset( $image[0] ) ) {
@@ -816,9 +837,9 @@
816 837 *
817 838 * @return array
818 839 */
819 840 public function wp_prepare_attachment_for_js( $response, $attachment, $meta ) {
820 - if (!Utils::is_ok_to_serve($attachment->ID)) {
841 + if (!Utils::should_serve_from_cloud($attachment->ID)) {
821 842 return $response;
822 843 }
823 844
824 845 if ( isset( $response['sizes'] ) && is_array( $response['sizes'] ) ) {
@@ -844,9 +865,9 @@
844 865 *
845 866 * @return array
846 867 */
847 868 public function image_get_intermediate_size( $data, $post_id, $size ) {
848 - if (!Utils::is_ok_to_serve($post_id)) {
869 + if (!Utils::should_serve_from_cloud($post_id)) {
849 870 return $data;
850 871 }
851 872 if ( isset( $data['url'] ) ) {
852 873 $size = Utils::maybe_convert_size_to_string( $post_id, $size );
@@ -996,8 +1017,11 @@
996 1017 $is_image = wp_attachment_is_image($attachment_id);
997 1018 $existing = $wpmcsItem->get($attachment_id, 'media_library');
998 1019 $existing_extras = $wpmcsItem->get_extras($attachment_id, false, 'media_library');
999 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']);
1000 1024 $sizes = [];
1001 1025 $uploaded = [];
1002 1026 $extras = [];
1003 1027 $prefix = '';
@@ -1027,35 +1051,43 @@
1027 1051 $extras['width'] = (isset($attachment_meta['width']) && !empty($attachment_meta['width'])) ? $attachment_meta['width'] : 0;
1028 1052 $extras['height'] = (isset($attachment_meta['height']) && !empty($attachment_meta['height'])) ? $attachment_meta['height'] : 0;
1029 1053 }
1030 1054
1031 - // Check whether the extension is enabled for uploading
1055 + // Excluded by extension settings — an intentional skip, not a sync failure, so no log entry.
1032 1056 if (!Utils::is_extension_available($file_path)) {
1033 - Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1034 - 'message' => __('File extension is not supported', 'media-cloud-sync'),
1035 - 'file' => $file_path,
1036 - 'code' => 415
1037 - ]);
1038 1057 return false;
1039 1058 }
1040 1059
1041 1060 // Upload main file first, if that fails no need to attempt the rest
1042 - $uploaded = $this->uploadFullFile($file_path, $source_path, $do_reupload, $has_existing, $existing, $prefix, $attachment_id);
1061 + $uploaded = $this->uploadFullFile($file_path, $source_path, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private);
1043 1062
1044 1063 if ($uploaded && isset($uploaded['success']) && $uploaded['success']) {
1045 1064 // Attempt to upload original file if present. Not critical if this fails so we don't check the result before proceeding.
1046 - $original = $this->uploadOriginalFile($original_file, $file_dir, $do_reupload, $has_existing, $existing, $prefix, $attachment_id);
1065 + $original = $this->uploadOriginalFile($original_file, $file_dir, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private);
1047 1066
1048 1067 if ($is_image && isset($file_dir) && !empty($file_dir)) {
1049 1068 $sizes_to_upload = $this->get_attachment_image_sizes_for_upload($attachment_meta, $attachment_id);
1050 1069 if (!empty($sizes_to_upload)) {
1051 1070 // Upload image sizes. Again, not critical if this fails so we don't check the result before proceeding.
1052 - $sizes = $this->uploadImageSizes($sizes_to_upload, $file_dir, $do_reupload, $existing_extras, $prefix, $attachment_id);
1071 + $sizes = $this->uploadImageSizes($sizes_to_upload, $file_dir, $do_reupload, $existing_extras, $prefix, $attachment_id, $is_private);
1053 1072 }
1054 1073 }
1055 1074
1056 1075 if (!empty($sizes)) {
1057 - $extras['sizes'] = $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 + : [];
1082 +
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]);
1086 + }
1087 + }
1088 +
1089 + $extras['sizes'] = array_merge($existing_sizes, $sizes);
1058 1090 }
1059 1091
1060 1092 return [
1061 1093 'file' => [
@@ -1069,12 +1101,13 @@
1069 1101 ];
1070 1102 }
1071 1103
1072 1104 if (isset($uploaded['success']) && !$uploaded['success']) {
1105 + // Prefer the service's own error message/code when it provided one.
1073 1106 Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1074 - 'message' => __('File upload failed', 'media-cloud-sync'),
1107 + 'message' => $uploaded['message'] ?? __('File upload failed', 'media-cloud-sync'),
1075 1108 'file' => $file_path,
1076 - 'code' => 500
1109 + 'code' => $uploaded['code'] ?? 500
1077 1110 ]);
1078 1111 }
1079 1112
1080 1113 return false;
@@ -1168,16 +1201,19 @@
1168 1201 * Upload the main/full file.
1169 1202 * @since 1.3.6
1170 1203 * @param string $file_path
1171 1204 * @param string $source_path
1172 - * @param bool $do_reupload
1205 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1173 1206 * @param bool $has_existing
1174 1207 * @param array $existing
1175 1208 * @param string $prefix
1176 1209 * @param int $attachment_id
1210 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1177 1211 */
1178 - private function uploadFullFile($file_path, $source_path, $do_reupload, $has_existing, $existing, $prefix, $attachment_id) {
1179 - if (!$do_reupload && $has_existing && ($existing['source_path'] == $source_path)) {
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)) {
1180 1216 return [
1181 1217 'success' => true,
1182 1218 'file_url' => $existing['url'],
1183 1219 'key' => $existing['key']
@@ -1184,9 +1220,9 @@
1184 1220 ];
1185 1221 }
1186 1222
1187 1223 if (file_exists($file_path)) {
1188 - return Service::instance()->uploadSingle($file_path, $source_path, $prefix);
1224 + return Service::instance()->uploadSingle($file_path, $source_path, $prefix, $is_private);
1189 1225 }
1190 1226
1191 1227 Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1192 1228 'message' => __('File not found', 'media-cloud-sync'),
@@ -1201,15 +1237,16 @@
1201 1237 * Upload original file when present.
1202 1238 * @since 1.3.6
1203 1239 * @param string $original_file
1204 1240 * @param string $file_dir
1205 - * @param bool $do_reupload
1241 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1206 1242 * @param bool $has_existing
1207 1243 * @param array $existing
1208 1244 * @param string $prefix
1209 1245 * @param int $attachment_id
1246 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1210 1247 */
1211 - private function uploadOriginalFile($original_file, $file_dir, $do_reupload, $has_existing, $existing, $prefix, $attachment_id) {
1248 + private function uploadOriginalFile($original_file, $file_dir, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private = false) {
1212 1249 $original = [];
1213 1250 if (empty($original_file)) {
1214 1251 return $original;
1215 1252 }
@@ -1216,14 +1253,16 @@
1216 1253
1217 1254 $original_file_path = trailingslashit($file_dir) . $original_file;
1218 1255 $original_file_source_path = Utils::get_attachment_source_path($original_file_path);
1219 1256
1220 - 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) {
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) {
1221 1260 return [ 'source_path' => $original_file_source_path, 'key' => $existing['original_key'] ];
1222 1261 }
1223 1262
1224 1263 if (file_exists($original_file_path)) {
1225 - $uploaded_original = Service::instance()->uploadSingle($original_file_path, $original_file_source_path, $prefix);
1264 + $uploaded_original = Service::instance()->uploadSingle($original_file_path, $original_file_source_path, $prefix, $is_private);
1226 1265 if (isset($uploaded_original['success']) && $uploaded_original['success']) {
1227 1266 return [ 'source_path' => $original_file_source_path, 'key' => $uploaded_original['key'] ];
1228 1267 }
1229 1268
@@ -1228,11 +1267,11 @@
1228 1267 }
1229 1268
1230 1269 if (isset($uploaded_original['success']) && !$uploaded_original['success']) {
1231 1270 Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1232 - 'message' => __('Original File upload failed', 'media-cloud-sync'),
1271 + 'message' => $uploaded_original['message'] ?? __('Original File upload failed', 'media-cloud-sync'),
1233 1272 'file' => $original_file_path,
1234 - 'code' => 500
1273 + 'code' => $uploaded_original['code'] ?? 500
1235 1274 ]);
1236 1275 }
1237 1276 } else {
1238 1277 Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
@@ -1249,15 +1288,16 @@
1249 1288 * Upload intermediate image sizes.
1250 1289 * @since 1.3.6
1251 1290 * @param array $attachment_sizes
1252 1291 * @param string $file_dir
1253 - * @param bool $do_reupload
1292 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1254 1293 * @param array $existing_extras
1255 1294 * @param string $prefix
1256 1295 * @param int $attachment_id
1257 - * @return array
1296 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1297 + * @return array
1258 1298 */
1259 - private function uploadImageSizes($attachment_sizes, $file_dir, $do_reupload, $existing_extras, $prefix, $attachment_id) {
1299 + private function uploadImageSizes($attachment_sizes, $file_dir, $do_reupload, $existing_extras, $prefix, $attachment_id, $is_private = false) {
1260 1300 $sizes = [];
1261 1301
1262 1302 foreach ($attachment_sizes as $size => $sub_image) {
1263 1303 $sub_size = [];
@@ -1271,10 +1311,14 @@
1271 1311 }
1272 1312
1273 1313 $uploaded_sub_image = [];
1274 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 +
1275 1319 if (
1276 - !$do_reupload &&
1320 + !$force_this_size &&
1277 1321 !Utils::is_empty($existing_extras) &&
1278 1322 isset($existing_extras['sizes']) && !Utils::is_empty($existing_extras['sizes']) &&
1279 1323 isset($existing_extras['sizes'][$size]) && !Utils::is_empty($existing_extras['sizes'][$size]) &&
1280 1324 $existing_extras['sizes'][$size]['source_path'] == $sub_file_source_path
@@ -1284,9 +1328,9 @@
1284 1328 'file_url' => $existing_extras['sizes'][$size]['url'],
1285 1329 'key' => $existing_extras['sizes'][$size]['key']
1286 1330 ];
1287 1331 } else if (file_exists($sub_file_path)) {
1288 - $uploaded_sub_image = Service::instance()->uploadSingle($sub_file_path, $sub_file_source_path, $prefix);
1332 + $uploaded_sub_image = Service::instance()->uploadSingle($sub_file_path, $sub_file_source_path, $prefix, $is_private);
1289 1333 } else {
1290 1334 Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1291 1335 /* translators: %1$s: Image size name (e.g., thumbnail, medium, large). */
1292 1336 'message' => sprintf(__('Image size %1$s not found', 'media-cloud-sync'), $size),
@@ -1309,8 +1353,14 @@
1309 1353 'file' => $sub_file_path,
1310 1354 'code' => 500
1311 1355 ]);
1312 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 + }
1313 1363 }
1314 1364
1315 1365 $sizes[$size] = $sub_size;
1316 1366 }
@@ -1341,18 +1391,40 @@
1341 1391 }
1342 1392
1343 1393
1344 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 + /**
1345 1412 * Upload pending media files.
1346 1413 *
1347 1414 * Processes media attachments that are pending and haven't been synced yet.
1348 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 + *
1349 1421 * @param string $source_type Source type for identifying which source..
1350 - * @param int $limit Number of media to process. Default is 50.
1351 - * @param int $offset Offset for media query. Default is 0.
1352 - * @return array Status array with success and failed media IDs.
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.
1353 1425 */
1354 - public function upload_pending_media( $source_type, $limit = 50, $offset = 0 ) {
1426 + public function upload_pending_media( $source_type, $limit = 50, $after_id = 0 ) {
1355 1427 global $wpdb;
1356 1428
1357 1429 $failed = 0;
1358 1430
@@ -1357,9 +1429,9 @@
1357 1429 $failed = 0;
1358 1430
1359 1431 $source_type_data = self::$source_types[ $source_type ] ?? false;
1360 1432 if ( ! $source_type_data ) {
1361 - return [ 'success' => true, 'failed' => $limit ];
1433 + return [ 'success' => true, 'failed' => $limit, 'last_id' => $after_id ];
1362 1434 }
1363 1435
1364 1436 $posts = $wpdb->prefix . $source_type_data['table'];
1365 1437 $items = Db::get_table_name();
@@ -1381,8 +1453,14 @@
1381 1453 $join .= " AND items.region = %s";
1382 1454 $params[] = $this->region;
1383 1455 }
1384 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 +
1385 1463 $sql = "
1386 1464 SELECT posts.ID
1387 1465 FROM {$posts} AS posts
1388 1466 LEFT JOIN {$items} AS items USE INDEX (idx_item_lookup)
@@ -1388,30 +1466,41 @@
1388 1466 LEFT JOIN {$items} AS items USE INDEX (idx_item_lookup)
1389 1467 ON {$join}
1390 1468 WHERE posts.post_type = 'attachment'
1391 1469 AND items.source_id IS NULL
1470 + {$after_id_sql}
1392 1471 ORDER BY posts.ID ASC
1393 - LIMIT %d OFFSET %d
1472 + LIMIT %d
1394 1473 ";
1395 1474
1396 1475 $params[] = (int) $limit;
1397 - $params[] = (int) $offset;
1398 1476
1399 1477 $rows = $wpdb->get_results( $wpdb->prepare( $sql, $params ) );
1400 1478
1401 1479 if ( empty( $rows ) ) {
1402 - return [ 'success' => true, 'failed' => 0 ];
1480 + return [ 'success' => true, 'failed' => 0, 'last_id' => $after_id ];
1403 1481 }
1404 1482
1483 + $last_id = $after_id;
1405 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 +
1406 1495 try {
1407 - $this->upload_single_media( (int) $row->ID, $source_type );
1496 + $this->upload_single_media( $attachment_id, $source_type );
1408 1497 } catch ( \Throwable $e ) {
1409 1498 $failed++;
1410 1499 continue;
1411 1500 }
1412 1501
1413 - if ( ! Item::instance()->get( (int) $row->ID, $source_type ) ) {
1502 + if ( ! Item::instance()->get( $attachment_id, $source_type ) ) {
1414 1503 $failed++;
1415 1504 }
1416 1505 }
1417 1506
@@ -1417,8 +1506,9 @@
1417 1506
1418 1507 return [
1419 1508 'success' => true,
1420 1509 'failed' => $failed,
1510 + 'last_id' => $last_id,
1421 1511 ];
1422 1512 }
1423 1513
1424 1514
@@ -1445,9 +1535,9 @@
1445 1535 $provider = $wpmcsItem->get_field($attachment_id, 'provider', 'media_library');
1446 1536 if($provider) {
1447 1537 $label = Schema::getServiceLabels($provider); ?>
1448 1538 <div class="misc-pub-section misc-pub-provider">
1449 - <?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>
1450 1540 </div>
1451 1541 <?php
1452 1542 }
1453 1543
@@ -1453,9 +1543,9 @@
1453 1543
1454 1544 $region = $wpmcsItem->get_field($attachment_id, 'region', 'media_library');
1455 1545 if($region) { ?>
1456 1546 <div class="misc-pub-section misc-pub-provider">
1457 - <?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>
1458 1548 </div>
1459 1549 <?php
1460 1550 }
1461 1551 $private = (int)$wpmcsItem->get_field($attachment_id, 'is_private', 'media_library');
@@ -1460,9 +1550,9 @@
1460 1550 }
1461 1551 $private = (int)$wpmcsItem->get_field($attachment_id, 'is_private', 'media_library');
1462 1552 ?>
1463 1553 <div class="misc-pub-section misc-pub-provider">
1464 - <?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>
1465 1555 </div>
1466 1556 <?php
1467 1557 }
1468 1558
@@ -1468,8 +1558,10 @@
1468 1558
1469 1559
1470 1560 /**
1471 1561 * Function to get attachment details by ID
1562 + *
1563 + * @since 1.3.12 Added capability checks for attachment access.
1472 1564 */
1473 1565 public function ajax_get_attachment_details() {
1474 1566 $result = array(
1475 1567 'status' => false,
@@ -1476,8 +1568,12 @@
1476 1568 'data' => array(),
1477 1569 'exclude' => false
1478 1570 );
1479 1571
1572 + if(!Utils::is_service_enabled()) {
1573 + wp_send_json_success( $result );
1574 + }
1575 +
1480 1576 if ( ! isset( $_POST['id'] ) ) {
1481 1577 wp_send_json_success( $result );
1482 1578 }
1483 1579
@@ -1482,9 +1578,18 @@
1482 1578 }
1483 1579
1484 1580 check_ajax_referer( 'get_media_provider_details', '_nonce' );
1485 1581
1486 - $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 + }
1487 1592
1488 1593 // Return if extension not allowed
1489 1594 $path = get_attached_file( $id );
1490 1595 if(!Utils::is_extension_available($path)) {
@@ -1512,9 +1617,9 @@
1512 1617 $region = $wpmcsItem->get_field($id, 'region', 'media_library');
1513 1618 if($region){
1514 1619 $item['region'] = $region;
1515 1620 }
1516 - $item['private'] = $wpmcsItem->get_field($id, 'private', 'media_library');
1621 + $item['private'] = (int) $wpmcsItem->get_field($id, 'is_private', 'media_library');
1517 1622 if($item) {
1518 1623 $result= array(
1519 1624 'status' => true,
1520 1625 'data' => $item
@@ -1549,8 +1654,26 @@
1549 1654 * Note: delete_post is used as there is a potential that deleted_post is not reached.
1550 1655 */
1551 1656 public function delete_post() {
1552 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' );
1553 1676 }
1554 1677
1555 1678 /**
1556 1679 * Has WP Core fixed wp_check_filetype when URL has params yet?