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
100 changes: 91 additions & 9 deletions php/class-media.php
Original file line number Diff line number Diff line change
Expand Up @@ -1187,9 +1187,7 @@ public function attachment_url( $url, $attachment_id ) {
}

if (
false === $this->in_downsize
&& ! doing_filter( 'content_save_pre' )
&& ! Utils::is_saving_metadata()
$this->is_replacement_paused( $attachment_id, false ) // false: videos still need their own URL corrected here.
/**
* Filter doing upload.
* If so, return the default attachment URL.
Expand All @@ -1198,11 +1196,13 @@ public function attachment_url( $url, $attachment_id ) {
*
* @return bool
*/
&& ! apply_filters( 'cloudinary_doing_upload', false )
|| apply_filters( 'cloudinary_doing_upload', false )
) {
if ( ! $this->is_cloudinary_url( $url ) && $this->cloudinary_id( $attachment_id ) ) {
$url = $this->cloudinary_url( $attachment_id );
}
return $url;
}

if ( ! $this->is_cloudinary_url( $url ) && $this->cloudinary_id( $attachment_id ) ) {
$url = $this->cloudinary_url( $attachment_id );
}

return $url;
Expand Down Expand Up @@ -1802,7 +1802,7 @@ public function filter_downsize( $image, $attachment_id, $size ) {
}

