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 +191 -64 1.3.111.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 }
@@ -183,8 +195,13 @@
183 195 */
184 196 public function update_attachment_metadata($attachment_meta, $attachment_id) {
185 197 // Remove Logs
186 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.
187 204 try {
188 205 // Ensure attachment is eligible for upload
189 206 $attachment_meta = is_array($attachment_meta) && !empty($attachment_meta)
190 207 ? $attachment_meta
@@ -544,9 +561,9 @@
544 561 if ( $this->deleting_attachment ) {
545 562 return $file;
546 563 }
547 564
548 - if (!Utils::is_ok_to_serve($attachment_id)) {
565 + if (!Utils::should_serve_from_cloud($attachment_id)) {
549 566 return $file;
550 567 }
551 568
552 569 $wpmcsItem = Item::instance();
@@ -599,9 +616,9 @@
599 616 return $sources;
600 617 }
601 618
602 619 // Must need $attachment_id other wise not possible to get data from the table
603 - if (!Utils::is_ok_to_serve($attachment_id)) {
620 + if (!Utils::should_serve_from_cloud($attachment_id)) {
604 621 return $sources;
605 622 }
606 623
607 624 $wpmcsItem = Item::instance();
@@ -634,22 +651,26 @@
634 651 *
635 652 * @return null|string
636 653 */
637 654 protected function find_image_size_from_width( $meta, $width, $filename ) {
638 - $sizes = $meta['sizes'];
639 - foreach ( $sizes as $name => $size ) {
640 - if ( $width === absint( $size['width'] ) && $size['file'] === $filename ) {
641 - 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 + }
642 664 }
643 665 }
644 666
645 - // Check for full size
646 - if($width === $meta['width']) {
647 - $full_file = wp_basename( $meta['file'] );
648 - if($full_file === $filename) {
649 - return 'full';
650 - }
651 - }
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 + }
652 673
653 674 return null;
654 675 }
655 676
@@ -664,10 +685,10 @@
664 685 * @return array
665 686 */
666 687 public function wp_get_attachment_image_attributes($attr, $attachment, $size='thumbnail') {
667 688 if (
668 - !$attachment ||
669 - !Utils::is_ok_to_serve($attachment->ID)
689 + !$attachment ||
690 + !Utils::should_serve_from_cloud($attachment->ID)
670 691 ) {
671 692 return $attr;
672 693 }
673 694
@@ -714,9 +735,9 @@
714 735 *
715 736 * @return bool|mixed|WP_Error
716 737 */
717 738 public function wp_get_attachment_url($url, $attachment_id) {
718 - if (!Utils::is_ok_to_serve($attachment_id)) {
739 + if (!Utils::should_serve_from_cloud($attachment_id)) {
719 740 return $url;
720 741 }
721 742
722 743 $new_url = Item::instance()->get_url($attachment_id, 'full', 'media_library');
@@ -741,14 +762,14 @@
741 762 * @param string $size
742 763 *
743 764 * @return string
744 765 */
745 - public function get_image_tag( $html, $id, $alt, $title, $align, $size ) {
766 + public function get_image_tag( $html, $id, $alt = '', $title = '', $align = '', $size = '' ) {
746 767 if ( ! is_string( $html ) ) {
747 768 return $html;
748 769 }
749 770
750 - if (!Utils::is_ok_to_serve($id)) {
771 + if (!Utils::should_serve_from_cloud($id)) {
751 772 return $html;
752 773 }
753 774
754 775 preg_match( '@\ssrc=[\'\"]([^\'\"]*)[\'\"]@', $html, $matches );
@@ -757,8 +778,16 @@
757 778 // Can't establish img src
758 779 return $html;
759 780 }
760 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 +
761 790 $img_src = $matches[1];
762 791 $size = Utils::maybe_convert_size_to_string( $id, $size );
763 792 $new_url = Item::instance()->get_url($id, $size, 'media_library');
764 793
@@ -780,9 +809,9 @@
780 809 *
781 810 * @return array
782 811 */
783 812 public function wp_get_attachment_image_src( $image, $attachment_id, $size, $icon ) {
784 - if (!Utils::is_ok_to_serve($attachment_id)) {
813 + if (!Utils::should_serve_from_cloud($attachment_id)) {
785 814 return $image;
786 815 }
787 816
788 817 if ( isset( $image[0] ) ) {
@@ -808,9 +837,9 @@
808 837 *
809 838 * @return array
810 839 */
811 840 public function wp_prepare_attachment_for_js( $response, $attachment, $meta ) {
812 - if (!Utils::is_ok_to_serve($attachment->ID)) {
841 + if (!Utils::should_serve_from_cloud($attachment->ID)) {
813 842 return $response;
814 843 }
815 844
816 845 if ( isset( $response['sizes'] ) && is_array( $response['sizes'] ) ) {
@@ -836,9 +865,9 @@
836 865 *
837 866 * @return array
838 867 */
839 868 public function image_get_intermediate_size( $data, $post_id, $size ) {
840 - if (!Utils::is_ok_to_serve($post_id)) {
869 + if (!Utils::should_serve_from_cloud($post_id)) {
841 870 return $data;
842 871 }
843 872 if ( isset( $data['url'] ) ) {
844 873 $size = Utils::maybe_convert_size_to_string( $post_id, $size );
@@ -988,8 +1017,11 @@
988 1017 $is_image = wp_attachment_is_image($attachment_id);
989 1018 $existing = $wpmcsItem->get($attachment_id, 'media_library');
990 1019 $existing_extras = $wpmcsItem->get_extras($attachment_id, false, 'media_library');
991 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']);
992 1024 $sizes = [];
993 1025 $uploaded = [];
994 1026 $extras = [];
995 1027 $prefix = '';
@@ -1019,35 +1051,43 @@
1019 1051 $extras['width'] = (isset($attachment_meta['width']) && !empty($attachment_meta['width'])) ? $attachment_meta['width'] : 0;
1020 1052 $extras['height'] = (isset($attachment_meta['height']) && !empty($attachment_meta['height'])) ? $attachment_meta['height'] : 0;
1021 1053 }
1022 1054
1023 - // Check whether the extension is enabled for uploading
1055 + // Excluded by extension settings — an intentional skip, not a sync failure, so no log entry.
1024 1056 if (!Utils::is_extension_available($file_path)) {
1025 - Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1026 - 'message' => __('File extension is not supported', 'media-cloud-sync'),
1027 - 'file' => $file_path,
1028 - 'code' => 415
1029 - ]);
1030 1057 return false;
1031 1058 }
1032 1059
1033 1060 // Upload main file first, if that fails no need to attempt the rest
1034 - $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);
1035 1062
1036 1063 if ($uploaded && isset($uploaded['success']) && $uploaded['success']) {
1037 1064 // Attempt to upload original file if present. Not critical if this fails so we don't check the result before proceeding.
1038 - $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);
1039 1066
1040 1067 if ($is_image && isset($file_dir) && !empty($file_dir)) {
1041 1068 $sizes_to_upload = $this->get_attachment_image_sizes_for_upload($attachment_meta, $attachment_id);
1042 1069 if (!empty($sizes_to_upload)) {
1043 1070 // Upload image sizes. Again, not critical if this fails so we don't check the result before proceeding.
1044 - $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);
1045 1072 }
1046 1073 }
1047 1074
1048 1075 if (!empty($sizes)) {
1049 - $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);
1050 1090 }
1051 1091
1052 1092 return [
1053 1093 'file' => [
@@ -1061,12 +1101,13 @@
1061 1101 ];
1062 1102 }
1063 1103
1064 1104 if (isset($uploaded['success']) && !$uploaded['success']) {
1105 + // Prefer the service's own error message/code when it provided one.
1065 1106 Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1066 - 'message' => __('File upload failed', 'media-cloud-sync'),
1107 + 'message' => $uploaded['message'] ?? __('File upload failed', 'media-cloud-sync'),
1067 1108 'file' => $file_path,
1068 - 'code' => 500
1109 + 'code' => $uploaded['code'] ?? 500
1069 1110 ]);
1070 1111 }
1071 1112
1072 1113 return false;
@@ -1160,16 +1201,19 @@
1160 1201 * Upload the main/full file.
1161 1202 * @since 1.3.6
1162 1203 * @param string $file_path
1163 1204 * @param string $source_path
1164 - * @param bool $do_reupload
1205 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1165 1206 * @param bool $has_existing
1166 1207 * @param array $existing
1167 1208 * @param string $prefix
1168 1209 * @param int $attachment_id
1210 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1169 1211 */
1170 - private function uploadFullFile($file_path, $source_path, $do_reupload, $has_existing, $existing, $prefix, $attachment_id) {
1171 - 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)) {
1172 1216 return [
1173 1217 'success' => true,
1174 1218 'file_url' => $existing['url'],
1175 1219 'key' => $existing['key']
@@ -1176,9 +1220,9 @@
1176 1220 ];
1177 1221 }
1178 1222
1179 1223 if (file_exists($file_path)) {
1180 - return Service::instance()->uploadSingle($file_path, $source_path, $prefix);
1224 + return Service::instance()->uploadSingle($file_path, $source_path, $prefix, $is_private);
1181 1225 }
1182 1226
1183 1227 Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1184 1228 'message' => __('File not found', 'media-cloud-sync'),
@@ -1193,15 +1237,16 @@
1193 1237 * Upload original file when present.
1194 1238 * @since 1.3.6
1195 1239 * @param string $original_file
1196 1240 * @param string $file_dir
1197 - * @param bool $do_reupload
1241 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1198 1242 * @param bool $has_existing
1199 1243 * @param array $existing
1200 1244 * @param string $prefix
1201 1245 * @param int $attachment_id
1246 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1202 1247 */
1203 - 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) {
1204 1249 $original = [];
1205 1250 if (empty($original_file)) {
1206 1251 return $original;
1207 1252 }
@@ -1208,14 +1253,16 @@
1208 1253
1209 1254 $original_file_path = trailingslashit($file_dir) . $original_file;
1210 1255 $original_file_source_path = Utils::get_attachment_source_path($original_file_path);
1211 1256
1212 - 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) {
1213 1260 return [ 'source_path' => $original_file_source_path, 'key' => $existing['original_key'] ];
1214 1261 }
1215 1262
1216 1263 if (file_exists($original_file_path)) {
1217 - $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);
1218 1265 if (isset($uploaded_original['success']) && $uploaded_original['success']) {
1219 1266 return [ 'source_path' => $original_file_source_path, 'key' => $uploaded_original['key'] ];
1220 1267 }
1221 1268
@@ -1220,11 +1267,11 @@
1220 1267 }
1221 1268
1222 1269 if (isset($uploaded_original['success']) && !$uploaded_original['success']) {
1223 1270 Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1224 - 'message' => __('Original File upload failed', 'media-cloud-sync'),
1271 + 'message' => $uploaded_original['message'] ?? __('Original File upload failed', 'media-cloud-sync'),
1225 1272 'file' => $original_file_path,
1226 - 'code' => 500
1273 + 'code' => $uploaded_original['code'] ?? 500
1227 1274 ]);
1228 1275 }
1229 1276 } else {
1230 1277 Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
@@ -1241,15 +1288,16 @@
1241 1288 * Upload intermediate image sizes.
1242 1289 * @since 1.3.6
1243 1290 * @param array $attachment_sizes
1244 1291 * @param string $file_dir
1245 - * @param bool $do_reupload
1292 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1246 1293 * @param array $existing_extras
1247 1294 * @param string $prefix
1248 1295 * @param int $attachment_id
1249 - * @return array
1296 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1297 + * @return array
1250 1298 */
1251 - 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) {
1252 1300 $sizes = [];
1253 1301
1254 1302 foreach ($attachment_sizes as $size => $sub_image) {
1255 1303 $sub_size = [];
@@ -1263,10 +1311,14 @@
1263 1311 }
1264 1312
1265 1313 $uploaded_sub_image = [];
1266 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 +
1267 1319 if (
1268 - !$do_reupload &&
1320 + !$force_this_size &&
1269 1321 !Utils::is_empty($existing_extras) &&
1270 1322 isset($existing_extras['sizes']) && !Utils::is_empty($existing_extras['sizes']) &&
1271 1323 isset($existing_extras['sizes'][$size]) && !Utils::is_empty($existing_extras['sizes'][$size]) &&
1272 1324 $existing_extras['sizes'][$size]['source_path'] == $sub_file_source_path
@@ -1276,9 +1328,9 @@
1276 1328 'file_url' => $existing_extras['sizes'][$size]['url'],
1277 1329 'key' => $existing_extras['sizes'][$size]['key']
1278 1330 ];
1279 1331 } else if (file_exists($sub_file_path)) {
1280 - $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);
1281 1333 } else {
1282 1334 Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1283 1335 /* translators: %1$s: Image size name (e.g., thumbnail, medium, large). */
1284 1336 'message' => sprintf(__('Image size %1$s not found', 'media-cloud-sync'), $size),
@@ -1301,8 +1353,14 @@
1301 1353 'file' => $sub_file_path,
1302 1354 'code' => 500
1303 1355 ]);
1304 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 + }
1305 1363 }
1306 1364
1307 1365 $sizes[$size] = $sub_size;
1308 1366 }
@@ -1333,18 +1391,40 @@
1333 1391 }
1334 1392
1335 1393
1336 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 + /**
1337 1412 * Upload pending media files.
1338 1413 *
1339 1414 * Processes media attachments that are pending and haven't been synced yet.
1340 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 + *
1341 1421 * @param string $source_type Source type for identifying which source..
1342 - * @param int $limit Number of media to process. Default is 50.
1343 - * @param int $offset Offset for media query. Default is 0.
1344 - * @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.
1345 1425 */
1346 - public function upload_pending_media( $source_type, $limit = 50, $offset = 0 ) {
1426 + public function upload_pending_media( $source_type, $limit = 50, $after_id = 0 ) {
1347 1427 global $wpdb;
1348 1428
1349 1429 $failed = 0;
1350 1430
@@ -1349,9 +1429,9 @@
1349 1429 $failed = 0;
1350 1430
1351 1431 $source_type_data = self::$source_types[ $source_type ] ?? false;
1352 1432 if ( ! $source_type_data ) {
1353 - return [ 'success' => true, 'failed' => $limit ];
1433 + return [ 'success' => true, 'failed' => $limit, 'last_id' => $after_id ];
1354 1434 }
1355 1435
1356 1436 $posts = $wpdb->prefix . $source_type_data['table'];
1357 1437 $items = Db::get_table_name();
@@ -1373,8 +1453,14 @@
1373 1453 $join .= " AND items.region = %s";
1374 1454 $params[] = $this->region;
1375 1455 }
1376 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 +
1377 1463 $sql = "
1378 1464 SELECT posts.ID
1379 1465 FROM {$posts} AS posts
1380 1466 LEFT JOIN {$items} AS items USE INDEX (idx_item_lookup)
@@ -1380,30 +1466,41 @@
1380 1466 LEFT JOIN {$items} AS items USE INDEX (idx_item_lookup)
1381 1467 ON {$join}
1382 1468 WHERE posts.post_type = 'attachment'
1383 1469 AND items.source_id IS NULL
1470 + {$after_id_sql}
1384 1471 ORDER BY posts.ID ASC
1385 - LIMIT %d OFFSET %d
1472 + LIMIT %d
1386 1473 ";
1387 1474
1388 1475 $params[] = (int) $limit;
1389 - $params[] = (int) $offset;
1390 1476
1391 1477 $rows = $wpdb->get_results( $wpdb->prepare( $sql, $params ) );
1392 1478
1393 1479 if ( empty( $rows ) ) {
1394 - return [ 'success' => true, 'failed' => 0 ];
1480 + return [ 'success' => true, 'failed' => 0, 'last_id' => $after_id ];
1395 1481 }
1396 1482
1483 + $last_id = $after_id;
1397 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 +
1398 1495 try {
1399 - $this->upload_single_media( (int) $row->ID, $source_type );
1496 + $this->upload_single_media( $attachment_id, $source_type );
1400 1497 } catch ( \Throwable $e ) {
1401 1498 $failed++;
1402 1499 continue;
1403 1500 }
1404 1501
1405 - if ( ! Item::instance()->get( (int) $row->ID, $source_type ) ) {
1502 + if ( ! Item::instance()->get( $attachment_id, $source_type ) ) {
1406 1503 $failed++;
1407 1504 }
1408 1505 }
1409 1506
@@ -1409,8 +1506,9 @@
1409 1506
1410 1507 return [
1411 1508 'success' => true,
1412 1509 'failed' => $failed,
1510 + 'last_id' => $last_id,
1413 1511 ];
1414 1512 }
1415 1513
1416 1514
@@ -1437,9 +1535,9 @@
1437 1535 $provider = $wpmcsItem->get_field($attachment_id, 'provider', 'media_library');
1438 1536 if($provider) {
1439 1537 $label = Schema::getServiceLabels($provider); ?>
1440 1538 <div class="misc-pub-section misc-pub-provider">
1441 - <?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>
1442 1540 </div>
1443 1541 <?php
1444 1542 }
1445 1543
@@ -1445,9 +1543,9 @@
1445 1543
1446 1544 $region = $wpmcsItem->get_field($attachment_id, 'region', 'media_library');
1447 1545 if($region) { ?>
1448 1546 <div class="misc-pub-section misc-pub-provider">
1449 - <?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>
1450 1548 </div>
1451 1549 <?php
1452 1550 }
1453 1551 $private = (int)$wpmcsItem->get_field($attachment_id, 'is_private', 'media_library');
@@ -1452,9 +1550,9 @@
1452 1550 }
1453 1551 $private = (int)$wpmcsItem->get_field($attachment_id, 'is_private', 'media_library');
1454 1552 ?>
1455 1553 <div class="misc-pub-section misc-pub-provider">
1456 - <?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>
1457 1555 </div>
1458 1556 <?php
1459 1557 }
1460 1558
@@ -1460,8 +1558,10 @@
1460 1558
1461 1559
1462 1560 /**
1463 1561 * Function to get attachment details by ID
1562 + *
1563 + * @since 1.3.12 Added capability checks for attachment access.
1464 1564 */
1465 1565 public function ajax_get_attachment_details() {
1466 1566 $result = array(
1467 1567 'status' => false,
@@ -1478,9 +1578,18 @@
1478 1578 }
1479 1579
1480 1580 check_ajax_referer( 'get_media_provider_details', '_nonce' );
1481 1581
1482 - $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 + }
1483 1592
1484 1593 // Return if extension not allowed
1485 1594 $path = get_attached_file( $id );
1486 1595 if(!Utils::is_extension_available($path)) {
@@ -1508,9 +1617,9 @@
1508 1617 $region = $wpmcsItem->get_field($id, 'region', 'media_library');
1509 1618 if($region){
1510 1619 $item['region'] = $region;
1511 1620 }
1512 - $item['private'] = $wpmcsItem->get_field($id, 'private', 'media_library');
1621 + $item['private'] = (int) $wpmcsItem->get_field($id, 'is_private', 'media_library');
1513 1622 if($item) {
1514 1623 $result= array(
1515 1624 'status' => true,
1516 1625 'data' => $item
@@ -1545,8 +1654,26 @@
1545 1654 * Note: delete_post is used as there is a potential that deleted_post is not reached.
1546 1655 */
1547 1656 public function delete_post() {
1548 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' );
1549 1676 }
1550 1677
1551 1678 /**
1552 1679 * Has WP Core fixed wp_check_filetype when URL has params yet?