Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions php/class-media.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 . '-' ) ) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocker: The prefix check marks a valid WP state as stale. When image_edit_thumbnails_separately is on and the user edits with target "All sizes except thumbnail" (nothumb), core keeps the old thumbnail on purpose. The main file and other sizes get the -e{timestamp} base.

I checked this locally (wp-env, WP 7.1):

file: 2026/10/pr1314-nothumb-2-e1791880319565.jpg
  medium       => pr1314-nothumb-2-e1791880319565-225x300.jpg
  thumbnail    => pr1314-nothumb-2-150x150.jpg
  medium_large => pr1314-nothumb-2-768x576.jpg
has_outdated_sizes: true
thumbnail after regenerate: pr1314-nothumb-2-e1791880319565-150x150.jpg

On the next render, regenerate_sizes() overwrites the user's unedited thumbnail with a crop of the edited image. The medium_large entry also keeps the old name in this output. The rotated 600px-wide image is too small for that size, so core does not regenerate it, and the old file stays on disk. A name-only check flags this case too.

Instead, check that each size file exists on disk (path_join( dirname( get_attached_file( $id ) ), $size['file'] )). This targets the real bug (no local file and no relationship row) and does not touch valid partial edits.

return true;
}
}

return false;
}

/**
* Get a Cloudinary ID which includes the file format extension.
*
Expand Down
27 changes: 27 additions & 0 deletions php/class-sync.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
Expand Down Expand Up @@ -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;
}
}
25 changes: 25 additions & 0 deletions php/sync/class-upload-sync.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocker: wp_generate_attachment_metadata() builds a fresh array, so this call replaces the full metadata record. Keys that are not regenerated are lost.

I checked this locally with a 3000x2000 upload (big-image -scaled path) and a third-party key:

before keys: width,height,file,filesize,sizes,image_meta,original_image,third_party_key
after keys:  width,height,file,filesize,sizes,image_meta

If original_image is lost, wp_get_original_image_path() stops working. This plugin uses that path for uploads (filter_backup_original, get_original_image). Other plugins' metadata (for example WPML, or custom keys) is also lost.

Suggest: keep the current metadata and replace only sizes (and maybe width/height/filesize):

$current          = wp_get_attachment_metadata( $attachment_id );
$current['sizes'] = $metadata['sizes'];
wp_update_attachment_metadata( $attachment_id, $current );

// 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.
*
Expand Down
Loading