WordPress External Image Import Plugin Snippet

Import external images to wp media library when pasted in the WP editor.

Introduction

The RR External Image Import Plugin automatically downloads external images from your post content and imports them into your WordPress Media Library. When you paste content from Google Docs, Microsoft Word, or any external source, this plugin detects remote image URLs and replaces them with local copies.

The Problem It Solves

When you copy content from external sources:

  • Images remain hosted on external servers
  • Broken links if source removes images
  • Slower page load times
  • No control over image optimization
  • SEO penalties for external resources
  • Generic filenames like “image.png”

The Solution

This plugin:

  • Automatically downloads all external images
  • Stores them in your Media Library
  • Renames files based on post title
  • Updates all image URLs in content
  • Preserves original file formats (JPG, PNG, etc.)
  • Sets SEO-friendly metadata
  • Works with Block Editor, Classic Editor, and page builders

Key Features

Automatic Image Detection

The plugin scans your post content for:

  • Standard HTML <img> tags
  • Gutenberg block image URLs
  • External image URLs from any domain
  • Multiple image formats (JPG, PNG, GIF, WebP, SVG, BMP)

Smart Filename Generation

Images are renamed using your post title with sequential numbering:

Post Title: “Best Chocolate Cake Recipe”

Generated Filenames:

best-chocolate-cake-recipe.jpg
best-chocolate-cake-recipe-2.png
best-chocolate-cake-recipe-3.jpg
best-chocolate-cake-recipe-4.gif
best-chocolate-cake-recipe-5.webp

Format Preservation

The plugin automatically detects and preserves the original image format:

  • .jpg remains .jpg
  • .png remains .png
  • .gif remains .gif
  • .webp remains .webp

Configurable Metadata

Control which metadata fields are auto-populated:

  • Image Title – WordPress Media Library title
  • Alt Text – For SEO and accessibility
  • Caption – Displayed below images
  • Description – Media Library description

Duplicate Prevention

The plugin maintains a mapping of imported images to prevent:

  • Re-downloading the same image
  • Duplicate files in Media Library
  • Unnecessary processing on every save

Performance Optimized

Built-in safeguards:

  • Maximum 50 images per save
  • 5MB content size limit
  • Timeout protection
  • Memory-efficient processing

Configuration

Basic Configuration

Edit the constants at the top of the plugin file:

/**
 * CONFIGURATION OPTIONS
 */
define( 'RR_EXTIMG_SET_TITLE',       true );  // Image Title
define( 'RR_EXTIMG_SET_ALT',         true );  // Alt Text
define( 'RR_EXTIMG_SET_CAPTION',     true );  // Caption
define( 'RR_EXTIMG_SET_DESCRIPTION', true );  // Description

Configuration Presets

Preset 1: Full Metadata (Default)

define( 'RR_EXTIMG_SET_TITLE',       true );
define( 'RR_EXTIMG_SET_ALT',         true );
define( 'RR_EXTIMG_SET_CAPTION',     true );
define( 'RR_EXTIMG_SET_DESCRIPTION', true );

Best for: New sites, complete SEO control


Preset 2: SEO Plugin Compatibility

define( 'RR_EXTIMG_SET_TITLE',       true );
define( 'RR_EXTIMG_SET_ALT',         false );  // Let Rank Math/Yoast handle
define( 'RR_EXTIMG_SET_CAPTION',     false );
define( 'RR_EXTIMG_SET_DESCRIPTION', false );

Best for: Sites using Rank Math, Yoast, or SEOPress


Preset 3: Accessibility Focus

define( 'RR_EXTIMG_SET_TITLE',       false );
define( 'RR_EXTIMG_SET_ALT',         true );  // Critical for screen readers
define( 'RR_EXTIMG_SET_CAPTION',     false );
define( 'RR_EXTIMG_SET_DESCRIPTION', false );

Best for: Accessibility-focused sites


Preset 4: Import Only

define( 'RR_EXTIMG_SET_TITLE',       false );
define( 'RR_EXTIMG_SET_ALT',         false );
define( 'RR_EXTIMG_SET_CAPTION',     false );
define( 'RR_EXTIMG_SET_DESCRIPTION', false );

Best for: Sites with existing metadata management


How It Works

Step-by-Step Process

1. Content Save Trigger

When you save/publish a post, the plugin hooks into:

  • save_post – Classic editor saves
  • rest_after_insert_post – Block editor saves
  • rest_after_insert_page – Page saves

