'',
'size' => 'thumbnail',
'thumb_meta' => 'post-image',
'acf_field' => '',
'thumb_html' => 'html',
'thumb_default' => self::get_option( 'thumb_default', '' ),
'thumb_default_show' => true,
'scan_images' => true,
'use_site_icon' => true,
'class' => self::$prefix . '_thumb',
'style' => '',
);
// Parse incomming $args into an array and merge it with $defaults.
$args = wp_parse_args( $args, $defaults );
$result = get_post( $args['post'] );
if ( empty( $result ) ) {
return '';
}
// Recursion protection - prevent infinite loops when shortcodes trigger nested thumbnail generation.
if ( isset( self::$processing_ids[ $result->ID ] ) ) {
return '';
}
self::$processing_ids[ $result->ID ] = true;
try {
if ( is_string( $args['size'] ) ) {
list( $args['thumb_width'], $args['thumb_height'] ) = self::get_thumb_size( $args['size'] );
} else {
$args['thumb_width'] = $args['size'][0];
$args['thumb_height'] = $args['size'][1];
$args['size'] = self::get_appropriate_image_size( $args['size'][0], $args['size'][1] );
}
$post_title = esc_attr( $result->post_title );
$output = '';
$postimage = '';
$pick = '';
$attachment_id = 0;
$extracted_alt = '';
$alt = '';
$strategies = array(
fn() => self::get_thumbnail_from_meta( $result, $args ),
fn() => self::get_thumbnail_from_acf( $result, $args ),
fn() => self::get_thumbnail_from_fifu( $result ),
fn() => self::get_thumbnail_from_featured_image( $result, $args ),
fn() => self::get_thumbnail_from_content_scan( $result, $args ),
fn() => self::get_thumbnail_from_first_child( $result, $args ),
fn() => self::get_thumbnail_from_video_meta( $result ),
fn() => self::get_thumbnail_from_default_thumb( $args ),
fn() => self::get_thumbnail_from_site_icon( $args ),
);
foreach ( $strategies as $strategy ) {
$thumb = $strategy();
if ( ! empty( $thumb['postimage'] ) ) {
$postimage = $thumb['postimage'];
$attachment_id = $thumb['attachment_id'];
$pick = $thumb['pick'];
$extracted_alt = $thumb['extracted_alt'];
break;
}
}
// Hopefully, we've found a thumbnail by now. If so, run it through the custom filter, check for SSL and create the image tag.
if ( $postimage ) {
/**
* Filters the thumbnail image URL.
*
* Use this filter to modify the thumbnail URL that is automatically created
* Before v2.1 this was used for cropping the post image using timthumb
*
* @param string $postimage URL of the thumbnail image
* @param array $args Arguments array.
* @param \WP_Post $result Post Object
*/
$postimage = apply_filters( self::$prefix . '_thumb_url', $postimage, $args, $result );
if ( is_ssl() ) {
$postimage = preg_replace( '~http://~', 'https://', $postimage );
}
$class = self::$prefix . "_{$pick} {$args['class']} {$args['size']}";
if ( empty( $attachment_id ) && ! in_array( $pick, array( 'video_thumb', 'default_thumb', 'site_icon_max', 'site_icon_min', 'fifu', 'acf', 'acfcorrect' ), true ) ) {
$attachment_id = self::get_cached_attachment_id( $postimage );
}
/**
* Flag to use the image's alt text as the thumbnail alt text.
*
* @param bool $use_image_alt Flag to use the image's alt text as the thumbnail alt text.
*/
$use_image_alt = apply_filters( self::$prefix . '_thumb_use_image_alt', true );
/**
* Flag to use the post title as the thumbnail alt text if no alt text is found.
*
* @param bool $alt_fallback Flag to use the post title as the thumbnail alt text if no alt text is found.
*/
$alt_fallback = apply_filters( self::$prefix . '_thumb_alt_fallback_post_title', true );
if ( ! empty( $attachment_id ) && $use_image_alt ) {
$alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
}
if ( empty( $alt ) && $extracted_alt ) {
$alt = $extracted_alt;
}
// If empty alt then try to get the title of the attachment.
if ( empty( $alt ) && ! empty( $attachment_id ) ) {
$alt = get_post_field( 'post_title', $attachment_id );
}
if ( empty( $alt ) ) {
$alt = $alt_fallback ? $post_title : '';
}
/**
* Filters the thumbnail styles attribute.
*
* @param string $styles Thumbnail styles
*/
$attr['style'] = apply_filters( self::$prefix . '_thumb_styles', $args['style'] );
/**
* Filters the thumbnail classes and allows a filter function to add any more classes if needed.
*
* @param string $class Thumbnail Class
*/
$attr['class'] = apply_filters( self::$prefix . '_thumb_class', $class );
/**
* Filters the thumbnail alt.
*
* @param string $alt Thumbnail alt attribute
*/
$attr['alt'] = apply_filters( self::$prefix . '_thumb_alt', $alt );
/**
* Filters the thumbnail title.
*
* @param string $post_title Thumbnail title attribute
*/
$attr['title'] = apply_filters( self::$prefix . '_thumb_title', $post_title );
$attr['thumb_html'] = $args['thumb_html'];
$attr['thumb_width'] = $args['thumb_width'];
$attr['thumb_height'] = $args['thumb_height'];
$output .= self::get_image_html( $postimage, $attr, $attachment_id, $args['size'] );
if ( function_exists( 'wp_img_tag_add_srcset_and_sizes_attr' ) && ! empty( $attachment_id ) ) {
$output = \wp_img_tag_add_srcset_and_sizes_attr( $output, $args['size'], $attachment_id );
}
if ( function_exists( 'wp_img_tag_add_loading_optimization_attrs' ) ) {
$output = \wp_img_tag_add_loading_optimization_attrs( $output, self::$prefix . '_thumbnail' );
}
}
/**
* Filters post thumbnail HTML.
*
* @param string $output HTML output.
* @param array $args Argument list
* @param string $postimage Thumbnail URL
*/
return apply_filters( self::$prefix . '_get_the_post_thumbnail', $output, $args, $postimage );
} finally {
// Clean up recursion protection - guaranteed to run even if exceptions occur.
unset( self::$processing_ids[ $result->ID ] );
}
}
/**
* Resize external image when attachment ID is not found.
*
* @param string $image_url Original image URL.
* @param string $size Target image size.
* @return string|false Resized image URL or false on failure.
*/
private static function resize_external_image( $image_url, $size ) {
// Check if this is a local image that can be resized.
$upload_dir = wp_upload_dir();
if ( empty( $upload_dir['baseurl'] ) ) {
return false;
}
$base_url = $upload_dir['baseurl'];
// Only attempt resizing for local uploads directory images.
if ( strpos( $image_url, $base_url ) !== 0 ) {
return false;
}
// Strip any existing size suffix to get the base/original image URL.
$base_image_url = self::get_base_image_url( $image_url );
// Convert URL to file path.
$image_path = str_replace( $base_url, $upload_dir['basedir'], $base_image_url );
$image_path = urldecode( $image_path ); // Handle URL-encoded characters (spaces, special chars).
// If base image doesn't exist, try the original URL path (might be the actual original).
if ( ! file_exists( $image_path ) ) {
$image_path = str_replace( $base_url, $upload_dir['basedir'], $image_url );
$image_path = urldecode( $image_path );
if ( ! file_exists( $image_path ) ) {
return false;
}
}
// Security: Validate path stays within uploads directory (after confirming file exists).
$real_image_path = realpath( $image_path );
$real_upload_dir = realpath( $upload_dir['basedir'] );
if ( false === $real_image_path || false === $real_upload_dir ||
0 !== strpos( $real_image_path, $real_upload_dir ) ) {
return false;
}
$image_path = $real_image_path;
// Get image dimensions for the target size.
$dimensions = self::get_thumb_size( $size );
$width = $dimensions[0];
$height = $dimensions[1];
// Generate resized filename from the BASE image (without any size suffix).
$path_info = pathinfo( $image_path );
// Strip any existing size suffix from the filename to ensure clean base name.
$base_filename = preg_replace( '/-\d+x\d+$/', '', $path_info['filename'] );
$resized_filename = $base_filename . "-{$width}x{$height}." . $path_info['extension'];
$resized_path = $path_info['dirname'] . '/' . $resized_filename;
$resized_url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $resized_path );
// Security: Validate resized output path stays within uploads directory.
$resized_dir = dirname( $resized_path );
$real_resized_dir = realpath( $resized_dir );
if ( false === $real_resized_dir ||
0 !== strpos( $real_resized_dir, $real_upload_dir ) ) {
return false;
}
// Return existing resized image if it exists.
if ( file_exists( $resized_path ) ) {
return $resized_url;
}
// Attempt to create resized image from the original/base image.
$image_editor = wp_get_image_editor( $image_path );
if ( is_wp_error( $image_editor ) ) {
return false;
}
// Security: Check original image dimensions to prevent memory exhaustion.
$original_size = $image_editor->get_size();
// Reject images larger than 10000x10000 pixels (adjustable via filter).
$max_dimension = apply_filters( self::$prefix . '_max_image_dimension', 10000 );
if ( $original_size['width'] > $max_dimension || $original_size['height'] > $max_dimension ) {
return false;
}
$resized = $image_editor->resize( $width, $height, true );
if ( is_wp_error( $resized ) ) {
return false;
}
$saved = $image_editor->save( $resized_path );
if ( is_wp_error( $saved ) ) {
return false;
}
return $resized_url;
}
/**
* Resolve a thumbnail from an ACF Image or Text field.
*
* Handles all three ACF Image field return formats (Image Array, Image ID,
* Image URL) as well as a plain Text field returning a URL string.
*
* @param string $acf_field ACF field name.
* @param int $post_id Post ID to retrieve the field value from.
* @param string $size Registered image size slug.
* @return array {
* @type string $postimage Image URL, or empty string if none found.
* @type int $attachment_id Attachment ID when resolvable, otherwise 0.
* @type string $pick Source identifier used for CSS class generation.
* }
*/
protected static function get_acf_thumbnail( string $acf_field, int $post_id, string $size ): array {
$result = array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
);
$acf_value = get_field( $acf_field, $post_id );
if ( empty( $acf_value ) ) {
return $result;
}
if ( is_array( $acf_value ) ) {
// Image Array return format — prefer the attachment ID for full srcset support.
if ( ! empty( $acf_value['id'] ) ) {
$attachment_id = (int) $acf_value['id'];
$postthumb = wp_get_attachment_image_src( $attachment_id, $size );
if ( false !== $postthumb ) {
$result['postimage'] = $postthumb[0];
$result['attachment_id'] = $attachment_id;
$result['pick'] = 'acf';
}
} elseif ( ! empty( $acf_value['url'] ) ) {
$validated = filter_var( $acf_value['url'], FILTER_VALIDATE_URL );
if ( $validated ) {
$result['postimage'] = $validated;
$result['pick'] = 'acf';
}
}
} elseif ( is_numeric( $acf_value ) ) {
// Image ID return format.
$attachment_id = (int) $acf_value;
$postthumb = wp_get_attachment_image_src( $attachment_id, $size );
if ( false !== $postthumb ) {
$result['postimage'] = $postthumb[0];
$result['attachment_id'] = $attachment_id;
$result['pick'] = 'acf';
}
} else {
// Image URL return format, or a Text field containing a URL.
$validated = filter_var( $acf_value, FILTER_VALIDATE_URL );
if ( $validated ) {
$attachment_id = self::get_cached_attachment_id( $validated );
$postthumb = wp_get_attachment_image_src( $attachment_id, $size );
$result['postimage'] = $postthumb ? $postthumb[0] : $validated;
$result['attachment_id'] = $attachment_id;
$result['pick'] = $postthumb ? 'acfcorrect' : 'acf';
}
}
return $result;
}
/**
* Get thumbnail from the post meta field.
*
* @param \WP_Post $post Post object.
* @param array $args Arguments array.
* @return array{postimage: string, attachment_id: int, pick: string, extracted_alt: string}
*/
protected static function get_thumbnail_from_meta( \WP_Post $post, array $args ): array {
$postimage = get_post_meta( $post->ID, $args['thumb_meta'], true );
$postimage = filter_var( $postimage, FILTER_VALIDATE_URL );
if ( ! $postimage ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
$pick = 'meta';
$attachment_id = self::get_cached_attachment_id( $postimage );
$postthumb = wp_get_attachment_image_src( $attachment_id, $args['size'] );
if ( false !== $postthumb ) {
$postimage = $postthumb[0];
$pick .= 'correct';
}
return array(
'postimage' => $postimage,
'attachment_id' => $attachment_id,
'pick' => $pick,
'extracted_alt' => '',
);
}
/**
* Get thumbnail from an ACF field.
*
* @param \WP_Post $post Post object.
* @param array $args Arguments array.
* @return array{postimage: string, attachment_id: int, pick: string, extracted_alt: string}
*/
protected static function get_thumbnail_from_acf( \WP_Post $post, array $args ): array {
if ( ! $args['acf_field'] || ! function_exists( 'get_field' ) ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
$result = self::get_acf_thumbnail( $args['acf_field'], $post->ID, $args['size'] );
$result['extracted_alt'] = '';
return $result;
}
/**
* Get thumbnail from the FIFU (Featured Image from URL) plugin.
*
* @param \WP_Post $post Post object.
* @return array{postimage: string, attachment_id: int, pick: string, extracted_alt: string}
*/
protected static function get_thumbnail_from_fifu( \WP_Post $post ): array {
/**
* Filters the FIFU meta key used to store external image URLs.
*
* @param string $fifu_meta_key Meta key used by FIFU plugin.
*/
$fifu_meta_key = apply_filters( self::$prefix . '_fifu_meta_key', 'fifu_image_url' );
$fifu_image_url = get_post_meta( $post->ID, $fifu_meta_key, true );
$fifu_image_url = filter_var( $fifu_image_url, FILTER_VALIDATE_URL );
if ( ! $fifu_image_url ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
return array(
'postimage' => $fifu_image_url,
'attachment_id' => 0,
'pick' => 'fifu',
'extracted_alt' => '',
);
}
/**
* Get thumbnail from the post's featured image.
*
* @param \WP_Post $post Post object.
* @param array $args Arguments array.
* @return array{postimage: string, attachment_id: int, pick: string, extracted_alt: string}
*/
protected static function get_thumbnail_from_featured_image( \WP_Post $post, array $args ): array {
if ( false === get_post_thumbnail_id( $post->ID ) ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
$attachment_id = ( 'attachment' === $post->post_type ) ? $post->ID : get_post_thumbnail_id( $post->ID );
$postthumb = wp_get_attachment_image_src( $attachment_id, $args['size'] );
if ( false === $postthumb ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
return array(
'postimage' => $postthumb[0],
'attachment_id' => $attachment_id,
'pick' => 'featured',
'extracted_alt' => '',
);
}
/**
* Get thumbnail by scanning the post content for img tags.
*
* @param \WP_Post $post Post object.
* @param array $args Arguments array.
* @return array{postimage: string, attachment_id: int, pick: string, extracted_alt: string}
*/
protected static function get_thumbnail_from_content_scan( \WP_Post $post, array $args ): array {
if ( ! $args['scan_images'] ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
// Skip content scanning for very large posts to prevent memory exhaustion.
if ( strlen( $post->post_content ) > 50000 ) { // 50KB limit.
$post_content = '';
} else {
/**
* Filters the post content that is used to scan for images.
*
* A filter function can be tapped into this to execute shortcodes, modify content, etc.
*
* @param string $post_content Post content.
* @param \WP_Post $post Post object.
*/
$post_content = apply_filters( self::$prefix . '_thumb_post_content', $post->post_content, $post );
}
preg_match_all( '/
]*src=[\'"]([^\'"]+)[\'"][^>]*>/i', $post_content, $matches );
if ( ! isset( $matches[1][0] ) || ! $matches[1][0] ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
$postimage = $matches[1][0];
$extracted_alt = self::get_alt_from_img_tag( $matches[0][0] );
$pick = 'first';
$attachment_id = self::get_cached_attachment_id( $postimage );
$postthumb = wp_get_attachment_image_src( $attachment_id, $args['size'] );
if ( false !== $postthumb ) {
$postimage = $postthumb[0];
$pick .= 'correct';
} else {
// Fallback: Try to resize the original URL if no attachment found.
$resized_url = self::resize_external_image( $postimage, $args['size'] );
if ( $resized_url ) {
$postimage = $resized_url;
$pick .= 'resized';
}
}
return array(
'postimage' => $postimage,
'attachment_id' => $attachment_id,
'pick' => $pick,
'extracted_alt' => $extracted_alt,
);
}
/**
* Get thumbnail from the first attached child image.
*
* @param \WP_Post $post Post object.
* @param array $args Arguments array.
* @return array{postimage: string, attachment_id: int, pick: string, extracted_alt: string}
*/
protected static function get_thumbnail_from_first_child( \WP_Post $post, array $args ): array {
$dimensions = self::get_thumb_size( $args['size'] );
$postimage = self::get_first_image( $post->ID, $dimensions[0], $dimensions[1] );
if ( ! $postimage ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
return array(
'postimage' => $postimage,
'attachment_id' => 0,
'pick' => 'firstchild',
'extracted_alt' => '',
);
}
/**
* Get thumbnail from the Video Thumbnails plugin meta field.
*
* @param \WP_Post $post Post object.
* @return array{postimage: string, attachment_id: int, pick: string, extracted_alt: string}
*/
protected static function get_thumbnail_from_video_meta( \WP_Post $post ): array {
$postimage = get_post_meta( $post->ID, '_video_thumbnail', true );
$postimage = filter_var( $postimage, FILTER_VALIDATE_URL );
if ( ! $postimage ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
return array(
'postimage' => $postimage,
'attachment_id' => 0,
'pick' => 'video_thumb',
'extracted_alt' => '',
);
}
/**
* Get thumbnail from the configured default thumbnail setting.
*
* @param array $args Arguments array.
* @return array{postimage: string, attachment_id: int, pick: string, extracted_alt: string}
*/
protected static function get_thumbnail_from_default_thumb( array $args ): array {
if ( ! $args['thumb_default_show'] || ! $args['thumb_default'] ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
$postimage = $args['thumb_default'];
$pick = 'default_thumb';
$attachment_id = 0;
if ( self::$default_thumb_url !== $postimage ) {
$attachment_id = self::get_cached_attachment_id( $postimage );
$postthumb = wp_get_attachment_image_src( $attachment_id, $args['size'] );
if ( false !== $postthumb ) {
$postimage = $postthumb[0];
$pick .= 'correct';
}
}
return array(
'postimage' => $postimage,
'attachment_id' => $attachment_id,
'pick' => $pick,
'extracted_alt' => '',
);
}
/**
* Get thumbnail from the site icon.
*
* @param array $args Arguments array.
* @return array{postimage: string, attachment_id: int, pick: string, extracted_alt: string}
*/
protected static function get_thumbnail_from_site_icon( array $args ): array {
if ( ! $args['use_site_icon'] ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
$postimage = get_site_icon_url( max( $args['thumb_width'], $args['thumb_height'] ) );
$pick = 'site_icon_max';
// Fallback to min size if max size not available.
if ( ! $postimage ) {
$postimage = get_site_icon_url( min( $args['thumb_width'], $args['thumb_height'] ) );
$pick = 'site_icon_min';
}
if ( ! $postimage ) {
return array(
'postimage' => '',
'attachment_id' => 0,
'pick' => '',
'extracted_alt' => '',
);
}
return array(
'postimage' => $postimage,
'attachment_id' => 0,
'pick' => $pick,
'extracted_alt' => '',
);
}
/**
* Get an HTML img element.
*
* When an attachment ID is available the function delegates to
* {@see wp_get_attachment_image()} so that srcset, sizes and all
* core image optimisations are applied automatically.
*
* When only a URL is available (external images, meta-key URLs, etc.)
* the function builds the `
` tag manually using the configured
* thumbnail dimensions and attributes.
*
* @param string $attachment_url Image URL.
* @param array $attr Optional. Attributes for the image markup.
* @param int $attachment_id Optional. Attachment ID. Default 0.
* @param string|int[] $size Optional. Registered image size name or
* array of width and height values in pixels.
* Default empty string.
* @return string HTML img element or empty string on failure.
*/
public static function get_image_html( $attachment_url, $attr = array(), $attachment_id = 0, $size = '' ) {
// If there is an attachment ID, delegate to wp_get_attachment_image().
if ( $attachment_id ) {
$attr = self::ensure_loading_and_decoding_attrs( $attr );
$attr = self::sanitize_image_attrs( $attr );
return wp_get_attachment_image( $attachment_id, $size, false, $attr );
}
// If there is no URL, return an empty string.
if ( empty( $attachment_url ) ) {
return '';
}
// Define default attributes.
$default_attr = array(
'src' => $attachment_url,
'alt' => '',
'thumb_html' => self::get_option( 'thumb_html', 'html' ),
'thumb_width' => self::get_option( 'thumb_width', 150 ),
'thumb_height' => self::get_option( 'thumb_height', 150 ),
'class' => "attachment-$size size-$size",
);
// Merge default attributes with provided attributes.
$attr = wp_parse_args( $attr, $default_attr );
$attr = self::ensure_loading_and_decoding_attrs( $attr );
// Generate width and height string before thumb_* keys are stripped.
$hwstring = self::get_image_hwstring( $attr );
// Add 'auto' to the sizes attribute for lazy-loaded images.
if (
isset( $attr['loading'] ) &&
'lazy' === $attr['loading'] &&
isset( $attr['sizes'] ) &&
function_exists( 'wp_sizes_attribute_includes_valid_auto' ) &&
! wp_sizes_attribute_includes_valid_auto( $attr['sizes'] )
) {
$attr['sizes'] = 'auto, ' . $attr['sizes'];
}
// Sanitise and strip internal attributes.
$attr = self::sanitize_image_attrs( $attr );
/**
* Filters the list of attachment image attributes.
*
* @param array $attr Attributes for the image markup.
* @param string $attachment_url Image URL.
*/
$attr = apply_filters( self::$prefix . '_get_image_attributes', $attr, $attachment_url );
$attr = array_map( 'esc_attr', $attr );
// Construct the HTML img tag.
$html = '
$value ) {
if ( '' !== $value ) {
$html .= " $name=" . '"' . $value . '"';
}
}
$html .= ' />';
/**
* Filters the img tag.
*
* @param string $html HTML img element or empty string on failure.
* @param string $attachment_url Image URL.
* @param array $attr Attributes for the image markup.
*/
return apply_filters( self::$prefix . '_get_image_html', $html, $attachment_url, $attr );
}
/**
* Ensures the loading/decoding attributes are set consistently for all thumbnails.
*
* @param array $attr Attributes array.
* @return array
*/
protected static function ensure_loading_and_decoding_attrs( array $attr ): array {
if ( empty( $attr['loading'] ) ) {
/**
* Filters the default loading attribute applied to Contextual Related Posts thumbnails.
*
* @param string|null $loading Loading attribute value or null to omit.
* @param array $attr Thumbnail attributes.
*/
$attr['loading'] = apply_filters( self::$prefix . '_thumbnail_loading_attribute', 'lazy', $attr );
}
if ( empty( $attr['decoding'] ) ) {
/**
* Filters the default decoding attribute applied to Contextual Related Posts thumbnails.
*
* @param string|null $decoding Decoding attribute value or null to omit.
* @param array $attr Thumbnail attributes.
*/
$attr['decoding'] = apply_filters( self::$prefix . '_thumbnail_decoding_attribute', 'async', $attr );
}
return $attr;
}
/**
* Sanitises optional image attributes before rendering.
*
* Removes `decoding`, `loading`, and `fetchpriority` when their values
* are empty or invalid, and strips internal `thumb_*` keys that must
* never appear in the final `
` tag.
*
* @param array $attr Image attributes.
* @return array Cleaned attributes.
*/
private static function sanitize_image_attrs( array $attr ): array {
// Omit the `decoding` attribute if the value is invalid according to the spec.
if ( empty( $attr['decoding'] ) || ! in_array( $attr['decoding'], array( 'async', 'sync', 'auto' ), true ) ) {
unset( $attr['decoding'] );
}
/*
* If the default value of `lazy` for the `loading` attribute is overridden
* to omit the attribute for this image, ensure it is not included.
*/
if ( isset( $attr['loading'] ) && ! $attr['loading'] ) {
unset( $attr['loading'] );
}
// If the `fetchpriority` attribute is overridden and set to false or an empty string.
if ( isset( $attr['fetchpriority'] ) && ! $attr['fetchpriority'] ) {
unset( $attr['fetchpriority'] );
}
// Strip internal keys that must not appear in the final
tag.
unset( $attr['thumb_html'], $attr['thumb_width'], $attr['thumb_height'] );
return $attr;
}
/**
* Extract alt text from an image tag string.
*
* @param string $img_tag Image tag HTML.
* @return string Sanitized alt text or empty string if none found.
*/
private static function get_alt_from_img_tag( string $img_tag ): string {
if ( ! preg_match( '/\salt=(\"|\')(.*?)\1/i', $img_tag, $matches ) ) {
return '';
}
$alt = wp_specialchars_decode( $matches[2], ENT_QUOTES );
$alt = sanitize_text_field( $alt );
return $alt;
}
/**
* Retrieve width and height attributes using given width and height values.
*
* @param array $args Argument array.
* @return string Height-width string.
*/
public static function get_image_hwstring( $args = array() ) {
$default_args = array(
'thumb_html' => self::get_option( 'thumb_html', 'html' ),
'thumb_width' => self::get_option( 'thumb_width', 150 ),
'thumb_height' => self::get_option( 'thumb_height', 150 ),
);
$args = wp_parse_args( $args, $default_args );
if ( 'css' === $args['thumb_html'] ) {
$thumb_html = ' style="max-width:' . $args['thumb_width'] . 'px;max-height:' . $args['thumb_height'] . 'px;" ';
} elseif ( 'html' === $args['thumb_html'] ) {
$thumb_html = ' width="' . $args['thumb_width'] . '" height="' . $args['thumb_height'] . '" ';
} else {
$thumb_html = '';
}
/**
* Filters the thumbnail HTML and allows a filter function to add any more HTML if needed.
*
* @param string $thumb_html Thumbnail HTML.
* @param array $args Argument array.
*/
return apply_filters( self::$prefix . '_thumb_html', $thumb_html, $args );
}
/**
* Get the first child image in the post.
*
* @param int|\WP_Post $postid Post ID or WP_Post object.
* @param int $thumb_width Thumb width.
* @param int $thumb_height Thumb height.
* @return string Location of thumbnail.
*/
public static function get_first_image( $postid, int $thumb_width, int $thumb_height ): string {
$args = array(
'numberposts' => 1,
'order' => 'ASC',
'post_mime_type' => 'image',
'post_parent' => $postid,
'post_status' => 'inherit',
'post_type' => 'attachment',
);
$attachments = get_children( $args );
if ( empty( $attachments ) ) {
return '';
}
$attachment = reset( $attachments );
$image_size = array( $thumb_width, $thumb_height );
if ( 0 < $attachment->ID ) {
$image_attributes = wp_get_attachment_image_src( $attachment->ID, $image_size );
if ( empty( $image_attributes ) ) {
$image_attributes = wp_get_attachment_image_src( $attachment->ID, 'full' );
}
if ( ! empty( $image_attributes ) ) {
/**
* Filter the first child image URL.
*
* @param string $image_url URL of the image.
* @param int|\WP_Post $postid Post ID or WP_Post object.
* @param int $thumb_width Thumb width.
* @param int $thumb_height Thumb height.
*/
return apply_filters(
self::$prefix . '_get_first_image',
$image_attributes[0],
$postid,
$thumb_width,
$thumb_height
);
}
}
return '';
}
/**
* Get cached attachment ID from URL to prevent database exhaustion.
*
* @param string $attachment_url Attachment URL.
* @return int Attachment ID.
*/
public static function get_cached_attachment_id( $attachment_url = '' ) {
$attachment_id = 0;
// If there is no URL, return.
if ( ! $attachment_url ) {
return $attachment_id;
}
// Check cache first.
$cache_key = self::$prefix . '_attachment_id_' . get_current_blog_id() . '_' . hash( 'sha256', $attachment_url );
$cached_id = wp_cache_get( $cache_key, self::$prefix . '_media' );
if ( false !== $cached_id ) {
return (int) $cached_id;
}
// Attempt to retrieve the attachment ID from the URL.
$attachment_id = attachment_url_to_postid( $attachment_url );
// If not found, try stripping the size suffix (e.g., -150x150, -1024x768) and lookup base URL.
if ( 0 === $attachment_id ) {
$base_url = self::get_base_image_url( $attachment_url );
if ( $base_url !== $attachment_url ) {
$attachment_id = attachment_url_to_postid( $base_url );
}
}
// Cache the result for 1 hour.
wp_cache_set( $cache_key, $attachment_id, self::$prefix . '_media', HOUR_IN_SECONDS );
/**
* Filter the cached attachment ID from the attachment URL.
*
* @param int $attachment_id Attachment ID.
* @param string $attachment_url Attachment URL.
*/
return apply_filters( self::$prefix . '_get_cached_attachment_id', $attachment_id, $attachment_url );
}
/**
* Get the base image URL by stripping WordPress size suffixes.
*
* Converts URLs like image-150x150.jpg or image-1024x768.jpg to image.jpg
*
* @param string $url Image URL potentially with size suffix.
* @return string Base image URL without size suffix.
*/
public static function get_base_image_url( $url ) {
// Remove WordPress size suffix (e.g., -150x150) while retaining filename and extension.
return preg_replace( '/-\d+x\d+(?=\.[^.]+$)/', '', $url );
}
/**
* Function to get the correct height and width of the thumbnail.
*
* @param string $size Image size.
* @return array Width and height. If no width and height is found, then 150 is returned for each.
*/
public static function get_thumb_size( $size = 'thumbnail' ) {
// Get thumbnail size.
$thumb_size_array = self::get_all_image_sizes( $size );
if ( isset( $thumb_size_array['width'] ) ) {
$thumb_width = $thumb_size_array['width'];
$thumb_height = $thumb_size_array['height'];
}
if ( isset( $thumb_width ) && isset( $thumb_height ) ) {
$thumb_size = array( $thumb_width, $thumb_height );
} else {
$thumb_size = array( 150, 150 );
}
/**
* Filter array of thumbnail size.
*
* @param array $thumb_size Array with width and height of thumbnail.
*/
return apply_filters( self::$prefix . '_get_thumb_size', $thumb_size );
}
/**
* Get all image sizes.
*
* @param string|int[] $size Image size.
* @return array If a single size is specified, then the array with width, height and crop status
* or an empty array if size is not found;
* If no size is specified then an Associative array of the registered image sub-sizes.
*/
public static function get_all_image_sizes( $size = '' ) {
if ( is_array( $size ) ) {
$size = self::get_appropriate_image_size( $size[0], $size[1] );
}
$sizes = wp_get_registered_image_subsizes();
/* Get only 1 size if found */
if ( $size ) {
if ( isset( $sizes[ $size ] ) ) {
return $sizes[ $size ];
}
return array();
}
/**
* Filters array of image sizes.
*
* @param array $sizes Image sizes.
*/
return apply_filters( self::$prefix . '_get_all_image_sizes', $sizes );
}
/**
* Get the most appropriate image size based on the given thumbnail width and height.
*
* @param int $thumb_width Thumbnail width.
* @param int $thumb_height Thumbnail height.
* @return string|bool Image size name if found, false otherwise.
*/
public static function get_appropriate_image_size( $thumb_width, $thumb_height ) {
$sizes = wp_get_registered_image_subsizes();
$closest_size = false;
$closest_distance = PHP_INT_MAX;
foreach ( $sizes as $size_name => $size_info ) {
$size_width = $size_info['width'];
$size_height = $size_info['height'];
$distance = sqrt( pow( $thumb_width - $size_width, 2 ) + pow( $thumb_height - $size_height, 2 ) );
if ( $distance < $closest_distance ) {
$closest_distance = $distance;
$closest_size = $size_name;
}
}
return $closest_size;
}
}