PluginProbe
Media Cloud Sync / 1.4.1
Media Cloud Sync v1.4.1
1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 All 35 releases
← All changes | includes/integrations/media-library.php +1098 -333 1.2.121.4.1 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,627 @@
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 + if($this->shouldForceReupload($attachment_meta, $attachment_id, $existing_extras)) {
1037 + $do_reupload = true;
1038 + }
1039 + }
1040 +
632 1041 // Add prefix if object versioning is ON
633 1042 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']
1043 + $prefix = ($has_existing && isset($existing_extras['prefix']) && !empty($existing_extras['prefix']))
1044 + ? $existing_extras['prefix']
636 1045 : Utils::generate_object_versioning_prefix();
637 - $extras['prefix'] = $prefix;
1046 + $extras['prefix'] = $prefix;
638 1047 }
639 1048
640 - // Get width and height from image meta
1049 + // Width/height
641 1050 if (isset($attachment_meta) && !empty($attachment_meta)) {
642 1051 $extras['width'] = (isset($attachment_meta['width']) && !empty($attachment_meta['width'])) ? $attachment_meta['width'] : 0;
643 1052 $extras['height'] = (isset($attachment_meta['height']) && !empty($attachment_meta['height'])) ? $attachment_meta['height'] : 0;
644 1053 }
645 1054
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
1055 + // Excluded by extension settings — an intentional skip, not a sync failure, so no log entry.
656 1056 if (!Utils::is_extension_available($file_path)) {
657 - return $attachment_meta;
1057 + return false;
658 1058 }
659 1059
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 - }
1060 + // Upload main file first, if that fails no need to attempt the rest
1061 + $uploaded = $this->uploadFullFile($file_path, $source_path, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private);
670 1062
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 = [];
1063 + if ($uploaded && isset($uploaded['success']) && $uploaded['success']) {
1064 + // Attempt to upload original file if present. Not critical if this fails so we don't check the result before proceeding.
1065 + $original = $this->uploadOriginalFile($original_file, $file_dir, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private);
680 1066
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);
1067 + if ($is_image && isset($file_dir) && !empty($file_dir)) {
1068 + $sizes_to_upload = $this->get_attachment_image_sizes_for_upload($attachment_meta, $attachment_id);
1069 + if (!empty($sizes_to_upload)) {
1070 + // Upload image sizes. Again, not critical if this fails so we don't check the result before proceeding.
1071 + $sizes = $this->uploadImageSizes($sizes_to_upload, $file_dir, $do_reupload, $existing_extras, $prefix, $attachment_id, $is_private);
694 1072 }
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 - }
706 1073 }
707 1074
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 - }
1075 + if (!empty($sizes)) {
1076 + // Merge over existing sizes rather than replacing wholesale — a size that
1077 + // came back empty from uploadImageSizes() (its own fallback included) must
1078 + // never erase a previously-good, unrelated size's record.
1079 + $existing_sizes = isset($existing_extras['sizes']) && is_array($existing_extras['sizes'])
1080 + ? $existing_extras['sizes']
1081 + : [];
724 1082
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);
1083 + foreach ($sizes as $size_name => $size_data) {
1084 + if (Utils::is_empty($size_data) && isset($existing_sizes[$size_name])) {
1085 + unset($sizes[$size_name]);
739 1086 }
1087 + }
740 1088
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 - }
1089 + $extras['sizes'] = array_merge($existing_sizes, $sizes);
754 1090 }
755 -
756 -
757 - if (isset($sizes) && !empty($sizes)) {
758 - $extras['sizes'] = $sizes;
759 - }
760 1091
761 1092 return [
762 1093 'file' => [
763 - 'source_path' => $source_path,
764 - 'url' => $uploaded['file_url'],
765 - 'key' => $uploaded['key'],
1094 + 'source_path' => $source_path,
1095 + 'url' => $uploaded['file_url'],
1096 + 'key' => $uploaded['key'],
1097 + 'original_source_path' => isset($original['source_path']) ? $original['source_path'] : '',
1098 + 'original_key' => isset($original['key']) ? $original['key'] : ''
766 1099 ],
767 1100 'extra' => $extras
768 1101 ];
1102 + }
769 1103
1104 + if (isset($uploaded['success']) && !$uploaded['success']) {
1105 + // Prefer the service's own error message/code when it provided one.
1106 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1107 + 'message' => $uploaded['message'] ?? __('File upload failed', 'media-cloud-sync'),
1108 + 'file' => $file_path,
1109 + 'code' => $uploaded['code'] ?? 500
1110 + ]);
770 1111 }
771 1112
772 1113 return false;
773 1114 }
774 1115
1116 + /**
1117 + * Image subsizes to sync to cloud (after filters). Empty means no subsizes are uploaded.
1118 + *
1119 + * `wpmcs_skip_upload_image_sizes` — (bool) When true, no subsizes are uploaded.
1120 + * `wpmcs_upload_attachment_image_sizes` — (array) Subset of `$attachment_meta['sizes']` to upload; empty skips all.
1121 + *
1122 + * @param array $attachment_meta Full attachment metadata.
1123 + * @param int $attachment_id Attachment post ID.
1124 + * @return array Same shape as attachment meta `sizes` entries.
1125 + */
1126 + private function get_attachment_image_sizes_for_upload($attachment_meta, $attachment_id) {
1127 + $sizes = [];
1128 + if (isset($attachment_meta['sizes']) && is_array($attachment_meta['sizes'])) {
1129 + $sizes = $attachment_meta['sizes'];
1130 + }
775 1131
1132 + if (apply_filters('wpmcs_skip_upload_image_sizes', false, $attachment_id, $attachment_meta)) {
1133 + return [];
1134 + }
1135 +
1136 + $sizes = apply_filters('wpmcs_upload_attachment_image_sizes', $sizes, $attachment_id, $attachment_meta);
1137 +
1138 + return is_array($sizes) ? $sizes : [];
1139 + }
1140 +
776 1141 /**
1142 + * Decide if we should force a reupload based on dims/sizes differences.
1143 + * @since 1.3.6
1144 + * @param array $attachment_meta
1145 + * @param int $attachment_id
1146 + * @param array $existing_extras
1147 + */
1148 + private function shouldForceReupload($attachment_meta, $attachment_id, $existing_extras) {
1149 + $existing_extras = $existing_extras ?? Item::instance()->get_extras($attachment_id, false, 'media_library');
1150 +
1151 + $current_width = isset($attachment_meta['width']) ? (int)$attachment_meta['width'] : 0;
1152 + $current_height = isset($attachment_meta['height']) ? (int)$attachment_meta['height'] : 0;
1153 +
1154 + $existing_width = isset($existing_extras['width']) ? (int)$existing_extras['width'] : 0;
1155 + $existing_height = isset($existing_extras['height']) ? (int)$existing_extras['height'] : 0;
1156 +
1157 + if ($current_width !== $existing_width || $current_height !== $existing_height) {
1158 + return true;
1159 + }
1160 +
1161 + $filtered_sizes = $this->get_attachment_image_sizes_for_upload($attachment_meta, $attachment_id);
1162 + if (empty($filtered_sizes)) {
1163 + return false;
1164 + }
1165 +
1166 + $existing_sizes = isset($existing_extras['sizes']) && is_array($existing_extras['sizes']) ? $existing_extras['sizes'] : [];
1167 + $current_sizes = [];
1168 + foreach ($filtered_sizes as $sname => $smeta) {
1169 + $current_sizes[$sname] = [
1170 + 'width' => isset($smeta['width']) ? (int)$smeta['width'] : 0,
1171 + 'height' => isset($smeta['height']) ? (int)$smeta['height'] : 0,
1172 + ];
1173 + }
1174 +
1175 + $existing_relevant = [];
1176 + foreach (array_keys($current_sizes) as $sname) {
1177 + if (isset($existing_sizes[$sname])) {
1178 + $existing_relevant[$sname] = $existing_sizes[$sname];
1179 + }
1180 + }
1181 +
1182 + if (array_keys($existing_relevant) !== array_keys($current_sizes)) {
1183 + return true;
1184 + }
1185 +
1186 + foreach ($current_sizes as $sname => $dims) {
1187 + if (!isset($existing_sizes[$sname])) {
1188 + return true;
1189 + }
1190 + $ex_w = isset($existing_sizes[$sname]['width']) ? (int)$existing_sizes[$sname]['width'] : 0;
1191 + $ex_h = isset($existing_sizes[$sname]['height']) ? (int)$existing_sizes[$sname]['height'] : 0;
1192 + if ($ex_w !== $dims['width'] || $ex_h !== $dims['height']) {
1193 + return true;
1194 + }
1195 + }
1196 +
1197 + return false;
1198 + }
1199 +
1200 + /**
1201 + * Upload the main/full file.
1202 + * @since 1.3.6
1203 + * @param string $file_path
1204 + * @param string $source_path
1205 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1206 + * @param bool $has_existing
1207 + * @param array $existing
1208 + * @param string $prefix
1209 + * @param int $attachment_id
1210 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1211 + */
1212 + private function uploadFullFile($file_path, $source_path, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private = false) {
1213 + // $do_reupload may be an array of size names (force just those sizes) — that
1214 + // shouldn't force the full file too, only a literal true does.
1215 + if ($do_reupload !== true && $has_existing && ($existing['source_path'] == $source_path)) {
1216 + return [
1217 + 'success' => true,
1218 + 'file_url' => $existing['url'],
1219 + 'key' => $existing['key']
1220 + ];
1221 + }
1222 +
1223 + if (file_exists($file_path)) {
1224 + return Service::instance()->uploadSingle($file_path, $source_path, $prefix, $is_private);
1225 + }
1226 +
1227 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1228 + 'message' => __('File not found', 'media-cloud-sync'),
1229 + 'file' => $file_path,
1230 + 'code' => 404
1231 + ]);
1232 +
1233 + return false;
1234 + }
1235 +
1236 + /**
1237 + * Upload original file when present.
1238 + * @since 1.3.6
1239 + * @param string $original_file
1240 + * @param string $file_dir
1241 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1242 + * @param bool $has_existing
1243 + * @param array $existing
1244 + * @param string $prefix
1245 + * @param int $attachment_id
1246 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1247 + */
1248 + private function uploadOriginalFile($original_file, $file_dir, $do_reupload, $has_existing, $existing, $prefix, $attachment_id, $is_private = false) {
1249 + $original = [];
1250 + if (empty($original_file)) {
1251 + return $original;
1252 + }
1253 +
1254 + $original_file_path = trailingslashit($file_dir) . $original_file;
1255 + $original_file_source_path = Utils::get_attachment_source_path($original_file_path);
1256 +
1257 + // $do_reupload may be an array of size names (force just those sizes) — that
1258 + // shouldn't force the original file too, only a literal true does.
1259 + if ($do_reupload !== true && $has_existing && isset($existing['original_source_path']) && !Utils::is_empty($existing['original_source_path']) && $existing['original_source_path'] === $original_file_source_path) {
1260 + return [ 'source_path' => $original_file_source_path, 'key' => $existing['original_key'] ];
1261 + }
1262 +
1263 + if (file_exists($original_file_path)) {
1264 + $uploaded_original = Service::instance()->uploadSingle($original_file_path, $original_file_source_path, $prefix, $is_private);
1265 + if (isset($uploaded_original['success']) && $uploaded_original['success']) {
1266 + return [ 'source_path' => $original_file_source_path, 'key' => $uploaded_original['key'] ];
1267 + }
1268 +
1269 + if (isset($uploaded_original['success']) && !$uploaded_original['success']) {
1270 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1271 + 'message' => $uploaded_original['message'] ?? __('Original File upload failed', 'media-cloud-sync'),
1272 + 'file' => $original_file_path,
1273 + 'code' => $uploaded_original['code'] ?? 500
1274 + ]);
1275 + }
1276 + } else {
1277 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1278 + 'message' => __('Original File not found', 'media-cloud-sync'),
1279 + 'file' => $original_file_path,
1280 + 'code' => 404
1281 + ]);
1282 + }
1283 +
1284 + return $original;
1285 + }
1286 +
1287 + /**
1288 + * Upload intermediate image sizes.
1289 + * @since 1.3.6
1290 + * @param array $attachment_sizes
1291 + * @param string $file_dir
1292 + * @param bool|array $do_reupload Reupload everything (true), nothing (false), or only these sizes (array of size names).
1293 + * @param array $existing_extras
1294 + * @param string $prefix
1295 + * @param int $attachment_id
1296 + * @param bool $is_private Keeps a reupload of an already-private item under the private path.
1297 + * @return array
1298 + */
1299 + private function uploadImageSizes($attachment_sizes, $file_dir, $do_reupload, $existing_extras, $prefix, $attachment_id, $is_private = false) {
1300 + $sizes = [];
1301 +
1302 + foreach ($attachment_sizes as $size => $sub_image) {
1303 + $sub_size = [];
1304 + $sub_file = isset($sub_image['file']) ? $sub_image['file'] : false;
1305 + $sub_file_path = '';
1306 + $sub_file_source_path = '';
1307 +
1308 + if ($sub_file) {
1309 + $sub_file_path = $file_dir . '/' . $sub_file;
1310 + $sub_file_source_path = Utils::get_attachment_source_path($sub_file_path);
1311 + }
1312 +
1313 + $uploaded_sub_image = [];
1314 +
1315 + // $do_reupload may be an array naming specific sizes to force (e.g. a
1316 + // WooCommerce on-the-fly regen of one size only) rather than a plain bool.
1317 + $force_this_size = ($do_reupload === true) || (is_array($do_reupload) && in_array($size, $do_reupload, true));
1318 +
1319 + if (
1320 + !$force_this_size &&
1321 + !Utils::is_empty($existing_extras) &&
1322 + isset($existing_extras['sizes']) && !Utils::is_empty($existing_extras['sizes']) &&
1323 + isset($existing_extras['sizes'][$size]) && !Utils::is_empty($existing_extras['sizes'][$size]) &&
1324 + $existing_extras['sizes'][$size]['source_path'] == $sub_file_source_path
1325 + ) {
1326 + $uploaded_sub_image = [
1327 + 'success' => true,
1328 + 'file_url' => $existing_extras['sizes'][$size]['url'],
1329 + 'key' => $existing_extras['sizes'][$size]['key']
1330 + ];
1331 + } else if (file_exists($sub_file_path)) {
1332 + $uploaded_sub_image = Service::instance()->uploadSingle($sub_file_path, $sub_file_source_path, $prefix, $is_private);
1333 + } else {
1334 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1335 + /* translators: %1$s: Image size name (e.g., thumbnail, medium, large). */
1336 + 'message' => sprintf(__('Image size %1$s not found', 'media-cloud-sync'), $size),
1337 + 'file' => $sub_file_path,
1338 + 'code' => 404
1339 + ]);
1340 + }
1341 +
1342 + if (isset($uploaded_sub_image) && !empty($uploaded_sub_image) && isset($uploaded_sub_image['success']) && $uploaded_sub_image['success']) {
1343 + $sub_size['source_path'] = $sub_file_source_path;
1344 + $sub_size['url'] = $uploaded_sub_image['file_url'];
1345 + $sub_size['key'] = $uploaded_sub_image['key'];
1346 + $sub_size['width'] = isset($sub_image['width']) ? $sub_image['width'] : 0;
1347 + $sub_size['height'] = isset($sub_image['height']) ? $sub_image['height'] : 0;
1348 + } else {
1349 + if (isset($uploaded_sub_image['success']) && !$uploaded_sub_image['success']) {
1350 + Logger::instance()->add_log('sync_to_cloud', $attachment_id, 'media_library', [
1351 + /* translators: %1$s: Image size name (e.g., thumbnail, medium, large). */
1352 + 'message' => sprintf(__('Image size %1$s upload failed', 'media-cloud-sync'), $size),
1353 + 'file' => $sub_file_path,
1354 + 'code' => 500
1355 + ]);
1356 + }
1357 +
1358 + // A failed (re)upload attempt shouldn't erase a previously-good record —
1359 + // fall back to what's already stored rather than overwriting it with empty data.
1360 + if (isset($existing_extras['sizes'][$size]) && !Utils::is_empty($existing_extras['sizes'][$size])) {
1361 + $sub_size = $existing_extras['sizes'][$size];
1362 + }
1363 + }
1364 +
1365 + $sizes[$size] = $sub_size;
1366 + }
1367 +
1368 + return $sizes;
1369 + }
1370 +
1371 +
1372 + /**
777 1373 * Get Total Media Count (queried)
778 1374 *
779 1375 * Param added because need a generic function name,
780 1376 * if the function calls by source_type
@@ -784,9 +1380,9 @@
784 1380
785 1381 // Fetch the count of attachments with the 'inherit' status
786 1382 $count = $wpdb->get_var(
787 1383 $wpdb->prepare(
788 - "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
1384 + "SELECT COUNT(1) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
789 1385 'attachment',
790 1386 'inherit'
791 1387 )
792 1388 );
@@ -795,8 +1391,129 @@
795 1391 }
796 1392
797 1393
798 1394 /**
1395 + * Whether an attachment is still waiting on WordPress-generated subsizes — same check
1396 + * update_attachment_metadata() uses, so callers can tell that apart from a real failure.
1397 + *
1398 + * @param int $attachment_id
1399 + * @return bool
1400 + */
1401 + private function is_attachment_not_yet_ready( $attachment_id ) {
1402 + if ( ! wp_attachment_is_image( $attachment_id ) ) {
1403 + return false;
1404 + }
1405 +
1406 + $attachment_meta = wp_get_attachment_metadata( $attachment_id );
1407 +
1408 + return $this->should_wait_for_subsizes( $attachment_meta, $attachment_id );
1409 + }
1410 +
1411 + /**
1412 + * Upload pending media files.
1413 + *
1414 + * Processes media attachments that are pending and haven't been synced yet.
1415 + *
1416 + * Paginates by `posts.ID` (a stable cursor) rather than a raw SQL OFFSET,
1417 + * so a concurrent single-item retry (which can remove an arbitrary item
1418 + * from the middle of the pending set, not just the front) can't desync
1419 + * which row gets picked up next.
1420 + *
1421 + * @param string $source_type Source type for identifying which source..
1422 + * @param int $limit Number of media to process. Default is 50.
1423 + * @param int $after_id Only consider attachments with ID greater than this cursor. Default is 0.
1424 + * @return array Status array with success, failed count, and the last attachment ID considered.
1425 + */
1426 + public function upload_pending_media( $source_type, $limit = 50, $after_id = 0 ) {
1427 + global $wpdb;
1428 +
1429 + $failed = 0;
1430 +
1431 + $source_type_data = self::$source_types[ $source_type ] ?? false;
1432 + if ( ! $source_type_data ) {
1433 + return [ 'success' => true, 'failed' => $limit, 'last_id' => $after_id ];
1434 + }
1435 +
1436 + $posts = $wpdb->prefix . $source_type_data['table'];
1437 + $items = Db::get_table_name();
1438 +
1439 + $join = "
1440 + items.source_id = posts.ID
1441 + AND items.source_type = %s
1442 + AND items.provider = %s
1443 + AND items.storage = %s
1444 + ";
1445 +
1446 + $params = [
1447 + $source_type,
1448 + $this->service,
1449 + $this->bucket_name,
1450 + ];
1451 +
1452 + if ( ! empty( $this->region ) ) {
1453 + $join .= " AND items.region = %s";
1454 + $params[] = $this->region;
1455 + }
1456 +
1457 + $after_id_sql = '';
1458 + if ( $after_id > 0 ) {
1459 + $after_id_sql = 'AND posts.ID > %d';
1460 + $params[] = (int) $after_id;
1461 + }
1462 +
1463 + $sql = "
1464 + SELECT posts.ID
1465 + FROM {$posts} AS posts
1466 + LEFT JOIN {$items} AS items USE INDEX (idx_item_lookup)
1467 + ON {$join}
1468 + WHERE posts.post_type = 'attachment'
1469 + AND items.source_id IS NULL
1470 + {$after_id_sql}
1471 + ORDER BY posts.ID ASC
1472 + LIMIT %d
1473 + ";
1474 +
1475 + $params[] = (int) $limit;
1476 +
1477 + $rows = $wpdb->get_results( $wpdb->prepare( $sql, $params ) );
1478 +
1479 + if ( empty( $rows ) ) {
1480 + return [ 'success' => true, 'failed' => 0, 'last_id' => $after_id ];
1481 + }
1482 +
1483 + $last_id = $after_id;
1484 + foreach ( $rows as $row ) {
1485 + $attachment_id = (int) $row->ID;
1486 +
1487 + // Still waiting on WordPress to finish generating subsizes — not a failure,
1488 + // just not ready yet. Leave $last_id alone so it's reconsidered next tick.
1489 + if ( $this->is_attachment_not_yet_ready( $attachment_id ) ) {
1490 + break;
1491 + }
1492 +
1493 + $last_id = $attachment_id;
1494 +
1495 + try {
1496 + $this->upload_single_media( $attachment_id, $source_type );
1497 + } catch ( \Throwable $e ) {
1498 + $failed++;
1499 + continue;
1500 + }
1501 +
1502 + if ( ! Item::instance()->get( $attachment_id, $source_type ) ) {
1503 + $failed++;
1504 + }
1505 + }
1506 +
1507 + return [
1508 + 'success' => true,
1509 + 'failed' => $failed,
1510 + 'last_id' => $last_id,
1511 + ];
1512 + }
1513 +
1514 +
1515 + /**
799 1516 * Edit Image Meta In View Image
800 1517 * @since 1.0.0
801 1518 */
802 1519 function attachment_submitbox_metadata( ) {
@@ -818,9 +1535,9 @@
818 1535 $provider = $wpmcsItem->get_field($attachment_id, 'provider', 'media_library');
819 1536 if($provider) {
820 1537 $label = Schema::getServiceLabels($provider); ?>
821 1538 <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>
1539 + <?php esc_html_e( 'Provider :', 'media-cloud-sync' ); ?> <strong><?php echo esc_textarea(!empty($label) ? $label : $provider); ?></strong>
823 1540 </div>
824 1541 <?php
825 1542 }
826 1543
@@ -826,9 +1543,9 @@
826 1543
827 1544 $region = $wpmcsItem->get_field($attachment_id, 'region', 'media_library');
828 1545 if($region) { ?>
829 1546 <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>
1547 + <?php esc_html_e( 'Region :', 'media-cloud-sync' ); ?> <strong><?php echo esc_textarea($region); ?></strong>
831 1548 </div>
832 1549 <?php
833 1550 }
834 1551 $private = (int)$wpmcsItem->get_field($attachment_id, 'is_private', 'media_library');
@@ -833,9 +1550,9 @@
833 1550 }
834 1551 $private = (int)$wpmcsItem->get_field($attachment_id, 'is_private', 'media_library');
835 1552 ?>
836 1553 <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>
1554 + <?php esc_html_e( 'Access :', 'media-cloud-sync' ); ?> <strong><?php $private ? esc_html_e( 'Private', 'media-cloud-sync' ) : esc_html_e( 'Public', 'media-cloud-sync' ); ?></strong>
838 1555 </div>
839 1556 <?php
840 1557 }
841 1558
@@ -841,8 +1558,10 @@
841 1558
842 1559
843 1560 /**
844 1561 * Function to get attachment details by ID
1562 + *
1563 + * @since 1.3.12 Added capability checks for attachment access.
845 1564 */
846 1565 public function ajax_get_attachment_details() {
847 1566 $result = array(
848 1567 'status' => false,
@@ -849,8 +1568,12 @@
849 1568 'data' => array(),
850 1569 'exclude' => false
851 1570 );
852 1571
1572 + if(!Utils::is_service_enabled()) {
1573 + wp_send_json_success( $result );
1574 + }
1575 +
853 1576 if ( ! isset( $_POST['id'] ) ) {
854 1577 wp_send_json_success( $result );
855 1578 }
856 1579
@@ -855,9 +1578,18 @@
855 1578 }
856 1579
857 1580 check_ajax_referer( 'get_media_provider_details', '_nonce' );
858 1581
859 - $id= intval( sanitize_text_field( $_POST['id'] ) );
1582 + if ( ! current_user_can( 'upload_files' ) ) {
1583 + wp_send_json_error();
1584 + }
1585 +
1586 + $id = absint( wp_unslash( $_POST['id'] ) );
1587 + $post = get_post( $id );
1588 +
1589 + if ( ! $post || 'attachment' !== $post->post_type || ! current_user_can( 'edit_post', $id ) ) {
1590 + wp_send_json_success( $result );
1591 + }
860 1592
861 1593 // Return if extension not allowed
862 1594 $path = get_attached_file( $id );
863 1595 if(!Utils::is_extension_available($path)) {
@@ -885,9 +1617,9 @@
885 1617 $region = $wpmcsItem->get_field($id, 'region', 'media_library');
886 1618 if($region){
887 1619 $item['region'] = $region;
888 1620 }
889 - $item['private'] = $wpmcsItem->get_field($id, 'private', 'media_library');
1621 + $item['private'] = (int) $wpmcsItem->get_field($id, 'is_private', 'media_library');
890 1622 if($item) {
891 1623 $result= array(
892 1624 'status' => true,
893 1625 'data' => $item
@@ -922,8 +1654,41 @@
922 1654 * Note: delete_post is used as there is a potential that deleted_post is not reached.
923 1655 */
924 1656 public function delete_post() {
925 1657 $this->deleting_attachment = false;
1658 + }
1659 +
1660 + /**
1661 + * Safety net for deletion that bypasses wp_delete_attachment().
1662 + *
1663 + * @handles deleted_post
1664 + */
1665 + public function deleted_post( $post_id, $post ) {
1666 + if ( ! $post || 'attachment' !== $post->post_type ) {
1667 + return;
1668 + }
1669 +
1670 + $item = Item::instance()->get( $post_id, 'media_library' );
1671 + if ( Utils::is_empty( $item ) ) {
1672 + return;
1673 + }
1674 +
1675 + Item::instance()->delete( $post_id, 'media_library' );
1676 + }
1677 +
1678 + /**
1679 + * Has WP Core fixed wp_check_filetype when URL has params yet?
1680 + *
1681 + * @see https://core.trac.wordpress.org/ticket/30377
1682 + * @see https://github.com/aaemnnosttv/fix-wp-media-shortcodes-with-params/blob/master/fix-wp-media-shortcodes-with-params.php
1683 + *
1684 + * @return bool
1685 + */
1686 + public static function wp_check_filetype_broken() {
1687 + $normal_file = wp_check_filetype( 'file.mp4', array( 'mp4' => 'video/mp4' ) );
1688 + $querys_file = wp_check_filetype( 'file.mp4?param=1', array( 'mp4' => 'video/mp4' ) );
1689 +
1690 + return $normal_file !== $querys_file;
926 1691 }
927 1692
928 1693 /**
929 1694 * Is installed?