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