2. Image Detection

The plugin scans content using regex patterns:

// Detects: <img src="https://example.com/image.jpg">
preg_match_all( '#<img\b[^>]*\K\s+src=("|\')([^"\']+)\1#i', $html, $matches );

// Detects: Gutenberg blocks with "url":"https://..."
preg_match_all( '#"url"\s*:\s*"([^"]+)"#i', $html, $matches );

3. External URL Validation

For each detected image:

// Check if URL is valid
if ( ! wp_http_validate_url( $src ) ) continue;

// Check if URL is external (not your domain)
$host = wp_parse_url( $src, PHP_URL_HOST );
if ( strcasecmp( $host, $site_host ) === 0 ) continue;

4. Duplicate Check

Before downloading:

// Check if already imported (stored in post meta)
$map = get_post_meta( $post_id, '_rr_extimg_map', true );
if ( isset( $map[ $src ] ) ) {
    // Use existing local URL
    $new_url = $map[ $src ];
}

5. Filename Generation

Create SEO-friendly filename:

$post_title = "Best Chocolate Cake Recipe";
$counter = 2;
$ext = "jpg";

// Result: best-chocolate-cake-recipe-2.jpg
$filename = sanitize_title( $post_title ) . '-' . $counter . '.' . $ext;

6. Image Download

// Download to temporary location
$temp_file = download_url( $url );

// Import to Media Library with custom filename
$file_array = [
    'name'     => 'best-chocolate-cake-recipe-2.jpg',
    'tmp_name' => $temp_file,
];
$attach_id = media_handle_sideload( $file_array, $post_id );

7. Metadata Population

Based on configuration:

if ( RR_EXTIMG_SET_TITLE ) {
    wp_update_post([
        'ID'         => $attach_id,
        'post_title' => 'Best Chocolate Cake Recipe 2',
    ]);
}

if ( RR_EXTIMG_SET_ALT ) {
    update_post_meta( $attach_id, '_wp_attachment_image_alt', 
        'Best Chocolate Cake Recipe 2' );
}

8. Content Update

Replace external URLs with local ones:

// Before: <img src="https://external.com/image.jpg">
// After:  <img src="https://yoursite.com/wp-content/uploads/2024/01/best-chocolate-cake-recipe-2.jpg">

$content = str_replace( 
    ' src="https://external.com/image.jpg"',
    ' src="https://yoursite.com/wp-content/uploads/2024/01/best-chocolate-cake-recipe-2.jpg"',
    $content 
);

wp_update_post([ 'ID' => $post_id, 'post_content' => $content ]);

9. Mapping Storage

Save URL mapping for future reference:

update_post_meta( $post_id, '_rr_extimg_map', [
    'https://external.com/image.jpg' => 'https://yoursite.com/.../best-chocolate-cake-recipe-2.jpg'
]);

Technical Architecture

Core Functions

rr_import_external_images_on_save()

Purpose: Main orchestration function
Hooks: save_postrest_after_insert_post
Parameters:

  • $post_id – Post ID being saved
  • $post – Full post object
  • $update – Whether this is an update or new post

Process Flow:

1. Validate post (not autosave, not revision)
2. Extract content
3. Detect external images
4. Loop through images
5. Download and import
6. Update content
7. Save mapping

rr_extract_img_srcs()

Purpose: Extract image URLs from HTML content
Returns: Array of [ 'raw_attribute' => 'url' ]

Regex Patterns:

// Pattern 1: Standard <img> tags
#<img\b[^>]*\K\s+src=("|\')([^"\']+)\1#i

// Pattern 2: Gutenberg blocks
#"url"\s*:\s*"([^"]+)"#i

Example Output:

[
    ' src="https://example.com/photo.jpg"' => 'https://example.com/photo.jpg',
    ' src="https://cdn.site.com/img.png"'  => 'https://cdn.site.com/img.png',
]

rr_sideload_image_as_attachment()

Purpose: Download and import image to Media Library
Parameters:

  • $url – External image URL
  • $post_id – Parent post ID
  • $post – Post object (for title)
  • $counter – Image sequence number

Special Handling:

// Google Docs redirect resolution
if ( strpos( $url, 'googleusercontent.com' ) !== false ) {
    $url = rr_resolve_redirect( $url );
}

