WordPress Logo

Automatic Featured Image for WordPress Posts

The following script will automatically set the first image in a WordPress post as the Featured Image when the post is saved. You can override the functionality by selecting a Featured Image.

/**
 * @package WordPress
 * @subpackage Automatic Featured Image for WordPress Posts
 * @author That Stevens Guy
 * @phpcs:disable PSR1.Files.SideEffects
 */

/**
 * Transition post status action.
 *
 * @param string $new_status
 * @param string $old_status
 * @param WP_Post $post
 * @return void
 */
add_action('transition_post_status', function (string $new_status, string $old_status, WP_Post $post): void {
    if (defined('REST_REQUEST') && REST_REQUEST) {
        $published_post = $post;

        /**
         * REST requests need to postpone changes until "rest_after_insert_{$post->post_type}".
         *
         * @param WP_Post $post
         * @param WP_REST_Request $request
         * @param bool $creating
         * @return void
         */

        add_action("rest_after_insert_{$post->post_type}", function (
            WP_Post $post,
            WP_REST_Request $request,
            bool $creating
        ) use (
            $new_status,
            $old_status,
            $published_post
        ): void {
            if ($published_post->ID !== $post->ID) {
                return;
            }

            tsg_transition_post_status($new_status, $old_status, $post);
        }, 10, 3);
    } else {
        tsg_transition_post_status($new_status, $old_status, $post);
    }
}, 10, 3);

/**
 * Transition post status function.
 *
 * @param string $new_status
 * @param string $old_status
 * @param WP_Post $post
 * @return void
 */
function tsg_transition_post_status(string $new_status, string $old_status, WP_Post $post): void
{
    // Set the first image in post_content as the Featured Image. If one wasn't set.
    tsg_set_featured_image($post);
}

/**
 * Set the Featured Image automatically.
 *
 * @param WP_Post $post
 * @return void
 */
function tsg_set_featured_image(WP_Post $post): void
{
    if (!in_array($post->post_type, [ 'post' ])) {
        return;
    }

    // Bypass automatic featured image if the post thumbnail was set manually.
    if (has_post_thumbnail($post)) {
        return;
    }

    $attachment_ids = tsg_get_image_attachment_ids_from_post_content(
        $post,
        [
            'get_first_attachment_id' => true,
            'check_aspect_ratio' => true
        ]
    );

    if (!empty($attachment_ids[ 0 ])) {
        update_post_meta($post->ID, '_thumbnail_id', $attachment_ids[ 0 ]);
    }
}

/**
 * Get image attachment ids from post content.
 *
 * @param WP_Post $post
 * @param array $args
 * @return array
 */
function tsg_get_image_attachment_ids_from_post_content(WP_Post $post, array $args = []): array
{
    $args = array_merge([
        'get_first_attachment_id' => false,
        'check_aspect_ratio' => false,
        'horizontal_aspect_ratio' => 2.5,
        'vertical_aspect_ratio' => 2.5
    ], $args);

    $attachment_ids = [];

    $images = tsg_get_images_from_post_content($post);

    if (empty($images)) {
        return $attachment_ids;
    }

    $site_url = parse_url(site_url());

    foreach ($images as $image) {
        // If the image is NOT from the current site, skip it.
        if (strpos($image[ 'src' ], $site_url[ 'host' ] . '/' . explode('/', $image[ 'src' ])[ 3 ]) === false) {
            continue;
        }

        $guid = tsg_get_original_image_src($image[ 'src' ]);

        if (empty($guid)) {
            continue;
        }

        $attachment_id = tsg_get_post_id_by_guid($guid);

        if (empty($attachment_id)) {
            continue;
        }

        if ($args[ 'check_aspect_ratio' ]) {
            $attachment_metadata = get_metadata('post', $attachment_id, '_wp_attachment_metadata', true);

            if (
                !tsg_check_image_aspect_ratio(
                    $attachment_metadata,
                    $args[ 'horizontal_aspect_ratio' ],
                    $args[ 'vertical_aspect_ratio' ]
                )
            ) {
                continue;
            }
        }

        $attachment_ids[] = $attachment_id;

        if ($args[ 'get_first_attachment_id' ]) {
            break;
        }
    }

    return $attachment_ids;
}

/**
 * Get the original image source, size 'full'.
 *
 * @param string $url
 * @param array $args
 * @return string
 */