// Don't do this while saving.
if ( true === $this->in_downsize || doing_filter( 'content_save_pre' ) || wp_attachment_is( 'video', $attachment_id ) || Utils::is_saving_metadata() ) {
if ( $this->is_replacement_paused( $attachment_id ) ) {
return $image;
}

Expand Down Expand Up @@ -1836,6 +1836,81 @@ public function filter_downsize( $image, $attachment_id, $size ) {
return $image;
}

/**
* Whether URL replacement should be paused for this attachment right now: while a
* downsize is already in progress (re-entrancy guard), while content or metadata is being
* saved, or -- unless explicitly included -- for a video attachment, since there is no
* "image size" to downsize a video to.
*
* Shared by filter_downsize(), filter_attachment_image_src(), and attachment_url(), so the
* save-state/re-entrancy logic lives in one place.
*
* @param int $attachment_id The attachment ID.
* @param bool $exclude_videos Whether a video attachment should also count as paused.
*
* @return bool
*/
private function is_replacement_paused( $attachment_id, $exclude_videos = true ) {
if ( true === $this->in_downsize || doing_filter( 'content_save_pre' ) || Utils::is_saving_metadata() ) {
return true;
}

return $exclude_videos && wp_attachment_is( 'video', $attachment_id );
}

/**
* Correct wp_get_attachment_image_src() results that image_downsize() missed, or that a
* later-priority plugin overwrote, so Cloudinary keeps the last word on this specific,
* commonly-targeted core hook.
*
* @param array|false $image The image src array, or false.
* @param int $attachment_id The ID of the attachment.
* @param string|array $size The requested size of the image.
* @param bool $icon Whether the image should be treated as an icon.
*
* @return array|false The image array of size and url.
* @uses filter:wp_get_attachment_image_src
*/
public function filter_attachment_image_src( $image, $attachment_id, $size, $icon ) {
if ( empty( $image ) ) {
return $image;
}

// Fast bow-out: already a Cloudinary URL, nothing to do.
if ( $this->is_cloudinary_url( $image[0] ) ) {
return $image;
}

// Only images and preview-capable formats (PDF, PSD) have a real image representation to
// correct to. is_deliverable() treats every other attachment type as deliverable too, so
// without this, a synced non-image (audio, zip, docx, ...) requested with icon=true would
// have its generic mime icon replaced with the raw asset's Cloudinary URL, breaking the
// resulting <img> tag.
if ( ! wp_attachment_is_image( $attachment_id ) && ! $this->is_preview_only( $attachment_id ) ) {
return $image;
}

if ( $this->is_replacement_paused( $attachment_id ) ) {
return $image;
}

if ( ! $this->plugin->get_component( 'delivery' )->is_deliverable( $attachment_id ) ) {
return $image;
}

$cloudinary_id = $this->cloudinary_id( $attachment_id );
if ( ! $cloudinary_id ) {
return $image;
}

$url = $this->cloudinary_url( $attachment_id, $size, array(), $cloudinary_id );
if ( $url ) {
$image[0] = $url;
Comment on lines +1906 to +1908

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.

warning: When $icon is true for a synced non-image attachment (audio, zip, docx, ...), WordPress gives a MIME icon array here. Delivery::is_deliverable() returns true for every non-image, non-video type, so this replaces the icon with the raw Cloudinary asset URL, and the result is a broken <img> (for example in the media list view).

Please limit this correction to images and supported preview formats (wp_attachment_is_image() || is_preview_only()). Please also add a test for a synced non-image attachment with icon=true.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That makes sense, thanks. Fixed as part of 6bfc2a5

}

return $image;
}

/**
* At the point of running wp_get_attachment_image_srcset, the $image_src Should be a Cloudinary URL, unless not synced.
* This will fix the $image_meta so that the there's a match $src_matched on wp_calculate_image_srcset.
Expand Down Expand Up @@ -3149,7 +3224,6 @@ public function add_live_url_filters() {
add_filter( 'wp_calculate_image_srcset', array( $this, 'image_srcset' ), 10, 5 );
add_filter( 'wp_get_attachment_url', array( $this, 'attachment_url' ), 10, 2 );
add_filter( 'wp_get_original_image_url', array( $this, 'original_attachment_url' ), 10, 2 );
add_filter( 'image_downsize', array( $this, 'filter_downsize' ), 10, 3 );
add_filter( 'wp_calculate_image_srcset_meta', array( $this, 'calculate_image_srcset_meta' ), 10, 3 );

// Hook into Featured Image cycle.
Expand Down Expand Up @@ -3191,6 +3265,14 @@ public function setup() {
if ( Utils::is_admin() ) {
$this->add_live_url_filters();
}

// image_downsize() and wp_get_attachment_image_src() are exercised by ordinary
// front-end theme code (e.g. the_post_thumbnail()), not just wp-admin screens, so
// these run everywhere rather than being scoped to add_live_url_filters()'s
// admin-only set.
add_filter( 'image_downsize', array( $this, 'filter_downsize' ), 10, 3 );
Comment on lines +3272 to +3273

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: These filters now run on the front end, but both bail out through is_replacement_paused() -> Utils::is_saving_metadata(), which is based on did_action(). After any post, term, or user meta write earlier in the request (view counters, session or analytics plugins, etc.), that check stays true until the request ends. Synced images then keep their local URLs for the rest of the page, which defeats the purpose of this change.

The new test file has to clear $GLOBALS['wp_actions'] in without_saving_metadata_guard() because of this same behaviour.

The guard should detect a metadata write that is running now (for example doing_action() / doing_filter(), or a flag set and cleared around the write), not one that ran before.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! This should be fixed now 6bfc2a5

add_filter( 'wp_get_attachment_image_src', array( $this, 'filter_attachment_image_src' ), PHP_INT_MAX, 4 );

// Filter default image Quality and Format transformations.
add_filter( 'cloudinary_default_qf_transformations_image', array( $this, 'default_image_transformations' ), 10 );
add_filter( 'cloudinary_default_freeform_transformations_image', array( $this, 'default_image_freeform_transformations' ), 10 );
Expand Down
8 changes: 6 additions & 2 deletions php/class-utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,11 @@ public static function strip_inline_svg_data_uris( $content ) {


/**
* Is saving metadata.
* Is metadata currently being saved, right now.
*
* Uses doing_action(), not did_action(): the latter is cumulative for the whole request, so
* it would stay true for the rest of the page after any earlier, unrelated post/term/user
* meta write (a view counter, a session plugin, etc.), long after that write finished.
*
* @return bool
*/
Expand All @@ -731,7 +735,7 @@ public static function is_saving_metadata() {
foreach ( $metadata['actions'] as $action ) {
foreach ( $metadata['objects'] as $object ) {
$inline_action = str_replace( array( '{object}', 'metadata' ), array( $object, 'meta' ), $action );
if ( did_action( $inline_action ) ) {
if ( doing_action( $inline_action ) ) {
$saving = true;
break;
}
Expand Down
Loading
Loading