The Code
This is version 2.3.0 - a major update that makes images fully compatible with WordPress's block editor and media library system.
<?php
/**
* Plugin Name: RR — Import External Images on Save
* Description: On save (draft/publish), sideload external <img> to Media Library, rewrite src, and set alt/title/description from post title + increment.
* Version: 2.3.0
*
* Changelog:
* 2.3.0 - Fixed block validation by removing srcset/sizes from saved content
* - Gutenberg adds srcset/sizes automatically on frontend render
* - Block attributes now perfectly match saved HTML
* - No more "unexpected or invalid content" errors
* 2.2.0 - Complete block rebuild for validation
* 2.1.0 - Fixed duplicate uploads
* 2.0.0 - Major update: Full Gutenberg compatibility
* 1.4.0 - Previous stable version
*/
if ( ! defined( 'ABSPATH' ) ) { exit; }
/**
* ============================================
* CONFIGURATION OPTIONS
* ============================================
*/
define( 'RR_EXTIMG_SET_TITLE', false );
define( 'RR_EXTIMG_SET_ALT', false );
define( 'RR_EXTIMG_SET_CAPTION', false );
define( 'RR_EXTIMG_SET_DESCRIPTION', false );
define( 'RR_EXTIMG_DEFAULT_SIZE', 'large' );
add_action( 'save_post', 'rr_import_external_images_on_save', 10, 3 );
add_action( 'rest_after_insert_post', 'rr_import_external_images_rest', 10, 3 );
add_action( 'rest_after_insert_page', 'rr_import_external_images_rest', 10, 3 );
function rr_import_external_images_rest( $post, $request, $creating ) {
rr_import_external_images_on_save( $post->ID, $post, ! $creating );
}
function rr_import_external_images_on_save( $post_id, $post, $update ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) return;
if ( empty( $post ) || empty( $post->post_content ) ) return;
rr_require_media_libs();
$ptype = get_post_type( $post );
if ( ! post_type_supports( $ptype, 'editor' ) ) return;
$content = $post->post_content;
if ( strlen( $content ) > 5000000 ) return;
$content = apply_filters( 'rr_extimg_pre_process_content', $content, $post_id );
// Get existing map
$map = get_post_meta( $post_id, '_rr_extimg_map', true );
if ( ! is_array( $map ) ) $map = [];
$site_host = wp_parse_url( home_url(), PHP_URL_HOST );
$changed = false;
$image_counter = 1;
// Process Gutenberg image blocks
$content = preg_replace_callback(
'#<!-- wp:image(\s+\{[^}]*\})?\s*-->.*?<!-- /wp:image -->#s',
function( $matches ) use ( &$map, &$image_counter, $post_id, $post, $site_host, &$changed ) {
$block = $matches[0];
// Extract the current URL from JSON or img src
$src = '';
// Try JSON first
if ( preg_match( '#"url"\s*:\s*"([^"]+)"#', $block, $url_match ) ) {
$src = trim( html_entity_decode( stripslashes( $url_match[1] ) ) );
}
// Fallback to img src
if ( empty( $src ) && preg_match( '#<img[^>]+src=(["\'])([^"\']+)\1#i', $block, $src_match ) ) {
$src = trim( html_entity_decode( $src_match[2] ) );
}
if ( empty( $src ) || ! wp_http_validate_url( $src ) ) {
return $block;
}
$host = wp_parse_url( $src, PHP_URL_HOST );
if ( ! $host || strcasecmp( $host, $site_host ) === 0 ) return $block;
// Check if already processed
$attach_id = $map[ $src ] ?? 0;
if ( ! $attach_id ) {
// Check if image already exists in media library
$existing_id = attachment_url_to_postid( $src );
if ( $existing_id ) {
$attach_id = $existing_id;
rr_fill_attachment_texts_if_empty( $existing_id, $src, $post, $image_counter );
} else {
// Download and sideload
$attach_id = rr_sideload_image_as_attachment( $src, $post_id, $post, $image_counter );
if ( is_wp_error( $attach_id ) || ! $attach_id ) {
return $block;
}
}
// Save to map
$map[ $src ] = $attach_id;
}
if ( $attach_id ) {
$new_block = rr_rebuild_image_block( $block, $attach_id );
if ( $new_block !== $block ) {
$changed = true;
$image_counter++;
return $new_block;
}
}
return $block;
},
$content
);
// Process any remaining standalone img tags (classic editor)
$content = preg_replace_callback(
'#<img[^>]+>#i',
function( $matches ) use ( &$map, &$image_counter, $post_id, $post, $site_host, &$changed ) {
$img_tag = $matches[0];
if ( ! preg_match( '#\s+src=(["\'])([^"\']+)\1#i', $img_tag, $src_match ) ) {
return $img_tag;
}
$src = trim( html_entity_decode( $src_match[2] ) );
if ( ! wp_http_validate_url( $src ) ) return $img_tag;
$host = wp_parse_url( $src, PHP_URL_HOST );
if ( ! $host || strcasecmp( $host, $site_host ) === 0 ) return $img_tag;
// Skip if already has wp-image class (already processed)
if ( preg_match( '#wp-image-\d+#', $img_tag ) ) {
return $img_tag;
}
$attach_id = $map[ $src ] ?? 0;
if ( ! $attach_id ) {
$existing_id = attachment_url_to_postid( $src );
if ( $existing_id ) {
$attach_id = $existing_id;
rr_fill_attachment_texts_if_empty( $existing_id, $src, $post, $image_counter );
} else {
$attach_id = rr_sideload_image_as_attachment( $src, $post_id, $post, $image_counter );
if ( is_wp_error( $attach_id ) || ! $attach_id ) {
return $img_tag;
}
}
$map[ $src ] = $attach_id;
}
if ( $attach_id ) {
// Extract existing attributes
$alt = '';
$class = '';
if ( preg_match( '#\s+alt=(["\'])([^"\']*)\1#i', $img_tag, $alt_match ) ) {
$alt = $alt_match[2];
}
if ( preg_match( '#\s+class=(["\'])([^"\']*)\1#i', $img_tag, $class_match ) ) {
$class = $class_match[2];
}
$new_img = rr_generate_wp_image_html( $attach_id, [
'alt' => $alt,
'class' => $class,
] );
if ( $new_img !== $img_tag ) {
$changed = true;
$image_counter++;
return $new_img;
}
}
return $img_tag;
},
$content
);
if ( $changed ) {
remove_action( 'save_post', 'rr_import_external_images_on_save', 10 );
remove_action( 'rest_after_insert_post', 'rr_import_external_images_rest', 10 );
remove_action( 'rest_after_insert_page', 'rr_import_external_images_rest', 10 );
wp_update_post( [ 'ID' => $post_id, 'post_content' => $content ] );
update_post_meta( $post_id, '_rr_extimg_map', $map );
add_action( 'save_post', 'rr_import_external_images_on_save', 10, 3 );
add_action( 'rest_after_insert_post', 'rr_import_external_images_rest', 10, 3 );
add_action( 'rest_after_insert_page', 'rr_import_external_images_rest', 10, 3 );
}
}
/**
* Completely rebuild the image block with proper WordPress structure
*/
function rr_rebuild_image_block( string $old_block, int $attach_id ) : string {
$size = RR_EXTIMG_DEFAULT_SIZE;
// Get image metadata
$meta = wp_get_attachment_metadata( $attach_id );
$full_url = wp_get_attachment_url( $attach_id );
// Get the sized image
$image = wp_get_attachment_image_src( $attach_id, $size );
if ( ! $image ) {
$image = wp_get_attachment_image_src( $attach_id, 'full' );
$size = 'full';
}
if ( ! $image ) return $old_block;
list( $src, $width, $height ) = $image;
// Get alt text (from AI plugin or metadata)
$alt = get_post_meta( $attach_id, '_wp_attachment_image_alt', true );
if ( empty( $alt ) ) {
// Try to extract from old block
if ( preg_match( '#<img[^>]+alt=(["\'])([^"\']*)\1#i', $old_block, $alt_match ) ) {
$alt = $alt_match[2];
}
}
// Extract any existing caption
$caption = '';
if ( preg_match( '#<figcaption[^>]*>(.*?)</figcaption>#s', $old_block, $caption_match ) ) {
$caption = trim( $caption_match[1] );
}
// Extract existing linkDestination if present
$link_destination = 'none';
if ( preg_match( '#"linkDestination"\s*:\s*"([^"]+)"#', $old_block, $link_match ) ) {
$link_destination = $link_match[1];
}
// Extract href if image is linked
$href = '';
if ( preg_match( '#<a[^>]+href=(["\'])([^"\']+)\1#i', $old_block, $href_match ) ) {
$href = $href_match[2];
}
// Build block attributes JSON
$attrs = [
'id' => $attach_id,
'sizeSlug' => $size,
'linkDestination' => $link_destination,
];
// Add width/height if available from metadata
if ( isset( $meta['sizes'][ $size ] ) ) {
$attrs['width'] = (int) $meta['sizes'][ $size ]['width'];
$attrs['height'] = (int) $meta['sizes'][ $size ]['height'];
} elseif ( $size === 'full' && isset( $meta['width'] ) && isset( $meta['height'] ) ) {
$attrs['width'] = (int) $meta['width'];
$attrs['height'] = (int) $meta['height'];
}
$attrs_json = wp_json_encode( $attrs, JSON_UNESCAPED_SLASHES );
// Build srcset and sizes
$srcset = wp_get_attachment_image_srcset( $attach_id, $size );
$sizes_attr = wp_get_attachment_image_sizes( $attach_id, $size );
// Build the img tag - DO NOT include srcset/sizes
// Gutenberg will add them automatically on the frontend
$img_tag = sprintf(
'<img src="%s" alt="%s" class="wp-image-%d"/>',
esc_url( $src ),
esc_attr( $alt ),
$attach_id
);
// Wrap in link if needed
if ( ! empty( $href ) ) {
$img_tag = sprintf( '<a href="%s">%s</a>', esc_url( $href ), $img_tag );
}
// Build figure
$figure_classes = 'wp-block-image size-' . $size;
$figure = sprintf( '<figure class="%s">%s', esc_attr( $figure_classes ), $img_tag );
if ( ! empty( $caption ) ) {
$figure .= '<figcaption class="wp-element-caption">' . $caption . '</figcaption>';
}
$figure .= '</figure>';
// Build complete block
$new_block = sprintf(
"<!-- wp:image %s -->\n%s\n<!-- /wp:image -->",
$attrs_json,
$figure
);
return $new_block;
}
/**
* Generate proper WordPress image HTML with all required attributes
* For classic editor content (not Gutenberg blocks)
*/
function rr_generate_wp_image_html( int $attach_id, array $original_data ) : string {
$size = RR_EXTIMG_DEFAULT_SIZE;
$image = wp_get_attachment_image_src( $attach_id, $size );
if ( ! $image ) {
$image = wp_get_attachment_image_src( $attach_id, 'full' );
}
if ( ! $image ) return '';
list( $src, $width, $height ) = $image;
$alt = get_post_meta( $attach_id, '_wp_attachment_image_alt', true );
if ( empty( $alt ) && ! empty( $original_data['alt'] ) ) {
$alt = $original_data['alt'];
}
$classes = [ "wp-image-{$attach_id}" ];
if ( ! empty( $original_data['class'] ) ) {
$existing_classes = explode( ' ', $original_data['class'] );
foreach ( $existing_classes as $cls ) {
$cls = trim( $cls );
if ( $cls && ! preg_match( '/^wp-image-\d+$/', $cls ) ) {
$classes[] = $cls;
}
}
}
$class_attr = implode( ' ', array_unique( $classes ) );
// For classic editor, we CAN include srcset since it's not validated by Gutenberg
$srcset = wp_get_attachment_image_srcset( $attach_id, $size );
$sizes_attr = wp_get_attachment_image_sizes( $attach_id, $size );
$img_html = sprintf(
'<img src="%s" alt="%s" class="%s"',
esc_url( $src ),
esc_attr( $alt ),
esc_attr( $class_attr )
);
if ( $srcset ) {
$img_html .= sprintf( ' srcset="%s"', esc_attr( $srcset ) );
}
if ( $sizes_attr ) {
$img_html .= sprintf( ' sizes="%s"', esc_attr( $sizes_attr ) );
}
$img_html .= '/>';
return $img_html;
}
function rr_sideload_image_as_attachment( string $url, int $post_id, $post, int $counter ) {
if ( strpos( $url, 'googleusercontent.com' ) !== false ||
strpos( $url, 'docs.google.com' ) !== false ) {
$url = rr_resolve_redirect( $url );
}
$path_parts = pathinfo( wp_parse_url( $url, PHP_URL_PATH ) );
$ext = strtolower( $path_parts['extension'] ?? '' );
$allowed = [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp' ];
if ( ! in_array( $ext, $allowed, true ) ) {
return new WP_Error( 'invalid_image', 'Unsupported image format: ' . $ext );
}
$post_title = $post->post_title;
if ( empty( $post_title ) ) {
$post_title = 'image';
}
$custom_filename = rr_generate_filename( $post_title, $counter, $ext );
$temp_file = download_url( $url );
if ( is_wp_error( $temp_file ) ) {
return $temp_file;
}
$file_array = [
'name' => $custom_filename,
'tmp_name' => $temp_file,
];
$id = media_handle_sideload( $file_array, $post_id );
if ( file_exists( $temp_file ) ) {
@unlink( $temp_file );
}
if ( is_wp_error( $id ) ) {
return $id;
}
$path = get_attached_file( $id );
if ( $path ) {
$meta = wp_generate_attachment_metadata( $id, $path );
if ( $meta ) wp_update_attachment_metadata( $id, $meta );
}
update_post_meta( $id, '_rr_source_url', esc_url_raw( $url ) );
// Only fill texts if configured - allows AI plugins to run first
rr_fill_attachment_texts_if_empty( $id, $url, $post, $counter );
return (int) $id;
}
function rr_generate_filename( string $post_title, int $counter, string $ext ) : string {
$slug = sanitize_title( $post_title );
$slug = preg_replace( '/[^a-z0-9\-]/', '', $slug );
$slug = substr( $slug, 0, 50 );
if ( $counter > 1 ) {
$filename = $slug . '-' . $counter . '.' . $ext;
} else {
$filename = $slug . '.' . $ext;
}
return $filename;
}
function rr_resolve_redirect( string $url ) : string {
$response = wp_safe_remote_head( $url, [
'timeout' => 10,
'redirection' => 5,
] );
if ( is_wp_error( $response ) ) {
return $url;
}
$final_url = wp_remote_retrieve_header( $response, 'location' );
return $final_url ? $final_url : $url;
}
/**
* Fill attachment metadata only if enabled and field is empty
* This allows AI plugins and other metadata plugins to run first
*/
function rr_fill_attachment_texts_if_empty( int $attach_id, string $fallback_url = '', $post = null, int $counter = 1 ) : void {
if ( ! RR_EXTIMG_SET_TITLE && ! RR_EXTIMG_SET_ALT && ! RR_EXTIMG_SET_CAPTION && ! RR_EXTIMG_SET_DESCRIPTION ) {
return;
}
$label = '';
if ( $post && ! empty( $post->post_title ) ) {
$label = $post->post_title;
if ( $counter > 1 ) {
$label .= ' ' . $counter;
}
} else {
$path = get_attached_file( $attach_id );
$base = '';
if ( $path && file_exists( $path ) ) {
$pi = pathinfo( $path );
$base = $pi['filename'] ?? '';
}
if ( ! $base && $fallback_url ) {
$pi = pathinfo( wp_parse_url( $fallback_url, PHP_URL_PATH ) ?? '' );
$base = $pi['filename'] ?? '';
}
$label = rr_humanize_filename( $base );
}
if ( $label === '' ) return;
if ( RR_EXTIMG_SET_TITLE ) {
$att = get_post( $attach_id );
if ( $att && empty( trim( $att->post_title ) ) ) {
wp_update_post( [
'ID' => $attach_id,
'post_title' => $label,
] );
}
}
if ( RR_EXTIMG_SET_ALT ) {
$alt = get_post_meta( $attach_id, '_wp_attachment_image_alt', true );
if ( empty( $alt ) ) {
update_post_meta( $attach_id, '_wp_attachment_image_alt', $label );
}
}
if ( RR_EXTIMG_SET_CAPTION ) {
$cur_caption = get_post_field( 'post_excerpt', $attach_id );
if ( empty( trim( $cur_caption ) ) ) {
wp_update_post( [
'ID' => $attach_id,
'post_excerpt' => $label,
] );
}
}
if ( RR_EXTIMG_SET_DESCRIPTION ) {
$cur_desc = get_post_field( 'post_content', $attach_id );
if ( empty( trim( $cur_desc ) ) ) {
wp_update_post( [
'ID' => $attach_id,
'post_content' => $label,
] );
}
}
}
function rr_humanize_filename( string $name ) : string {
$name = urldecode( $name );
$name = preg_replace( '/[_\-]+/u', ' ', $name );
$name = preg_replace( '/\s+/u', ' ', $name );
$name = trim( $name );
if ( $name === '' ) return '';
if ( function_exists( 'mb_convert_case' ) ) {
$name = mb_convert_case( $name, MB_CASE_TITLE, 'UTF-8' );
} else {
$name = ucwords( strtolower( $name ) );
}
return sanitize_text_field( $name );
}
function rr_require_media_libs() : void {
if ( ! function_exists( 'download_url' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
require_once ABSPATH . 'wp-admin/includes/image.php';
}
if ( ! function_exists( 'media_sideload_image' ) ) {
require_once ABSPATH . 'wp-admin/includes/media.php';
}
}