function tsg_get_original_image_src(string $url, array $args = []): string
{
    if (!$url) {
        return $url;
    }

    $args = array_merge([
        'check_exists' => false,
        'check_filesize' => false,
        'filesize_limit' => 4000000,
        'strip_edit' => false
    ], $args);

    // Strip the thumbnail size at the end of the URL so that we end up with what
    // potentially could be the full size original image source.
    //
    // There is an edge case where the original URL has dimensions in the filename
    // with the same format. These will be skipped, this is unaviodable, particularly
    // for an offsite URL.
    $url = preg_replace("/\-\d{2,4}[xX]\d{2,4}(\.[a-zA-Z]{2,4})$/", '$1', $url);

    // Strip the edit timestamp for WordPress edited images.
    // Turns out this isn't the best idea, end up with unedited images. But can be used for some things.
    if (!empty($args[ 'strip_edit' ])) {
        if (strpos($url, '-e') !== false) {
            $pathinfo = pathinfo($url);

            if (
                !empty($pathinfo[ 'dirname' ]) &&
                !empty($pathinfo[ 'filename' ]) &&
                !empty($pathinfo[ 'extension' ])
            ) {
                $filename_split = array_reverse(explode('-e', $pathinfo[ 'filename' ]));

                if (!empty($filename_split[ 0 ]) && is_int((int)$filename_split[ 0 ])) {
                    unset($filename_split[ 0 ]);
                }

                $url = $pathinfo[ 'dirname' ] . '/' .
                    implode('-e', array_reverse($filename_split)) . '.' . $pathinfo [ 'extension' ];
            }
        }
    }

    // Because we've chopped the URL up so much, we may want to check if the image even exists.
    if (!empty($args[ 'check_exists' ]) || !empty($args[ 'check_filesize' ])) {
        $stream_options = [
            'http' => [
                'user_agent' =>
                    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6"
            ],
            // 'ssl'  => [
            //     'verify_peer' => false,
            //     'verify_peer_name' => false
            // ]
        ];
        $stream_context = stream_context_create($stream_options);
        $headers = @get_headers($url, true, $stream_context);

        if (!empty($args[ 'check_exists' ])) {
            if (empty($headers[ 0 ]) || strpos($headers[ 0 ], '404') !== false) {
                $url = '';
            }
        }

        if ($url && !empty($args[ 'check_filesize' ]) && !empty($args[ 'filesize_limit' ])) {
            if (
                empty($headers[ 'Content-Length' ]) ||
                (int)$headers[ 'Content-Length' ] > (int)$args[ 'filesize_limit' ]
            ) {
                $url = '';
            }
        }
    }

    return $url;
}

/**
 * Get all images from the post content.
 *
 * @param WP_Post $post
 * @return array
 */
function tsg_get_images_from_post_content(WP_Post $post): array
{
    $images = [];

    if (empty($post->post_content)) {
        return $images;
    }

    $content = apply_filters('the_content', $post->post_content);

    preg_match_all('/<img\b[^>]+src=[\'"]([^\'"]+\.(?:jpg|png|jpeg))[\'"][^>]*>/i', $content, $matchesImages);

    if (!empty($matchesImages[ 0 ])) {
        foreach ($matchesImages[ 0 ] as $key => $img) {
            $images[ $key ][ 'img' ] = $img;
            $images[ $key ][ 'src' ] = $matchesImages[ 1 ][ $key ];

            preg_match_all(
                '/(<img\b|(?!^)\G)[^>]*?\b(alt|width|height|srcset|sizes)=([\'"]?)([^>]*?)\3/i',
                $img,
                $matchesAttr
            );

            if (!empty($matchesAttr[ 2 ])) {
                foreach ($matchesAttr[ 2 ] as $attr_key => $attr) {
                    if (!empty($matchesAttr[ 4 ][ $attr_key ])) {
                        $images[ $key ][ $attr ] = $matchesAttr[ 4 ][ $attr_key ];
                    }
                }
            }
        }
    }

    $images = apply_filters('tsg_get_images_from_post_content', $images, $post);

    return $images;
}

/**
 * Get check if an image fits within a suitable aspect ratio.
 *
 * @param array $image [ 'height' => int, 'width' => int ]
 * @param float $horizontal_aspect_ratio
 * @param float $vertical_aspect_ratio
 * @return bool
 */
function tsg_check_image_aspect_ratio(
    array $image,
    float $horizontal_aspect_ratio = 2.5,
    float $vertical_aspect_ratio = 2.5
): bool {
    if (empty($image[ 'width' ]) || empty($image[ 'height' ])) {
        return false;
    }

    $calculated_horizontal_aspect_ratio = (int)$image[ 'width' ] / (int)$image[ 'height' ];
    $calculated_vertical_aspect_ratio = (int)$image[ 'height' ] / (int)$image[ 'width' ];

    if (
        $calculated_horizontal_aspect_ratio > $horizontal_aspect_ratio ||
        $calculated_vertical_aspect_ratio > $vertical_aspect_ratio
    ) {
        return false;
    }

    return true;
}

/**
 * Get post ID by guid.
 *
 * @param string $guid
 * @return int ID if found, 0 if not
 */
function tsg_get_post_id_by_guid(string $guid): int
{
    global $wpdb;

    $post_id = $wpdb->get_var(
        $wpdb->prepare("
            SELECT ID
            FROM $wpdb->posts
            WHERE instr( guid, '%s' ) > 0
        ", $guid)
    );

    return intval($post_id);
}

https://gist.github.com/ThatStevensGuy/7020010fe667106f79d2556f386933d0