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:
.jpgremains.jpg.pngremains.png.gifremains.gif.webpremains.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 savesrest_after_insert_post– Block editor savesrest_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_post, rest_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
| Editor | Compatibility | Notes |
|---|---|---|
| Block Editor (Gutenberg) | Full Support | Detects block image URLs |
| Classic Editor | Full Support | Standard <img> tag detection |
| Code Editor | Full Support | Works with raw HTML |
Page Builders
| Page Builder | Compatibility | Notes |
|---|---|---|
| Elementor | Full Support | Works with Elementor content |
| Divi Builder | Full Support | Detects Divi image modules |
| WPBakery | Full Support | Compatible with shortcodes |
| Beaver Builder | Full Support | Works with BB content |
| Oxygen Builder | Full Support | Supports Oxygen structure |
SEO Plugins
| Plugin | Recommended Config | Notes |
|---|---|---|
| Rank Math | Disable ALT | Let Rank Math auto-generate |
| Yoast SEO | Disable ALT | Yoast handles image SEO |
| SEOPress | Disable ALT | SEOPress optimization |
| All in One SEO | Keep enabled | Works 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
| Plugin | Compatibility | Notes |
|---|---|---|
| ShortPixel | Full Support | Auto-optimizes imported images |
| Smush | Full Support | Processes on import |
| Imagify | Full Support | Automatic optimization |
| EWWW Image Optimizer | Full Support | Works seamlessly |
Content Sources
| Source | Compatibility | Special Handling |
|---|---|---|
| Google Docs | Full Support | Redirect resolution |
| Microsoft Word | Full Support | Standard import |
| Notion | Full Support | External URL detection |
| Medium | Full Support | CDN URL handling |
| Dropbox | Full Support | Direct link support |
| Google Drive | Full Support | Redirect 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:
- Writer creates article in Google Docs with images
- Writer copies entire content (Ctrl+A, Ctrl+C)
- Writer pastes into WordPress Block Editor
- Writer clicks “Publish”
- 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.jpg,post-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
- Copy the plugin code into a new file:
wp-content/plugins/rr-import-external-images/rr-import-external-images.php - 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
/**
* 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';
}
}