// Extension validation
$allowed = [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp' ];
if ( ! in_array( $ext, $allowed ) ) {
    return new WP_Error( 'invalid_image', 'Unsupported format' );
}

rr_generate_filename()

Purpose: Create SEO-friendly filename from post title
Algorithm:

1. Convert post title to slug: "Best Recipe""best-recipe"
2. Remove special characters: Keep only a-z, 0-9, hyphens
3. Limit length: Maximum 50 characters
4. Add counter: "best-recipe-2"
5. Add extension: "best-recipe-2.jpg"

Examples:

rr_generate_filename( "How to Bake Cake", 1, "jpg" )
// → "how-to-bake-cake.jpg"

rr_generate_filename( "10 Tips & Tricks!", 3, "png" )
// → "10-tips-tricks-3.png"

rr_generate_filename( "Café Menu — 2024 Edition", 5, "webp" )
// → "cafe-menu-2024-edition-5.webp"

rr_fill_attachment_texts_if_empty()

Purpose: Populate image metadata based on configuration
Checks: Only fills empty fields (won’t overwrite existing data)

Metadata Fields:

// Title (post_title)
wp_update_post([ 'ID' => $attach_id, 'post_title' => $label ]);

// Alt Text (_wp_attachment_image_alt)
update_post_meta( $attach_id, '_wp_attachment_image_alt', $label );

// Caption (post_excerpt)
wp_update_post([ 'ID' => $attach_id, 'post_excerpt' => $label ]);

// Description (post_content)
wp_update_post([ 'ID' => $attach_id, 'post_content' => $label ]);

rr_resolve_redirect()

Purpose: Follow redirects for Google Docs/Drive URLs
Why Needed: Google often serves images through redirect URLs

// Before: https://docs.google.com/redirect?url=...
// After:  https://lh3.googleusercontent.com/actual-image.jpg

$response = wp_safe_remote_head( $url, [
    'timeout'     => 10,
    'redirection' => 5,  // Follow up to 5 redirects
]);

Data Storage

Post Meta: _rr_extimg_map

Type: Array
Structure:

[
    'https://external.com/image1.jpg' => 'https://yoursite.com/wp-content/uploads/2024/01/post-title.jpg',
    'https://cdn.site.com/photo.png'  => 'https://yoursite.com/wp-content/uploads/2024/01/post-title-2.png',
]

Purpose:

  • Prevent re-downloading the same image
  • Track import history
  • Enable future migrations

Attachment Meta: _rr_source_url

Type: String
Example: https://external.com/original-image.jpg

Purpose:

  • Track original source
  • Debugging
  • Audit trail

Security Measures

1. URL Validation

// WordPress core function validates URL structure
if ( ! wp_http_validate_url( $src ) ) continue;

2. File Type Whitelist

$allowed = [ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp' ];
if ( ! in_array( $ext, $allowed, true ) ) {
    return new WP_Error( 'invalid_image', 'Unsupported format' );
}

3. Sanitization

// URL sanitization
esc_url_raw( $url );

// Filename sanitization
sanitize_title( $post_title );

// Text field sanitization
sanitize_text_field( $label );

4. Timeout Protection

wp_safe_remote_head( $url, [
    'timeout'     => 10,  // 10 second max
    'redirection' => 5,   // Max 5 redirects
]);

Compatibility

WordPress Editors

EditorCompatibilityNotes
Block Editor (Gutenberg)Full SupportDetects block image URLs
Classic EditorFull SupportStandard <img> tag detection
Code EditorFull SupportWorks with raw HTML

Page Builders

Page BuilderCompatibilityNotes
ElementorFull SupportWorks with Elementor content
Divi BuilderFull SupportDetects Divi image modules
WPBakeryFull SupportCompatible with shortcodes
Beaver BuilderFull SupportWorks with BB content
Oxygen BuilderFull SupportSupports Oxygen structure

SEO Plugins

PluginRecommended ConfigNotes
Rank MathDisable ALTLet Rank Math auto-generate
Yoast SEODisable ALTYoast handles image SEO
SEOPressDisable ALTSEOPress optimization
All in One SEOKeep enabledWorks alongside

Configuration for SEO Plugins:

define( 'RR_EXTIMG_SET_TITLE',       true );
define( 'RR_EXTIMG_SET_ALT',         false );  // Let SEO plugin handle
define( 'RR_EXTIMG_SET_CAPTION',     false );
define( 'RR_EXTIMG_SET_DESCRIPTION', false );

Image Optimization Plugins

PluginCompatibilityNotes
ShortPixelFull SupportAuto-optimizes imported images
SmushFull SupportProcesses on import
ImagifyFull SupportAutomatic optimization
EWWW Image OptimizerFull SupportWorks seamlessly

Content Sources

SourceCompatibilitySpecial Handling
Google DocsFull SupportRedirect resolution
Microsoft WordFull SupportStandard import
NotionFull SupportExternal URL detection
MediumFull SupportCDN URL handling
DropboxFull SupportDirect link support
Google DriveFull SupportRedirect resolution

Use Cases

Use Case 1: Content Migration

Scenario: Migrating 100 blog posts from Medium to WordPress

Before:

<img src="https://cdn-images-1.medium.com/max/1600/1*abc123.jpeg">
<img src="https://cdn-images-1.medium.com/max/1600/1*def456.jpeg">

After Plugin Activation:

<img src="https://yoursite.com/wp-content/uploads/2024/01/migrated-post-title.jpeg">
<img src="https://yoursite.com/wp-content/uploads/2024/01/migrated-post-title-2.jpeg">

Benefits:

  • All images are now hosted locally
  • No broken links if Medium changes URLs
  • Faster page loads (your CDN)
  • SEO-friendly filenames

Use Case 2: Team Content Creation

Scenario: Writers create content in Google Docs, then paste it into WordPress

Workflow:

  1. Writer creates article in Google Docs with images
  2. Writer copies entire content (Ctrl+A, Ctrl+C)
  3. Writer pastes into WordPress Block Editor
  4. Writer clicks “Publish”
  5. Plugin automatically:
    • Downloads all Google-hosted images
    • Renames them based on article title
    • Updates all image references
    • Sets ALT text for SEO

Time Saved: Approximately 5 minutes per article (no manual image downloads)


Use Case 3: Guest Post Management

Scenario: Accepting guest posts with external images

Problem:

  • Guest authors use Dropbox/Google Drive links
  • Links expire after 30 days
  • Images break, posts look unprofessional

Solution:

// Plugin automatically imports all external images on publish
// No manual intervention needed

Result:

  • All images permanently hosted
  • Consistent filename structure
  • No broken links ever

Use Case 4: E-commerce Product Imports

Scenario: Importing product descriptions from suppliers

Before:

Product: "Premium Coffee Maker"
<img src="https://supplier-cdn.com/products/temp/image1.jpg">
<img src="https://supplier-cdn.com/products/temp/image2.jpg">

After:

Product: "Premium Coffee Maker"
<img src="https://yourstore.com/wp-content/uploads/2024/01/premium-coffee-maker.jpg">
<img src="https://yourstore.com/wp-content/uploads/2024/01/premium-coffee-maker-2.jpg">

SEO Impact:

  • Keyword-rich filenames
  • Proper ALT text
  • Faster image loading
  • Better Google Image Search ranking

Troubleshooting

Issue 1: Images Not Importing

Symptoms:

  • External images remain external after saving
  • No new files in Media Library

Diagnosis Checklist:

// 1. Check if post type supports editor
$ptype = get_post_type( $post );
if ( ! post_type_supports( $ptype, 'editor' ) ) {
    // Plugin won't run
}

// 2. Verify URL is external
$site_host = wp_parse_url( home_url(), PHP_URL_HOST );
$img_host = wp_parse_url( $img_url, PHP_URL_HOST );
// Must be different domains

// 3. Check file extension
$ext = pathinfo( $img_url, PATHINFO_EXTENSION );
// Must be: jpg, jpeg, png, gif, webp, svg, ico, bmp

Solutions:

A. Enable Debug Logging:

// Add to plugin after line 25
add_action( 'save_post', function( $post_id ) {
    error_log( 'RR Plugin: Processing post ' . $post_id );
}, 9 );

B. Check File Permissions:

# wp-content/uploads must be writable
chmod 755 wp-content/uploads

C. Increase PHP Limits:

// In wp-config.php
define( 'WP_MEMORY_LIMIT', '256M' );
ini_set( 'max_execution_time', '300' );

Issue 2: Duplicate Images

Symptoms:

  • Same image imported multiple times
  • Media Library cluttered with duplicates

Cause:

// Mapping not being saved properly
$map = get_post_meta( $post_id, '_rr_extimg_map', true );
// Returns empty array

Solution:

// Verify post meta is being saved
add_action( 'updated_post_meta', function( $meta_id, $post_id, $meta_key ) {
    if ( $meta_key === '_rr_extimg_map' ) {
        error_log( 'Mapping saved for post ' . $post_id );
    }
}, 10, 3 );

Manual Fix:

// Delete mapping to force fresh import
delete_post_meta( $post_id, '_rr_extimg_map' );

Issue 3: Google Docs Images Not Loading

Symptoms:

  • Google Docs images show as broken
  • Error: “Invalid image format”

Cause: Google uses redirect URLs that expire or require authentication

Solution Already Built-In:

function rr_resolve_redirect( string $url ) : string {
    // Plugin automatically follows redirects
    $response = wp_safe_remote_head( $url, [
        'timeout'     => 10,
        'redirection' => 5,
    ]);

    $final_url = wp_remote_retrieve_header( $response, 'location' );
    return $final_url ? $final_url : $url;
}

Additional Fix:

// For Google Drive shared links, ensure they're set to "Anyone with link"
// Change: https://drive.google.com/file/d/ABC123/view
// To:     https://drive.google.com/uc?export=view&id=ABC123

Issue 4: Slow Save Times

Symptoms:

  • Post takes 30+ seconds to save
  • Timeout errors

Diagnosis:

// Check how many images are being processed
add_action( 'save_post', function( $post_id ) {
    $content = get_post_field( 'post_content', $post_id );
    preg_match_all( '#<img[^>]+src=(["\'])([^"\']+)\1#i', $content, $m );
    error_log( 'Images found: ' . count( $m[2] ) );
}, 9 );

Solutions:

A. Reduce Image Limit:

// In plugin, change line ~68
if ( count( $attrs ) > 50 ) {
    $attrs = array_slice( $attrs, 0, 20, true );  // Reduce to 20
}

B. Increase Timeouts:

// In wp-config.php
set_time_limit( 300 );  // 5 minutes

C. Process in Background:

// Use WP Cron for large imports
add_action( 'save_post', function( $post_id ) {
    wp_schedule_single_event( time() + 10, 'rr_process_images', [ $post_id ] );
});

Issue 5: Filename Collisions

Symptoms:

  • Images renamed to post-title-1.jpgpost-title-1-2.jpg
  • WordPress auto-increment creates messy names

Cause: Multiple posts with same title importing images simultaneously

Solution:

// Add timestamp to filename generation
function rr_generate_filename( string $post_title, int $counter, string $ext ) : string {
    $slug = sanitize_title( $post_title );
    $slug = substr( $slug, 0, 40 );  // Leave room for timestamp

    $timestamp = date( 'YmdHis' );

    if ( $counter > 1 ) {
        $filename = $slug . '-' . $timestamp . '-' . $counter . '.' . $ext;
    } else {
        $filename = $slug . '-' . $timestamp . '.' . $ext;
    }

    return $filename;
}

// Result: best-recipe-20240115143022.jpg

Developer Hooks

Filter: rr_extimg_pre_process_content

Purpose: Modify content before image extraction

Parameters:

  • $content (string) – Post content HTML
  • $post_id (int) – Post ID

Example: Skip Processing for Specific Categories

add_filter( 'rr_extimg_pre_process_content', function( $content, $post_id ) {
    // Don't process posts in "External Content" category
    if ( has_category( 'external-content', $post_id ) ) {
        return ''; // Return empty to skip
    }
    return $content;
}, 10, 2 );

Example: Pre-process Shortcodes

add_filter( 'rr_extimg_pre_process_content', function( $content, $post_id ) {
    // Expand shortcodes before image detection
    return do_shortcode( $content );
}, 10, 2 );

Filter: rr_extimg_metadata_config

Purpose: Programmatically control metadata settings

Parameters:

  • $config (array) – Configuration array
  • $attach_id (int) – Attachment ID
  • $post (object) – Parent post object

Example: Disable ALT for Specific Post Types

add_filter( 'rr_extimg_metadata_config', function( $config, $attach_id, $post ) {
    if ( $post->post_type === 'product' ) {
        $config['alt'] = false;  // Let WooCommerce handle
    }
    return $config;
}, 10, 3 );

Example: Role-Based Configuration

add_filter( 'rr_extimg_metadata_config', function( $config, $attach_id, $post ) {
    $author = get_userdata( $post->post_author );

    if ( in_array( 'contributor', $author->roles ) ) {
        // Contributors: only set title
        $config['alt']         = false;
        $config['caption']     = false;
        $config['description'] = false;
    }

    return $config;
}, 10, 3 );

Action: rr_after_image_import

Custom Hook (Add This to Plugin):

// Add after line 142 in rr_sideload_image_as_attachment()
do_action( 'rr_after_image_import', $id, $url, $post_id, $counter );

Example: Set Featured Image

add_action( 'rr_after_image_import', function( $attach_id, $source_url, $post_id, $counter ) {
    if ( $counter === 1 ) {
        // Set first image as featured
        set_post_thumbnail( $post_id, $attach_id );
    }

    // Log import
    error_log( "Imported: {$source_url} → " . wp_get_attachment_url( $attach_id ) );
}, 10, 4 );

Filter: rr_extimg_custom_filename

Custom Hook (Add This to Plugin):

// In rr_generate_filename(), add before return:
$filename = apply_filters( 'rr_extimg_custom_filename', $filename, $post_title, $counter, $ext );

Example: Add Date to Filename

add_filter( 'rr_extimg_custom_filename', function( $filename, $post_title, $counter, $ext ) {
    $date = date( 'Y-m-d' );
    $slug = sanitize_title( $post_title );

    return "{$slug}-{$date}-{$counter}.{$ext}";
    // Result: best-recipe-2024-01-15-2.jpg
}, 10, 4 );

Example: Add Post ID

add_filter( 'rr_extimg_custom_filename', function( $filename, $post_title, $counter, $ext ) {
    global $post;
    $slug = sanitize_title( $post_title );

    return "{$slug}-{$post->ID}-{$counter}.{$ext}";
    // Result: best-recipe-12345-2.jpg
}, 10, 4 );

Performance Considerations

Memory Usage

Default Limits:

  • WordPress default: 40MB
  • Recommended for plugin: 128MB minimum
  • Heavy usage: 256MB

Configuration:

// wp-config.php
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );

Execution Time

Typical Processing Times:

  • 1 image: 2-3 seconds
  • 5 images: 10-15 seconds
  • 10 images: 20-30 seconds
  • 50 images: 2-3 minutes

Timeout Configuration:

// wp-config.php
set_time_limit( 300 );  // 5 minutes
ini_set( 'max_execution_time', '300' );

Database Impact

Queries Per Image:

  • 1x get_post_meta (check mapping)

Code Sample

Installation

Method 1: Manual Installation

  1. Copy the plugin code into a new file: wp-content/plugins/rr-import-external-images/rr-import-external-images.php
  2. Activate the plugin:
    • Go to WordPress Admin → Plugins
    • Find “RR — Import External Images on Save”
    • Click Activate

Method 2: Use a Code Snippet plugin like WPCodeBox or FluentSnippets.

Copy the snippet below and paste it in your favorite code snippet plugin.

PHP
<?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: 1.4.0
 */

if ( ! defined( 'ABSPATH' ) ) { exit; }

/**
 * ============================================
 * CONFIGURATION OPTIONS
 * ============================================
 * Set to false to disable auto-filling
 */
define( 'RR_EXTIMG_SET_TITLE',       false );  // Image Title
define( 'RR_EXTIMG_SET_ALT',         false );  // Alt Text
define( 'RR_EXTIMG_SET_CAPTION',     false );  // Caption (Excerpt)
define( 'RR_EXTIMG_SET_DESCRIPTION', false );  // Description (Content)

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;
    
    // Performance check
    if ( strlen( $content ) > 5000000 ) return;
    
    // Allow filtering
    $content = apply_filters( 'rr_extimg_pre_process_content', $content, $post_id );
    
    $attrs = rr_extract_img_srcs( $content );
    if ( empty( $attrs ) ) return;
    
    // Limit processing
    if ( count( $attrs ) > 50 ) {
        $attrs = array_slice( $attrs, 0, 50, true );
    }

    $site_host = wp_parse_url( home_url(), PHP_URL_HOST );
    $map       = get_post_meta( $post_id, '_rr_extimg_map', true );
    if ( ! is_array( $map ) ) $map = [];

    $changed = false;
    $image_counter = 1;

    foreach ( $attrs as $raw => $src ) {
        if ( ! wp_http_validate_url( $src ) ) continue;

        $host = wp_parse_url( $src, PHP_URL_HOST );
        if ( ! $host || strcasecmp( $host, $site_host ) === 0 ) continue;

        $new_url = $map[ $src ] ?? '';

        if ( empty( $new_url ) ) {
            $existing_id = attachment_url_to_postid( $src );
            if ( $existing_id ) {
                rr_fill_attachment_texts_if_empty( $existing_id, $src, $post, $image_counter );
                $new_url = wp_get_attachment_url( $existing_id );
            } else {
                $attach_id = rr_sideload_image_as_attachment( $src, $post_id, $post, $image_counter );
                if ( is_wp_error( $attach_id ) || ! $attach_id ) continue;
                $new_url = wp_get_attachment_url( $attach_id );
            }
            if ( $new_url ) $map[ $src ] = $new_url;
        }

        if ( $new_url && $new_url !== $src ) {
            $content = str_replace( $raw, ' src="' . esc_url( $new_url ) . '"', $content );
            $changed = true;
        }
        
        $image_counter++;
    }

    if ( $changed ) {
        remove_action( 'save_post', 'rr_import_external_images_on_save', 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 );
    }
}

