'', '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; } }