diff --git a/php/class-media.php b/php/class-media.php index cf4a3370b..1064c15c2 100644 --- a/php/class-media.php +++ b/php/class-media.php @@ -1693,6 +1693,36 @@ public function has_public_id( $attachment_id ) { return ! empty( $new_id ) || ! empty( $id ); } + /** + * Check if an attachment's registered sizes reference filenames that no longer match the current attached file. + * + * This happens when an image is edited (e.g. cropped) via the WordPress image editor: the main file gets + * renamed (an `-e{timestamp}` suffix is added), but the `sizes` metadata can be left pointing at the + * pre-edit filenames, leaving no local file matching that size record. + * + * @param int $attachment_id The Attachment ID. + * + * @return bool + */ + public function has_outdated_sizes( $attachment_id ) { + $metadata = wp_get_attachment_metadata( $attachment_id ); + if ( empty( $metadata['file'] ) || empty( $metadata['sizes'] ) ) { + return false; + } + + $base = wp_basename( $metadata['file'] ); + $ext = pathinfo( $base, PATHINFO_EXTENSION ); + $base = ! empty( $ext ) ? substr( $base, 0, - ( strlen( $ext ) + 1 ) ) : $base; + + foreach ( $metadata['sizes'] as $size ) { + if ( empty( $size['file'] ) || 0 !== strpos( $size['file'], $base . '-' ) ) { + return true; + } + } + + return false; + } + /** * Get a Cloudinary ID which includes the file format extension. * diff --git a/php/class-sync.php b/php/class-sync.php index 30e73015d..e9bcd7d5e 100644 --- a/php/class-sync.php +++ b/php/class-sync.php @@ -568,6 +568,18 @@ public function setup_sync_base_struct() { 'required' => false, // Required to complete URL render flag. 'realtime' => true, ), + 'sizes' => array( + 'generate' => array( $this, 'generate_sizes_signature' ), + 'priority' => 5.25, + 'sync' => array( $this->managers['upload'], 'regenerate_sizes' ), + 'validate' => function ( $attachment_id ) { + return $this->managers['media']->has_outdated_sizes( $attachment_id ); + }, + 'state' => 'info syncing', + 'note' => __( 'Regenerating thumbnail sizes', 'cloudinary' ), + 'required' => false, + 'realtime' => true, + ), 'folder' => array( 'generate' => array( $this->managers['media'], 'get_cloudinary_folder' ), 'validate' => array( $this->managers['media'], 'is_folder_synced' ), @@ -1389,4 +1401,19 @@ public function generate_edit_signature( $attachment_id ) { return $str; } + + /** + * Generate a signature for an attachment's thumbnail sizes. + * + * @param int $attachment_id The attachment ID. + * + * @return string + */ + public function generate_sizes_signature( $attachment_id ) { + $metadata = wp_get_attachment_metadata( $attachment_id ); + $file = ! empty( $metadata['file'] ) ? wp_basename( $metadata['file'] ) : ''; + $sizes = ! empty( $metadata['sizes'] ) ? wp_json_encode( $metadata['sizes'] ) : ''; + + return $file . $sizes; + } } diff --git a/php/sync/class-upload-sync.php b/php/sync/class-upload-sync.php index aa8b7f9de..2c81adfe5 100644 --- a/php/sync/class-upload-sync.php +++ b/php/sync/class-upload-sync.php @@ -277,6 +277,31 @@ public function edit_upload( $attachment_id ) { return $this->upload_asset( $attachment_id, 'edit' ); } + /** + * Regenerate an attachment's thumbnail sizes when they no longer match the current attached file. + * + * @param int $attachment_id The attachment ID. + * + * @return array|\WP_Error + */ + public function regenerate_sizes( $attachment_id ) { + if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { + require_once ABSPATH . 'wp-admin/includes/image.php'; + } + + $file = get_attached_file( $attachment_id ); + $metadata = wp_generate_attachment_metadata( $attachment_id, $file ); + if ( empty( $metadata ) ) { + return new \WP_Error( 'cloudinary_regenerate_sizes_failed', __( 'Could not regenerate attachment sizes.', 'cloudinary' ) ); + } + + wp_update_attachment_metadata( $attachment_id, $metadata ); + // Persist the new signature, otherwise this would re-trigger on every subsequent render. + $this->sync->set_signature_item( $attachment_id, 'sizes' ); + + return $metadata; + } + /** * Upload an asset to Cloudinary. *