function rr_extract_img_srcs( string $html ) : array {
    $results = [];
    
    // Standard <img> tags
    if ( preg_match_all( '#<img\b[^>]*\K\s+src=("|\')([^"\']+)\1#i', $html, $m, PREG_SET_ORDER ) ) {
        foreach ( $m as $match ) {
            $results[ $match[0] ] = trim( html_entity_decode( $match[2] ) );
        }
    }
    
    // Gutenberg block image URLs
    if ( preg_match_all( '#"url"\s*:\s*"([^"]+)"#i', $html, $m ) ) {
        foreach ( $m[1] as $url ) {
            $decoded = trim( html_entity_decode( stripslashes( $url ) ) );
            if ( wp_http_validate_url( $decoded ) ) {
                $key = ' src="' . $decoded . '"';
                if ( ! isset( $results[ $key ] ) ) {
                    $results[ $key ] = $decoded;
                }
            }
        }
    }
    
    return $results;
}

function rr_sideload_image_as_attachment( string $url, int $post_id, $post, int $counter ) {
    // Handle Google Docs/Drive URLs
    if ( strpos( $url, 'googleusercontent.com' ) !== false || 
         strpos( $url, 'docs.google.com' ) !== false ) {
        $url = rr_resolve_redirect( $url );
    }
    
    // Get file extension from 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 );
    }
    
    // Generate custom filename from post title
    $post_title = $post->post_title;
    if ( empty( $post_title ) ) {
        $post_title = 'image';
    }
    
    $custom_filename = rr_generate_filename( $post_title, $counter, $ext );
    
    // Download to temp file
    $temp_file = download_url( $url );
    if ( is_wp_error( $temp_file ) ) {
        return $temp_file;
    }
    
    // Prepare file array
    $file_array = [
        'name'     => $custom_filename,
        'tmp_name' => $temp_file,
    ];
    
    // Import the file
    $id = media_handle_sideload( $file_array, $post_id );
    
    // Clean up temp file
    if ( file_exists( $temp_file ) ) {
        @unlink( $temp_file );
    }
    
    if ( is_wp_error( $id ) ) {
        return $id;
    }

    // Generate image sizes
    $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 ) );
    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 based on configuration constants
 */
function rr_fill_attachment_texts_if_empty( int $attach_id, string $fallback_url = '', $post = null, int $counter = 1 ) : void {
    // Check if any field is enabled
    if ( ! RR_EXTIMG_SET_TITLE && ! RR_EXTIMG_SET_ALT && ! RR_EXTIMG_SET_CAPTION && ! RR_EXTIMG_SET_DESCRIPTION ) {
        return; // All disabled, skip processing
    }
    
    // Generate label from post title
    $label = '';
    
    if ( $post && ! empty( $post->post_title ) ) {
        $label = $post->post_title;
        if ( $counter > 1 ) {
            $label .= ' ' . $counter;
        }
    } else {
        // Fallback to filename
        $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;

    // Title - Only if enabled
    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,
            ] );
        }
    }

    // Alt text - Only if enabled
    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 );
        }
    }

    // Caption - Only if enabled
    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,
            ] );
        }
    }
    
    // Description - Only if enabled
    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';
    }
}

Leave the first comment


  • Custom Plugin
  • WordPress
  • WP Admin

Content on this page

This website uses cookies to enhance your browsing experience and ensure the site functions properly. By continuing to use this site, you acknowledge and accept our use of cookies.

Accept All Accept Required Only