Created
June 1, 2025 00:45
-
-
Save Jany-M/a57d880c65fe5a87a779bacb6655c39e to your computer and use it in GitHub Desktop.
[WP] WordPress utility for cleaning up generated image variations (thumbnails) garbage in bulk, and for regenerating thumbnails for specific images. With every deletion or regeneration, it also updates the attachment metadata in the database to remove references to deleted variations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
WordPress utility for cleaning up generated image variations (thumbnails) in bulk, in a wp-content/uploads/{year} directory (all months), and for regenerating thumbnails for specific images. | |
With every deletion or regeneration, it also updates the attachment metadata in the database to remove references to deleted variations. | |
Drop the file in the ROOT of your website (not in plugins) and access it via browser directly after you have logged into your WordPress dashboard first. It only works for administrator users. | |
ALWAYS backup your database AND files before using it. Delete the file after use. | |
Made to work on WP Engine, but should work on any host. | |
Last Update: 1 June 2025 | |
Version: 1.0 | |
Developed by Shambix, a web & mobile development agency with 10+ experience in WordPress. | |
https://www.shambix.com | |
[email protected] | |
*/ | |
?> | |
<?php | |
define('SHAMBIX_BATCH_SIZE', 100); // Unified batch size for all batch operations | |
/** | |
* WordPress Generated Image Variation Cleaner for WP Engine | |
* Place this file in your WordPress *root* directory and access via browser | |
* Delete the file after you're done for safety reasons | |
*/ | |
// ===================================================================== | |
// 1. HANDLE BATCH PROCESSING FIRST (BEFORE ANY OUTPUT) | |
// ===================================================================== | |
if (isset($_POST['action']) && $_POST['action'] === 'process_batch') { | |
ob_start(); | |
require_once('wp-load.php'); | |
if (!current_user_can('manage_options')) { | |
wp_die('Access Denied: You must be an administrator to run this script.'); | |
} | |
set_time_limit(0); | |
ini_set('memory_limit', '1024M'); | |
ignore_user_abort(true); | |
try { | |
$year = isset($_POST['year']) ? sanitize_text_field($_POST['year']) : '2025'; | |
$offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0; | |
$dry_run = isset($_POST['dry_run']) ? boolval($_POST['dry_run']) : true; | |
$YEAR_DIR = ABSPATH . 'wp-content/uploads/' . $year; | |
if (!is_dir($YEAR_DIR)) { | |
throw new Exception("Directory does not exist: $YEAR_DIR"); | |
} | |
$BATCH_SIZE = SHAMBIX_BATCH_SIZE; | |
// Accept matching_files from POST (JSON encoded array) | |
$matching_files = null; | |
if (!empty($_POST['matching_files'])) { | |
$matching_files = json_decode(stripslashes($_POST['matching_files']), true); | |
if (!is_array($matching_files)) $matching_files = null; | |
} | |
// If not provided, build the list (first batch) | |
if ($matching_files === null) { | |
$matching_files = []; | |
$iterator = new RecursiveIteratorIterator( | |
new RecursiveDirectoryIterator($YEAR_DIR, FilesystemIterator::SKIP_DOTS), | |
RecursiveIteratorIterator::SELF_FIRST | |
); | |
foreach ($iterator as $file) { | |
if ($file->isDir()) continue; | |
$filename = $file->getFilename(); | |
if (preg_match('/^(.+)-(\d+)x(\d+)\.(jpe?g|png|webp|gif)$/i', $filename)) { | |
$matching_files[] = $file->getPathname(); | |
} | |
} | |
} | |
$total_files = count($matching_files); | |
list($deleted, $skipped, $metadata_updated, $output, $current) = process_directory_batch( | |
$YEAR_DIR, | |
$dry_run, | |
$offset, | |
$BATCH_SIZE, | |
$matching_files | |
); | |
$finished = ($deleted + $skipped == 0) || ($current >= $total_files); | |
ob_end_clean(); | |
header('Content-Type: application/json'); | |
echo json_encode([ | |
'output' => $output, | |
'current' => $current, | |
'total' => $total_files, | |
'finished' => $finished, | |
'deleted' => $deleted, | |
'skipped' => $skipped, | |
'metadata_updated' => $metadata_updated, | |
// Always return the file list in every batch | |
'matching_files' => $matching_files | |
]); | |
exit; | |
} catch (Exception $e) { | |
ob_end_clean(); | |
header('Content-Type: application/json'); | |
echo json_encode([ | |
'error' => 'Error: ' . $e->getMessage(), | |
'output' => 'Exception occurred during processing' | |
]); | |
exit; | |
} | |
} | |
// ===================================================================== | |
// 2. REGULAR PAGE LOAD - REST OF THE SCRIPT | |
// ===================================================================== | |
require_once('wp-load.php'); // Load WordPress environment | |
// Only allow administrators to run this script | |
if (!current_user_can('manage_options')) { | |
wp_die('Access Denied: You must be an administrator to run this script.'); | |
} | |
// Only run if we're in a controlled environment | |
$allowed_ips = ['127.0.0.1', '::1']; | |
$server_ip = $_SERVER['REMOTE_ADDR'] ?? ''; | |
if (!in_array($server_ip, $allowed_ips) && !current_user_can('manage_options')) { | |
wp_die('<h1>Access Denied</h1><p>This script can only be run by administrators.</p>'); | |
} | |
// Disable time limits and memory limits | |
set_time_limit(0); | |
ini_set('memory_limit', '1024M'); | |
ignore_user_abort(true); | |
// Main processing function | |
function process_directory($dir, $dry_run = true) { | |
if (!is_dir($dir)) return [0, 0, 0, '']; | |
$deleted = 0; | |
$skipped = 0; | |
$metadata_updated = 0; | |
$output = ''; | |
$iterator = new RecursiveIteratorIterator( | |
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), | |
RecursiveIteratorIterator::SELF_FIRST | |
); | |
$original_files = []; | |
$attachments_to_update = []; | |
// First pass: Collect all original files | |
foreach ($iterator as $file) { | |
if ($file->isDir()) continue; | |
$filename = $file->getFilename(); | |
if (preg_match('/^(.+)\.(jpe?g|png|webp|gif)$/i', $filename)) { | |
$original_files[strtolower($file->getPathname())] = true; | |
} | |
} | |
// Second pass: Process files | |
foreach ($iterator as $file) { | |
if ($file->isDir()) continue; | |
$path = $file->getPathname(); | |
$filename = $file->getFilename(); | |
// Match generated variations | |
if (preg_match('/^(.+)-(\d+)x(\d+)\.(jpe?g|png|webp|gif)$/i', $filename, $matches)) { | |
$original_filename = $matches[1] . '.' . $matches[4]; | |
$original_path = $file->getPath() . DIRECTORY_SEPARATOR . $original_filename; | |
// Check if original exists (case-insensitive) | |
if (isset($original_files[strtolower($original_path)])) { | |
$deleted++; | |
if (!$dry_run) { | |
if (@unlink($path)) { | |
$output .= "Deleted: " . str_replace(ABSPATH, '', $path) . "\n"; | |
// Track attachments to update metadata | |
$attachments_to_update[$original_path] = $original_path; | |
} else { | |
$output .= "Error deleting: " . str_replace(ABSPATH, '', $path) . "\n"; | |
} | |
} else { | |
$output .= "Would delete: " . str_replace(ABSPATH, '', $path) . "\n"; | |
} | |
} else { | |
$skipped++; | |
$output .= "Skipped (no original): " . str_replace(ABSPATH, '', $path) . "\n"; | |
} | |
} | |
} | |
// Update attachment metadata for processed files | |
if (!$dry_run && !empty($attachments_to_update)) { | |
foreach ($attachments_to_update as $original_path) { | |
$attachment_id = attachment_url_to_postid( | |
content_url(str_replace(wp_normalize_path(WP_CONTENT_DIR), '', wp_normalize_path($original_path))) | |
); | |
if ($attachment_id) { | |
$result = clean_attachment_metadata($attachment_id, $dry_run); | |
if ($result) { | |
$metadata_updated++; | |
$output .= "Updated metadata for attachment ID: $attachment_id\n"; | |
} | |
} | |
} | |
} | |
return [$deleted, $skipped, $metadata_updated, $output]; | |
} | |
// Batch processing function | |
function process_directory_batch($dir, $dry_run = true, $offset = 0, $batch_size = null, $matching_files = null) { | |
if ($batch_size === null) $batch_size = SHAMBIX_BATCH_SIZE; | |
if (!is_dir($dir)) return [0, 0, 0, '', 0]; | |
$deleted = 0; | |
$skipped = 0; | |
$metadata_updated = 0; | |
$output = ''; | |
$processed_count = 0; | |
// If matching_files is not provided, build it (for backward compatibility) | |
if ($matching_files === null) { | |
$matching_files = []; | |
$iterator = new RecursiveIteratorIterator( | |
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), | |
RecursiveIteratorIterator::SELF_FIRST | |
); | |
foreach ($iterator as $file) { | |
if ($file->isDir()) continue; | |
$filename = $file->getFilename(); | |
if (preg_match('/^(.+)-(\d+)x(\d+)\.(jpe?g|png|webp|gif)$/i', $filename)) { | |
$matching_files[] = $file->getPathname(); | |
} | |
} | |
} | |
// Only process the current batch slice | |
$batch_files = array_slice($matching_files, $offset, $batch_size); | |
$attachments_to_update = []; | |
$original_files = []; | |
// Build original files list for existence check | |
$iterator = new RecursiveIteratorIterator( | |
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), | |
RecursiveIteratorIterator::SELF_FIRST | |
); | |
foreach ($iterator as $file) { | |
if ($file->isDir()) continue; | |
$filename = $file->getFilename(); | |
if (preg_match('/^(.+)\.(jpe?g|png|webp|gif)$/i', $filename)) { | |
$original_files[strtolower($file->getPathname())] = true; | |
} | |
} | |
foreach ($batch_files as $path) { | |
$filename = basename($path); | |
if (preg_match('/^(.+)-(\d+)x(\d+)\.(jpe?g|png|webp|gif)$/i', $filename, $matches)) { | |
$original_filename = $matches[1] . '.' . $matches[4]; | |
$original_path = dirname($path) . DIRECTORY_SEPARATOR . $original_filename; | |
$original_path_norm = wp_normalize_path(strtolower($original_path)); | |
$original_files_norm = array_map('wp_normalize_path', array_map('strtolower', array_keys($original_files))); | |
if (in_array($original_path_norm, $original_files_norm)) { | |
$deleted++; | |
if (!$dry_run) { | |
if (@unlink($path)) { | |
$output .= "Deleted: " . str_replace(ABSPATH, '', $path) . "\n"; | |
$attachments_to_update[$original_path] = $original_path; | |
} else { | |
$output .= "Error deleting (permission denied?): " . str_replace(ABSPATH, '', $path) . "\n"; | |
} | |
} else { | |
$output .= "Would delete: " . str_replace(ABSPATH, '', $path) . "\n"; | |
} | |
} else { | |
if (!file_exists($original_path)) { | |
$output .= "Skipped (original missing): " . str_replace(ABSPATH, '', $path) . "\n"; | |
} else { | |
$output .= "Skipped (original exists but not matched, check case/path): " . str_replace(ABSPATH, '', $path) . "\n"; | |
} | |
$skipped++; | |
} | |
} | |
$processed_count++; | |
} | |
// Update attachment metadata for processed files | |
if (!$dry_run && !empty($attachments_to_update)) { | |
foreach ($attachments_to_update as $original_path) { | |
$attachment_id = attachment_url_to_postid( | |
content_url(str_replace(wp_normalize_path(WP_CONTENT_DIR), '', wp_normalize_path($original_path))) | |
); | |
if ($attachment_id) { | |
$result = clean_attachment_metadata($attachment_id, $dry_run); | |
if ($result) { | |
$metadata_updated++; | |
$output .= "Updated metadata for attachment ID: $attachment_id\n"; | |
} | |
} | |
} | |
} | |
// $current is offset + processed_count (number of matching files processed so far) | |
return [$deleted, $skipped, $metadata_updated, $output, $offset + $processed_count]; | |
} | |
// Clean attachment metadata by removing references to missing files | |
function clean_attachment_metadata($attachment_id, $dry_run = true) { | |
$metadata = wp_get_attachment_metadata($attachment_id); | |
if (empty($metadata['sizes'])) return false; | |
$original_file = get_attached_file($attachment_id); | |
if (!$original_file || !file_exists($original_file)) return false; | |
$base_dir = dirname($original_file); | |
$updated = false; | |
$valid_sizes = []; | |
foreach ($metadata['sizes'] as $size_name => $size_data) { | |
$file_path = path_join($base_dir, $size_data['file']); | |
// Check if file exists and has valid dimensions | |
if (file_exists($file_path)) { | |
$valid_sizes[$size_name] = $size_data; | |
} else { | |
$updated = true; | |
} | |
} | |
if ($updated && !$dry_run) { | |
$metadata['sizes'] = $valid_sizes; | |
wp_update_attachment_metadata($attachment_id, $metadata); | |
} | |
return $updated; | |
} | |
// Regenerate thumbnails for a specific attachment | |
function regenerate_attachment_thumbs($filename, $dry_run = true) { | |
$output = ''; | |
$success = false; | |
// Find attachment by filename | |
$attachments = get_posts([ | |
'post_type' => 'attachment', | |
'post_status' => 'inherit', | |
'posts_per_page' => -1, | |
'meta_query' => [[ | |
'key' => '_wp_attached_file', | |
'value' => $filename, | |
'compare' => 'LIKE' | |
]] | |
]); | |
if (empty($attachments)) { | |
$output .= "No attachment found for filename: $filename\n"; | |
return [false, $output]; | |
} | |
// Handle multiple matches | |
if (count($attachments) > 1) { | |
$output .= "Found multiple attachments matching filename: $filename\n"; | |
$output .= "Please use the exact relative path (e.g., '2025/05/your-image.jpg')\n"; | |
$output .= "Matching attachments:\n"; | |
foreach ($attachments as $attachment) { | |
$file = get_post_meta($attachment->ID, '_wp_attached_file', true); | |
$output .= "- ID {$attachment->ID}: $file\n"; | |
} | |
return [false, $output]; | |
} | |
$attachment = reset($attachments); | |
$attachment_id = $attachment->ID; | |
$original_file = get_attached_file($attachment_id); | |
$relative_path = get_post_meta($attachment_id, '_wp_attached_file', true); | |
if (!$original_file || !file_exists($original_file)) { | |
$output .= "Original file not found for attachment ID: $attachment_id\n"; | |
$output .= "Path: $original_file\n"; | |
return [false, $output]; | |
} | |
$output .= "Processing attachment ID: $attachment_id\n"; | |
$output .= "Original file: $relative_path\n"; | |
if (!$dry_run) { | |
// Regenerate thumbnails | |
require_once(ABSPATH . 'wp-admin/includes/image.php'); | |
// First clean up existing thumbnails | |
$metadata = wp_get_attachment_metadata($attachment_id); | |
$deleted_thumbs = 0; | |
if (isset($metadata['sizes']) && is_array($metadata['sizes'])) { | |
$base_dir = dirname($original_file); | |
foreach ($metadata['sizes'] as $size_name => $size_data) { | |
$thumb_path = path_join($base_dir, $size_data['file']); | |
if (file_exists($thumb_path)) { | |
if (unlink($thumb_path)) { | |
$output .= "Deleted old thumbnail: " . $size_data['file'] . "\n"; | |
$deleted_thumbs++; | |
} | |
} | |
} | |
} | |
if ($deleted_thumbs > 0) { | |
$output .= "Deleted $deleted_thumbs old thumbnails\n"; | |
} else { | |
$output .= "No existing thumbnails found to delete\n"; | |
} | |
// Regenerate metadata | |
$new_metadata = wp_generate_attachment_metadata($attachment_id, $original_file); | |
if (is_wp_error($new_metadata)) { | |
$output .= "Error regenerating metadata: " . $new_metadata->get_error_message() . "\n"; | |
} else { | |
wp_update_attachment_metadata($attachment_id, $new_metadata); | |
$output .= "Successfully regenerated thumbnails!\n"; | |
$success = true; | |
// List new thumbnails | |
if (!empty($new_metadata['sizes'])) { | |
$output .= "Generated thumbnails:\n"; | |
foreach ($new_metadata['sizes'] as $size => $size_data) { | |
$output .= "- {$size}: {$size_data['width']}x{$size_data['height']} ({$size_data['file']})\n"; | |
} | |
} else { | |
$output .= "No thumbnails generated (check registered image sizes)\n"; | |
} | |
} | |
} else { | |
$output .= "Would regenerate thumbnails for attachment ID: $attachment_id\n"; | |
$output .= "Relative path: $relative_path\n"; | |
$success = true; // For dry run reporting | |
} | |
return [$success, $output]; | |
} | |
// Start HTML output | |
?> | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Image Variation Manager</title> | |
<style> | |
body { font-family: sans-serif; line-height: 1.6; margin: 2em; } | |
pre { background: #f5f5f5; padding: 1em; overflow: auto; max-height: 400px; } | |
.success { color: #2e7d32; } | |
.error { color: #c62828; } | |
.warning { color: #f57f17; } | |
.tabs { display: flex; margin-bottom: 1em; } | |
.tab { padding: 10px 20px; cursor: pointer; border: 1px solid #ccc; border-bottom: none; } | |
.tab.active { background: #f5f5f5; font-weight: bold; } | |
.tab-content { display: none; border: 1px solid #ccc; padding: 20px; } | |
.tab-content.active { display: block; } | |
.progress-container { width: 100%; background: #f1f1f1; margin: 20px 0; } | |
.progress-bar { width: 0%; height: 30px; background: #4CAF50; text-align: center; line-height: 30px; color: white; } | |
.btn { padding: 10px 15px; background: #2196F3; color: white; border: none; cursor: pointer; } | |
.btn:disabled { background: #cccccc; } | |
.batch-section { margin-top: 30px; padding-top: 20px; } | |
</style> | |
<script> | |
// Initialize variables for batch processing | |
let paused = false; | |
let currentOffset = 0; | |
let totalFiles = 0; | |
let dryRun = true; | |
let processingYear = ''; | |
let matchingFiles = null; // Store the list of files for batching | |
function openTab(evt, tabName) { | |
var i, tabcontent, tablinks; | |
tabcontent = document.getElementsByClassName("tab-content"); | |
for (i = 0; i < tabcontent.length; i++) { | |
tabcontent[i].classList.remove("active"); | |
} | |
tablinks = document.getElementsByClassName("tab"); | |
for (i = 0; i < tablinks.length; i++) { | |
tablinks[i].classList.remove("active"); | |
} | |
document.getElementById(tabName).classList.add("active"); | |
evt.currentTarget.classList.add("active"); | |
// Reset batch UI when switching to batch tab | |
if (tabName === 'cleaner-batch') { | |
document.getElementById('batch-output').innerHTML = ''; | |
document.getElementById('progress-bar').style.width = '0%'; | |
document.getElementById('progress-bar').innerHTML = '0%'; | |
document.getElementById('progress-bar').style.backgroundColor = '#4CAF50'; | |
document.getElementById('progress-text').innerHTML = 'Processed: 0/0 files'; | |
currentOffset = 0; | |
} | |
} | |
function startBatchProcessing() { | |
processingYear = document.getElementById('batch_year').value; | |
dryRun = !document.getElementById('batch_confirm').checked; | |
paused = false; | |
document.getElementById('batch-output').innerHTML = ''; | |
document.getElementById('progress-bar').style.width = '0%'; | |
document.getElementById('progress-bar').innerHTML = '0%'; | |
document.getElementById('progress-bar').style.backgroundColor = '#4CAF50'; | |
document.getElementById('progress-text').innerHTML = 'Processed: 0/0 files'; | |
currentOffset = 0; | |
matchingFiles = null; // Reset file list on new run | |
document.getElementById('start-btn').disabled = true; | |
document.getElementById('pause-btn').disabled = false; | |
document.getElementById('resume-btn').disabled = true; | |
processBatch(); | |
} | |
function pauseProcessing() { | |
paused = true; | |
document.getElementById('pause-btn').disabled = true; | |
document.getElementById('resume-btn').disabled = false; | |
} | |
function resumeProcessing() { | |
paused = false; | |
document.getElementById('pause-btn').disabled = false; | |
document.getElementById('resume-btn').disabled = true; | |
processBatch(); | |
} | |
function processBatch() { | |
if (paused) return; | |
const outputElement = document.getElementById('batch-output'); | |
const progressBar = document.getElementById('progress-bar'); | |
const progressText = document.getElementById('progress-text'); | |
const xhr = new XMLHttpRequest(); | |
xhr.open('POST', '<?php echo $_SERVER['PHP_SELF']; ?>', true); | |
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); | |
xhr.onreadystatechange = function() { | |
if (xhr.readyState === 4) { | |
if (xhr.status === 200) { | |
try { | |
const response = JSON.parse(xhr.responseText); | |
if (response.error) { | |
outputElement.innerHTML += '<br><span class="error">' + response.error + '</span>'; | |
document.getElementById('start-btn').disabled = false; | |
document.getElementById('pause-btn').disabled = true; | |
return; | |
} | |
// Update output | |
if (response.output) { | |
outputElement.innerHTML += response.output.replace(/\n/g, '<br>'); | |
outputElement.scrollTop = outputElement.scrollHeight; | |
} | |
// Always update the file list from the response | |
if (response.matching_files && Array.isArray(response.matching_files)) { | |
matchingFiles = response.matching_files; | |
} | |
// Only set totalFiles on the first batch | |
if (totalFiles === 0 && response.total > 0) { | |
totalFiles = response.total; | |
} | |
const progress = totalFiles > 0 ? Math.round((response.current / totalFiles) * 100) : 0; | |
progressBar.style.width = progress + '%'; | |
progressBar.innerHTML = progress + '%'; | |
progressText.innerHTML = `Processed: ${response.current}/${totalFiles} files`; | |
currentOffset = response.current; | |
// Continue processing if not finished | |
if (response.finished) { | |
progressBar.style.backgroundColor = '#4CAF50'; | |
progressBar.innerHTML = 'Completed!'; | |
progressText.innerHTML = `Finished! Processed ${totalFiles} files`; | |
document.getElementById('start-btn').disabled = false; | |
document.getElementById('pause-btn').disabled = true; | |
outputElement.innerHTML += `<br><br><b>Summary:</b><br>Files deleted: ${response.deleted}<br>Files skipped: ${response.skipped}<br>Metadata updated: ${response.metadata_updated}`; | |
} else if (!paused) { | |
setTimeout(processBatch, 500); | |
} | |
} catch (e) { | |
outputElement.innerHTML += '<br><span class="error">Error parsing response: ' + e.message + '</span>'; | |
document.getElementById('start-btn').disabled = false; | |
document.getElementById('pause-btn').disabled = true; | |
} | |
} else { | |
outputElement.innerHTML += '<br><span class="error">Server Error: ' + xhr.statusText + '</span>'; | |
document.getElementById('start-btn').disabled = false; | |
document.getElementById('pause-btn').disabled = true; | |
} | |
} | |
}; | |
xhr.onerror = function() { | |
outputElement.innerHTML += '<br><span class="error">Request failed. Check console for details.</span>'; | |
document.getElementById('start-btn').disabled = false; | |
document.getElementById('pause-btn').disabled = true; | |
}; | |
// Send request, always include matchingFiles if available | |
let postData = `action=process_batch&year=${processingYear}&offset=${currentOffset}&dry_run=${dryRun ? 1 : 0}`; | |
if (matchingFiles !== null) { | |
postData += `&matching_files=${encodeURIComponent(JSON.stringify(matchingFiles))}`; | |
} | |
xhr.send(postData); | |
} | |
</script> | |
</head> | |
<body> | |
<h2>WordPress Thumbs Cleaner - by <a href="https://www.shambix.com" target="_blank">Shambix</a></h2> | |
<p>Use this tool to clean up generated image variations and regenerate thumbnails.</p> | |
<p class="warning"><strong>Please ensure you have a backup of your site before proceeding, the deletions are irreversible. Use the script at your own risk and delete it after use.</strong></p> | |
<hr/> | |
<div class="tabs"> | |
<div class="tab active" onclick="openTab(event, 'cleaner-standard')">Clean Variations (Standard)</div> | |
<div class="tab" onclick="openTab(event, 'cleaner-batch')">Clean Variations (Batch)</div> | |
<div class="tab" onclick="openTab(event, 'regenerate')">Regenerate Thumbnails</div> | |
</div> | |
<?php | |
if ($_SERVER['REQUEST_METHOD'] === 'POST') { | |
if (isset($_POST['action']) && $_POST['action'] === 'regenerate') { | |
// Handle thumbnail regeneration | |
$dry_run = empty($_POST['confirm']); | |
$filename = isset($_POST['filename']) ? sanitize_text_field($_POST['filename']) : ''; | |
echo "<div id='regenerate' class='tab-content active'>"; | |
echo "<h2>Regenerate Thumbnails</h2>"; | |
if (empty($filename)) { | |
echo "<p class='error'>Please enter a filename</p>"; | |
} else { | |
list($success, $output) = regenerate_attachment_thumbs($filename, $dry_run); | |
echo "<p>Target file: $filename</p>"; | |
echo "<p>Mode: " . ($dry_run ? "DRY RUN (no changes)" : "LIVE REGENERATION") . "</p>"; | |
echo "<hr>"; | |
echo "<pre>$output</pre>"; | |
echo "<hr>"; | |
if ($success) { | |
echo "<p class='success'>Operation completed successfully!</p>"; | |
} else { | |
echo "<p class='error'>There were errors during processing</p>"; | |
} | |
} | |
echo '<p><a href="'.esc_url($_SERVER['REQUEST_URI']).'">Run again</a></p>'; | |
echo "</div>"; | |
} else if (isset($_POST['action']) && $_POST['action'] === 'clean_standard') { | |
// Handle standard image cleanup | |
$dry_run = empty($_POST['confirm']); | |
$year = isset($_POST['year']) ? sanitize_text_field($_POST['year']) : '2025'; | |
$YEAR_DIR = ABSPATH . 'wp-content/uploads/' . $year; | |
echo "<div id='cleaner-standard' class='tab-content active'>"; | |
echo "<h2>Processing: $year</h2>"; | |
echo "<p>Target directory: " . str_replace(ABSPATH, '', $YEAR_DIR) . "</p>"; | |
echo "<p>Mode: " . ($dry_run ? "DRY RUN (no changes)" : "LIVE DELETION") . "</p>"; | |
echo "<hr>"; | |
list($total_deleted, $total_skipped, $metadata_updated, $output) = process_directory($YEAR_DIR, $dry_run); | |
echo "<pre>$output</pre>"; | |
echo "<hr>"; | |
echo "<h3>Summary:</h3>"; | |
echo "<p>Total variations deleted: $total_deleted</p>"; | |
echo "<p>Total variations skipped: $total_skipped</p>"; | |
echo "<p>Metadata updated (unique attachments ID): $metadata_updated</p>"; | |
echo "<p>Original files preserved</p>"; | |
if ($dry_run) { | |
echo '<p class="warning">NOTE: Run in LIVE mode to perform actual deletion and metadata updates</p>'; | |
} | |
echo '<p><a href="'.esc_url($_SERVER['REQUEST_URI']).'">Run again</a></p>'; | |
echo "</div>"; | |
} | |
} else { | |
// Show forms | |
?> | |
<!-- Standard Cleaner Tab --> | |
<div id="cleaner-standard" class="tab-content active"> | |
<h2>Clean Image Variations (Standard)</h2> | |
<form method="post"> | |
<input type="hidden" name="action" value="clean_standard"> | |
<p> | |
<label for="standard_year">Year to process:</label> | |
<input type="text" id="standard_year" name="year" value="2025" required> | |
<span>(e.g., 2025)</span> | |
</p> | |
<p> | |
<label> | |
<input type="checkbox" name="confirm" value="1"> | |
Check to confirm actual deletion (otherwise dry run) | |
</label> | |
</p> | |
<p> | |
<input type="submit" value="Process Images" class="btn"> | |
</p> | |
</form> | |
<div class="warning"> | |
<h3>Important Notes:</h3> | |
<ul> | |
<li>Best for small to medium directories</li> | |
<li>Backup your site before running this script</li> | |
<li>Dry run mode is enabled by default</li> | |
<li>Only checks image variations in the specified year directory</li> | |
<li>Will not delete original images</li> | |
<li>Will automatically update attachment metadata</li> | |
<li>Process might time out for large directories</li> | |
</ul> | |
</div> | |
</div> | |
<!-- Batch Cleaner Tab --> | |
<div id="cleaner-batch" class="tab-content"> | |
<h2>Clean Image Variations (Batch)</h2> | |
<p> | |
<label for="batch_year">Year to process:</label> | |
<input type="text" id="batch_year" name="year" value="2025" required> | |
<span>(e.g., 2025)</span> | |
</p> | |
<p> | |
<label> | |
<input type="checkbox" id="batch_confirm" name="confirm" value="1"> | |
Check to confirm actual deletion (otherwise dry run) | |
</label> | |
</p> | |
<div class="batch-section"> | |
<div class="progress-container"> | |
<div id="progress-bar" class="progress-bar">0%</div> | |
</div> | |
<p id="progress-text">Processed: 0/0 files</p> | |
<div id="batch-output" style="height: 300px; overflow: auto; border: 1px solid #ccc; padding: 10px; margin: 20px 0;"></div> | |
<button id="start-btn" class="btn" onclick="startBatchProcessing()">Start Batch Processing</button> | |
<button id="pause-btn" class="btn" onclick="pauseProcessing()" disabled>Pause</button> | |
<button id="resume-btn" class="btn" onclick="resumeProcessing()" disabled>Resume</button> | |
</div> | |
<div class="warning"> | |
<h3>Important Notes:</h3> | |
<ul> | |
<li>For large directories (1000+ files)</li> | |
<li>Processes in batches to prevent timeouts</li> | |
<li>Backup your site before running this script</li> | |
<li>Dry run mode is enabled by default</li> | |
<li>Only checks image variations in the specified year directory</li> | |
<li>Will not delete original images</li> | |
<li>Will automatically update attachment metadata</li> | |
</ul> | |
</div> | |
</div> | |
<!-- Regenerate Thumbnails Tab --> | |
<div id="regenerate" class="tab-content"> | |
<h2>Regenerate Thumbnails</h2> | |
<form method="post"> | |
<input type="hidden" name="action" value="regenerate"> | |
<p> | |
<label for="filename">File path:</label> | |
<input type="text" id="filename" name="filename" required> | |
<span>(e.g., 2025/05/ABB25_2500x650.jpg)</span> | |
</p> | |
<p class="warning"> | |
<strong>Important:</strong> Use the relative path as shown in Media Library<br> | |
(e.g., "2025/05/your-image.jpg" or just "your-image.jpg" if unique) | |
</p> | |
<p> | |
<label> | |
<input type="checkbox" name="confirm" value="1"> | |
Check to confirm actual regeneration (otherwise dry run) | |
</label> | |
</p> | |
<p> | |
<input type="submit" value="Regenerate Thumbnails" class="btn"> | |
</p> | |
</form> | |
<div class="warning"> | |
<h3>Important Notes:</h3> | |
<ul> | |
<li>Enter the exact filename as it appears in the Media Library</li> | |
<li>For best results, use the full relative path (e.g., 2025/05/filename.jpg)</li> | |
<li>If multiple matches found, the script will show options</li> | |
<li>This will delete existing thumbnails and generate new ones</li> | |
<li>Dry run mode will show what would be generated</li> | |
<li>Thumbnails will be created for all registered image sizes</li> | |
</ul> | |
</div> | |
</div> | |
<?php | |
} | |
?> | |
<hr/> | |
<p>This <small><a href="https://gist.github.com/Jany-M/a57d880c65fe5a87a779bacb6655c39e" target="_blank">Gist</a> is on Github, <a href="https://www.shambix.com" target="_blank">contact the developers</a> if you need help with your WordPress site's regular maintenance, custom plugins, features and themes, but also for custom web or mobile app development and cloud setup services.</small></p> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment