| @@ -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? |