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