Se o seu site WordPress ou loja Woocommerce está ocupando muito espaço no servidor com imagens e arquivos antigos inúteis, o Media Scanner é para você.
O Media Scanner é um trecho de código que cria uma interface no admin do seu WordPress, onde você pode fazer uma busca, selecionando período ou palavra-chave, por imagens ou arquivos obsoletos para seu site.
Após fazer a busca, ele irá mostrar 3 abas: Todos os arquivos; Usados; Não Usados. Então, é só marcar o que você não quer ou não precisa mais e clicar em Excluir. Mas, por medida de segurança, o Média Scanner irá criar um arquivo .zip com as imagens que serão apagadas e baixará para seu computador, caso você precise novamente de alguma imagem que foi deletada.
Instalando o Media Scanner
O Media Scanner é um trecho de código, então, para instalá-lo você precisa de um plugin como o Code Snippet. Basta instalá-lo no seu WordPress e criar um novo snippet com o código abaixo:
/**
* Media Scanner 1.6.1
*
* Batched media usage scanner with optional date filters, intelligent Elementor usage detection, and ZIP backup before delete.
*
* WARNING: Best-effort only. Always backup before deleting.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! defined( 'MEDIA_SCANNER_VERSION' ) ) {
define( 'MEDIA_SCANNER_VERSION', '1.6.1' );
}
if ( ! class_exists( 'Media_Scanner' ) ) {
class Media_Scanner {
const NONCE_ACTION = 'angie_media_scanner_nonce_media_scanner';
const PAGE_SLUG = 'media-scanner';
const AJAX_SCAN = 'angie_media_scanner_scan';
const AJAX_DELETE = 'angie_media_scanner_delete';
const AJAX_EXPORT = 'angie_media_scanner_export_zip';
const AJAX_DROP_ZIP = 'angie_media_scanner_drop_zip';
const BATCH_SIZE = 10; // Reduced from 40 to avoid server timeouts
public function __construct() {
add_action( 'admin_menu', [ $this, 'add_admin_menu' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_action( 'wp_ajax_' . self::AJAX_SCAN, [ $this, 'ajax_scan_media' ] );
add_action( 'wp_ajax_' . self::AJAX_DELETE, [ $this, 'ajax_delete_media' ] );
add_action( 'wp_ajax_' . self::AJAX_EXPORT, [ $this, 'ajax_export_zip' ] );
add_action( 'wp_ajax_' . self::AJAX_DROP_ZIP, [ $this, 'ajax_drop_zip' ] );
}
public function add_admin_menu() {
add_submenu_page(
'upload.php',
__( 'Media Usage Scanner', 'angie-snippets' ),
__( 'Media Usage Scanner', 'angie-snippets' ),
'manage_options',
self::PAGE_SLUG,
[ $this, 'render_admin_page' ]
);
}
public function enqueue_assets( $hook ) {
if ( strpos( (string) $hook, self::PAGE_SLUG ) === false ) {
return;
}
wp_register_style( 'media-scanner-inline', false, [], MEDIA_SCANNER_VERSION );
wp_enqueue_style( 'media-scanner-inline' );
wp_add_inline_style( 'media-scanner-inline', $this->get_inline_css() );
wp_register_script( 'media-scanner-inline', '', [], MEDIA_SCANNER_VERSION, true );
wp_enqueue_script( 'media-scanner-inline' );
wp_add_inline_script(
'media-scanner-inline',
'window.angie_media_scanner = ' . wp_json_encode(
[
'ajax_url' => admin_url( 'admin-ajax.php' ),
'ajax_url_fallback' => site_url( '/wp-admin/admin-ajax.php' ),
'nonce' => wp_create_nonce( self::NONCE_ACTION ),
'scan_action' => self::AJAX_SCAN,
'delete_action' => self::AJAX_DELETE,
'export_action' => self::AJAX_EXPORT,
'drop_zip_action' => self::AJAX_DROP_ZIP,
'batch_size' => self::BATCH_SIZE,
'confirm' => __( 'Are you sure you want to export a ZIP backup of the actual files and then permanently delete the selected items? This cannot be undone.', 'angie-snippets' ),
'scan_failed' => __( 'Scan failed.', 'angie-snippets' ),
'delete_failed' => __( 'Delete failed.', 'angie-snippets' ),
'unknown_error' => __( 'Unknown error', 'angie-snippets' ),
'network_error' => __( 'Request could not reach admin-ajax.php.', 'angie-snippets' ),
'http_error' => __( 'The server returned an unexpected HTTP status.', 'angie-snippets' ),
'invalid_json' => __( 'The server did not return valid JSON.', 'angie-snippets' ),
'results_notice' => __( 'Note: this scanner is best-effort only. Always backup before deleting.', 'angie-snippets' ),
'scan_complete' => __( 'Scan complete.', 'angie-snippets' ),
'scanning' => __( 'Scanning...', 'angie-snippets' ),
'preparing_zip' => __( 'Preparing ZIP backup...', 'angie-snippets' ),
'deleting' => __( 'Deleting...', 'angie-snippets' ),
'from_label' => __( 'From date', 'angie-snippets' ),
'to_label' => __( 'To date', 'angie-snippets' ),
'search_label' => __( 'Search files', 'angie-snippets' ),
'search_placeholder' => __( 'Search filename or media title…', 'angie-snippets' ),
'search_mode_label' => __( 'Match type', 'angie-snippets' ),
'zip_filename' => 'media-usage-delete-backup',
]
),
'before'
);
wp_add_inline_script( 'media-scanner-inline', $this->get_inline_js() );
}
public function render_admin_page() {
?>
<div class="wrap media-scanner-wrap">
<h1><?php esc_html_e( 'Media Usage Scanner', 'angie-snippets' ); ?></h1>
<p class="description">
<?php esc_html_e( 'Scan your media library to see what is being used and where. Includes batch scanning, optional date range filtering, ZIP export before delete, Elementor custom fonts, post content, post meta, options, featured images, WooCommerce galleries and common CSS/font references.', 'angie-snippets' ); ?>
</p>
<p class="description angie-results-note"></p>
<div class="angie-scanner-actions">
<div class="angie-scan-filters">
<label>
<span><?php esc_html_e( 'From date', 'angie-snippets' ); ?></span>
<input type="date" id="scan-date-from-media-scanner">
</label>
<label>
<span><?php esc_html_e( 'To date', 'angie-snippets' ); ?></span>
<input type="date" id="scan-date-to-media-scanner">
</label>
<div class="angie-search-pane">
<label>
<span><?php esc_html_e( 'Search files', 'angie-snippets' ); ?></span>
<input type="search" id="scan-search-term-media-scanner" placeholder="<?php echo esc_attr__( 'Search filename or media title…', 'angie-snippets' ); ?>">
</label>
<label>
<span><?php esc_html_e( 'Match type', 'angie-snippets' ); ?></span>
<select id="scan-search-mode-media-scanner">
<option value="contains"><?php esc_html_e( 'Contains', 'angie-snippets' ); ?></option>
<option value="starts"><?php esc_html_e( 'Begins with', 'angie-snippets' ); ?></option>
<option value="ends"><?php esc_html_e( 'Ends with', 'angie-snippets' ); ?></option>
<option value="exact"><?php esc_html_e( 'Exact match', 'angie-snippets' ); ?></option>
</select>
</label>
</div>
</div>
<div class="angie-scan-buttons">
<button id="scan-media-btn-media-scanner" class="button button-primary button-large">
<?php esc_html_e( 'Scan Media Library', 'angie-snippets' ); ?>
</button>
<span class="spinner"></span>
<span id="scan-progress-media-scanner" class="scan-progress-media-scanner" aria-live="polite"></span>
</div>
</div>
<div id="scan-results-container-media-scanner" style="display:none;">
<h2 class="nav-tab-wrapper">
<a href="#" class="nav-tab nav-tab-active" data-tab="all"><?php esc_html_e( 'All Media', 'angie-snippets' ); ?></a>
<a href="#" class="nav-tab" data-tab="unused"><?php esc_html_e( 'Unused', 'angie-snippets' ); ?></a>
<a href="#" class="nav-tab" data-tab="used"><?php esc_html_e( 'Used', 'angie-snippets' ); ?></a>
</h2>
<div class="tablenav top">
<div class="alignleft actions">
<button id="delete-selected-btn-media-scanner" class="button button-link-delete" disabled style="display:none;">
<?php esc_html_e( 'Export ZIP + Delete Selected', 'angie-snippets' ); ?>
</button>
</div>
<div class="tablenav-pages">
<span class="displaying-num">
<span id="result-count-media-scanner">0</span> <?php esc_html_e( 'items shown', 'angie-snippets' ); ?>
</span>
</div>
</div>
<table class="wp-list-table widefat fixed striped table-view-list media">
<thead>
<tr>
<td class="manage-column column-cb check-column">
<input id="cb-select-all-1" type="checkbox">
</td>
<th scope="col" class="manage-column column-thumbnail"><?php esc_html_e( 'Preview', 'angie-snippets' ); ?></th>
<th scope="col" class="manage-column column-title"><?php esc_html_e( 'Details', 'angie-snippets' ); ?></th>
<th scope="col" class="manage-column column-status"><?php esc_html_e( 'Status', 'angie-snippets' ); ?></th>
<th scope="col" class="manage-column column-used-in"><?php esc_html_e( 'Used In', 'angie-snippets' ); ?></th>
</tr>
</thead>
<tbody id="scan-results-body-media-scanner"></tbody>
</table>
</div>
</div>
<?php
}
private function normalize_date( $date, $end_of_day = false ) {
$date = is_string( $date ) ? trim( $date ) : '';
if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
return '';
}
return $date . ( $end_of_day ? ' 23:59:59' : ' 00:00:00' );
}
private function normalize_search_term( $term ) {
$term = is_string( $term ) ? sanitize_text_field( wp_unslash( $term ) ) : '';
return trim( $term );
}
private function normalize_search_mode( $mode ) {
$mode = is_string( $mode ) ? sanitize_key( $mode ) : '';
$allowed = [ 'contains', 'starts', 'ends', 'exact' ];
return in_array( $mode, $allowed, true ) ? $mode : 'contains';
}
private function get_search_like_value( $term, $mode = 'contains' ) {
global $wpdb;
$term = (string) $term;
if ( '' === $term ) {
return '';
}
$like = $wpdb->esc_like( $term );
switch ( $mode ) {
case 'starts':
return $like . '%';
case 'ends':
return '%' . $like;
case 'exact':
return $like;
case 'contains':
default:
return '%' . $like . '%';
}
}
private function get_scan_where_sql( $from_date = '', $to_date = '', $search_term = '', $search_mode = 'contains' ) {
global $wpdb;
$where = " WHERE post_type = 'attachment' AND post_status = 'inherit' ";
$args = [];
$from = $this->normalize_date( $from_date, false );
$to = $this->normalize_date( $to_date, true );
$search_term = $this->normalize_search_term( $search_term );
$search_mode = $this->normalize_search_mode( $search_mode );
if ( $from ) {
$where .= ' AND post_date >= %s ';
$args[] = $from;
}
if ( $to ) {
$where .= ' AND post_date <= %s ';
$args[] = $to;
}
if ( '' !== $search_term ) {
$search_like = $this->get_search_like_value( $search_term, $search_mode );
$path_like = $search_like;
if ( 'starts' === $search_mode || 'exact' === $search_mode ) {
$path_like = '%/' . $wpdb->esc_like( $search_term ) . ( 'starts' === $search_mode ? '%' : '' );
}
$where .= " AND ( post_title LIKE %s OR EXISTS (
SELECT 1 FROM {$wpdb->postmeta} pm
WHERE pm.post_id = {$wpdb->posts}.ID
AND pm.meta_key = '_wp_attached_file'
AND ( pm.meta_value LIKE %s OR pm.meta_value LIKE %s )
) ) ";
$args[] = $search_like;
$args[] = $search_like;
$args[] = $path_like;
}
return [
'sql' => $where,
'args' => $args,
];
}
private function get_total_attachments( $from_date = '', $to_date = '', $search_term = '', $search_mode = 'contains' ) {
global $wpdb;
$where = $this->get_scan_where_sql( $from_date, $to_date, $search_term, $search_mode );
$sql = "SELECT COUNT(ID) FROM {$wpdb->posts}" . $where['sql'];
if ( ! empty( $where['args'] ) ) {
$sql = $wpdb->prepare( $sql, $where['args'] );
}
return (int) $wpdb->get_var( $sql );
}
private function get_attachment_batch( $offset = 0, $limit = self::BATCH_SIZE, $from_date = '', $to_date = '', $search_term = '', $search_mode = 'contains' ) {
global $wpdb;
$offset = max( 0, (int) $offset );
$limit = max( 1, min( 100, (int) $limit ) );
$where = $this->get_scan_where_sql( $from_date, $to_date, $search_term, $search_mode );
$args = $where['args'];
$args[] = $limit;
$args[] = $offset;
$sql = "SELECT ID, post_title, post_date, post_mime_type
FROM {$wpdb->posts}
{$where['sql']}
ORDER BY ID DESC
LIMIT %d OFFSET %d";
return $wpdb->get_results( $wpdb->prepare( $sql, $args ) );
}
private function get_attachment_filename( $attachment_id ) {
$file_url = wp_get_attachment_url( $attachment_id );
if ( $file_url ) {
$path = wp_parse_url( $file_url, PHP_URL_PATH );
if ( $path ) {
return wp_basename( $path );
}
}
$file_path = get_attached_file( $attachment_id );
if ( $file_path ) {
return wp_basename( $file_path );
}
return '';
}
private function get_relative_upload_path( $attachment_id ) {
$relative = get_post_meta( $attachment_id, '_wp_attached_file', true );
return is_string( $relative ) ? trim( $relative ) : '';
}
private function is_font_attachment( $attachment_id, $filename = '' ) {
$filename = $filename ? $filename : $this->get_attachment_filename( $attachment_id );
$ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
return in_array( $ext, [ 'woff', 'woff2', 'ttf', 'otf', 'eot', 'svg' ], true );
}
private function get_search_terms_for_attachment( $attachment_id, $filename = '' ) {
$terms = [];
$filename = $filename ? $filename : $this->get_attachment_filename( $attachment_id );
$file_url = wp_get_attachment_url( $attachment_id );
$relative_path = $this->get_relative_upload_path( $attachment_id );
if ( $filename ) {
$terms[] = $filename;
}
if ( $file_url ) {
$terms[] = $file_url;
$parsed_path = wp_parse_url( $file_url, PHP_URL_PATH );
if ( $parsed_path ) {
$terms[] = $parsed_path;
}
}
if ( $relative_path ) {
$terms[] = $relative_path;
$terms[] = wp_basename( $relative_path );
}
$terms = array_filter( array_map( 'trim', $terms ) );
$terms = array_unique( $terms );
return array_values( $terms );
}
private function add_usage_label( &$used_in, $label ) {
$label = trim( (string) $label );
if ( $label !== '' && ! in_array( $label, $used_in, true ) ) {
$used_in[] = $label;
}
}
private function elementor_value_matches_attachment( $value, $terms, $attachment_id ) {
if ( is_numeric( $value ) && (int) $value === (int) $attachment_id ) {
return true;
}
if ( is_scalar( $value ) ) {
$value = (string) $value;
foreach ( (array) $terms as $term ) {
$term = (string) $term;
if ( $term !== '' && stripos( $value, $term ) !== false ) {
return true;
}
}
}
return false;
}
private function elementor_collect_match_paths( $value, $terms, $attachment_id, $prefix = '' ) {
$paths = [];
if ( is_array( $value ) ) {
foreach ( $value as $key => $subvalue ) {
$key_str = is_int( $key ) ? (string) $key : sanitize_key( (string) $key );
$path = '' === $prefix ? $key_str : $prefix . '/' . $key_str;
if ( $this->elementor_value_matches_attachment( $subvalue, $terms, $attachment_id ) ) {
$paths[] = $path;
}
if ( is_array( $subvalue ) ) {
$paths = array_merge( $paths, $this->elementor_collect_match_paths( $subvalue, $terms, $attachment_id, $path ) );
}
}
}
return array_values( array_unique( array_filter( $paths ) ) );
}
private function elementor_get_class_hint( $settings ) {
$parts = [];
if ( is_array( $settings ) ) {
if ( ! empty( $settings['css_classes'] ) ) {
$parts[] = 'class: ' . trim( (string) $settings['css_classes'] );
}
if ( ! empty( $settings['_css_classes'] ) ) {
$parts[] = 'class: ' . trim( (string) $settings['_css_classes'] );
}
if ( ! empty( $settings['element_id'] ) ) {
$parts[] = 'id: ' . trim( (string) $settings['element_id'] );
}
}
$parts = array_values( array_unique( array_filter( $parts ) ) );
return empty( $parts ) ? '' : ' [' . implode( ' | ', $parts ) . ']';
}
private function elementor_get_base_label( $element ) {
$el_type = isset( $element['elType'] ) ? (string) $element['elType'] : '';
$widget_type = isset( $element['widgetType'] ) ? (string) $element['widgetType'] : '';
if ( 'widget' === $el_type ) {
$map = [
'image' => 'Image widget',
'image-box' => 'Image Box widget',
'image-carousel' => 'Image Carousel widget',
'media-carousel' => 'Media Carousel widget',
'slides' => 'Slides widget',
'gallery' => 'Gallery widget',
'heading' => 'Heading widget',
'text-editor' => 'Text Editor widget',
'icon-box' => 'Icon Box widget',
'icon-list' => 'Icon List widget',
'video' => 'Video widget',
'button' => 'Button widget',
'testimonial' => 'Testimonial widget',
'testimonials' => 'Testimonials widget',
'carousel' => 'Carousel widget',
];
if ( isset( $map[ $widget_type ] ) ) {
return $map[ $widget_type ];
}
if ( $widget_type ) {
return ucwords( str_replace( [ '-', '_' ], ' ', $widget_type ) ) . ' widget';
}
return 'Widget';
}
if ( 'container' === $el_type ) {
return 'Container';
}
if ( 'section' === $el_type ) {
return 'Section';
}
if ( 'column' === $el_type ) {
return 'Column';
}
return $el_type ? ucwords( str_replace( [ '-', '_' ], ' ', $el_type ) ) : 'Element';
}
private function elementor_describe_match_path( $path, $element, $is_font ) {
$path = strtolower( (string) $path );
$base_label = $this->elementor_get_base_label( $element );
$widget_type = isset( $element['widgetType'] ) ? (string) $element['widgetType'] : '';
if ( false !== strpos( $path, 'background_overlay' ) ) {
return $base_label . ' background overlay image';
}
if ( false !== strpos( $path, 'background_image' ) || false !== strpos( $path, 'background/background' ) || false !== strpos( $path, 'background' ) ) {
return $base_label . ' background image';
}
if ( false !== strpos( $path, 'carousel' ) || false !== strpos( $path, 'slides' ) ) {
return $base_label . ' carousel/slide item';
}
if ( false !== strpos( $path, 'gallery' ) ) {
return $base_label . ' gallery item';
}
if ( false !== strpos( $path, 'image' ) ) {
return $base_label . ' image';
}
if ( false !== strpos( $path, 'poster' ) || false !== strpos( $path, 'video' ) ) {
return $base_label . ' media asset';
}
if ( $is_font ) {
if ( false !== strpos( $path, 'font_family' ) || false !== strpos( $path, 'typography' ) ) {
return $base_label . ' typography';
}
if ( in_array( $widget_type, [ 'heading', 'text-editor' ], true ) ) {
return $base_label . ' typography';
}
return $base_label . ' font/style setting';
}
return $base_label;
}
private function elementor_extract_labels_from_elements( $elements, $post_title, $post_type, $terms, $attachment_id, $is_font ) {
$labels = [];
if ( ! is_array( $elements ) ) {
return $labels;
}
foreach ( $elements as $element ) {
if ( ! is_array( $element ) ) {
continue;
}
$settings = isset( $element['settings'] ) && is_array( $element['settings'] ) ? $element['settings'] : [];
$class_hint = $this->elementor_get_class_hint( $settings );
$match_paths = $this->elementor_collect_match_paths( $settings, $terms, $attachment_id );
$match_paths = array_slice( $match_paths, 0, 3 );
foreach ( $match_paths as $path ) {
$labels[] = sprintf(
'Elementor: %s (%s) → %s%s',
$post_title ?: '(no title)',
$post_type,
$this->elementor_describe_match_path( $path, $element, $is_font ),
$class_hint
);
}
if ( isset( $element['elements'] ) && is_array( $element['elements'] ) ) {
$labels = array_merge(
$labels,
$this->elementor_extract_labels_from_elements( $element['elements'], $post_title, $post_type, $terms, $attachment_id, $is_font )
);
}
}
return array_values( array_unique( array_filter( $labels ) ) );
}
private function get_elementor_usage_labels( $post_id, $post_title, $post_type, $meta_key, $terms, $attachment_id, $is_font = false ) {
$labels = [];
if ( '_elementor_data' === $meta_key ) {
$raw = get_post_meta( $post_id, '_elementor_data', true );
$data = is_string( $raw ) ? json_decode( $raw, true ) : $raw;
if ( is_array( $data ) ) {
$labels = $this->elementor_extract_labels_from_elements( $data, $post_title, $post_type, $terms, $attachment_id, $is_font );
}
} elseif ( '_elementor_page_settings' === $meta_key ) {
$settings = get_post_meta( $post_id, '_elementor_page_settings', true );
if ( is_array( $settings ) ) {
$match_paths = array_slice( $this->elementor_collect_match_paths( $settings, $terms, $attachment_id ), 0, 3 );
foreach ( $match_paths as $path ) {
$desc = $is_font ? 'Page settings font/style setting' : 'Page settings background/media';
if ( false !== strpos( strtolower( $path ), 'background' ) ) {
$desc = 'Page settings background image';
} elseif ( $is_font && ( false !== strpos( strtolower( $path ), 'font' ) || false !== strpos( strtolower( $path ), 'typography' ) ) ) {
$desc = 'Page settings typography';
}
$labels[] = sprintf( 'Elementor: %s (%s) → %s', $post_title ?: '(no title)', $post_type, $desc );
}
}
}
return array_values( array_unique( array_filter( $labels ) ) );
}
private function get_elementor_custom_font_map() {
$custom_font_files = [];
if ( ! post_type_exists( 'elementor_font_face' ) ) {
return $custom_font_files;
}
$font_posts = get_posts(
[
'post_type' => 'elementor_font_face',
'posts_per_page' => -1,
'post_status' => 'publish',
]
);
foreach ( $font_posts as $font ) {
$meta = get_post_meta( $font->ID );
foreach ( $meta as $key => $values ) {
if ( strpos( $key, 'font_face_file_' ) !== false ) {
foreach ( (array) $values as $val ) {
$data = maybe_unserialize( $val );
if ( is_array( $data ) && isset( $data['id'] ) ) {
$fid = (int) $data['id'];
if ( $fid > 0 ) {
$custom_font_files[ $fid ][] = 'Elementor Font: ' . $font->post_title;
}
}
}
}
if ( 'font_face' === $key ) {
foreach ( (array) $values as $val ) {
$repeater = maybe_unserialize( $val );
if ( is_array( $repeater ) ) {
foreach ( $repeater as $row ) {
foreach ( [ 'woff', 'woff2', 'ttf', 'svg', 'eot', 'otf' ] as $type ) {
if ( isset( $row[ $type ] ) && is_array( $row[ $type ] ) && isset( $row[ $type ]['id'] ) ) {
$fid = (int) $row[ $type ]['id'];
if ( $fid > 0 ) {
$custom_font_files[ $fid ][] = 'Elementor Font: ' . $font->post_title;
}
}
}
}
}
}
}
}
}
return $custom_font_files;
}
private function search_posts_for_terms( $terms ) {
global $wpdb;
$matches = [];
foreach ( (array) $terms as $term ) {
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title, post_type
FROM {$wpdb->posts}
WHERE post_content LIKE %s
AND post_type NOT IN ('attachment', 'revision')
AND post_status NOT IN ('trash', 'auto-draft')",
'%' . $wpdb->esc_like( $term ) . '%'
)
);
foreach ( $rows as $row ) {
$key = $row->ID . '|content';
$matches[ $key ] = $row;
}
}
return $matches;
}
private function scalar_string_matches_attachment( $value, $terms, $attachment_id ) {
$value = is_scalar( $value ) ? trim( (string) $value ) : '';
if ( '' === $value ) {
return false;
}
if ( ctype_digit( $value ) && (int) $value === (int) $attachment_id ) {
return true;
}
if ( preg_match( '/(^|[^0-9])' . preg_quote( (string) $attachment_id, '/' ) . '([^0-9]|$)/', $value ) ) {
return true;
}
foreach ( (array) $terms as $term ) {
$term = trim( (string) $term );
if ( '' !== $term && false !== stripos( $value, $term ) ) {
return true;
}
}
return false;
}
private function meta_value_matches_attachment( $value, $terms, $attachment_id ) {
if ( is_numeric( $value ) && (int) $value === (int) $attachment_id ) {
return true;
}
if ( is_string( $value ) ) {
$json = json_decode( $value, true );
if ( JSON_ERROR_NONE === json_last_error() && is_array( $json ) ) {
if ( $this->meta_value_matches_attachment( $json, $terms, $attachment_id ) ) {
return true;
}
}
return $this->scalar_string_matches_attachment( $value, $terms, $attachment_id );
}
if ( is_array( $value ) ) {
foreach ( $value as $subvalue ) {
if ( $this->meta_value_matches_attachment( $subvalue, $terms, $attachment_id ) ) {
return true;
}
}
}
if ( is_object( $value ) ) {
foreach ( get_object_vars( $value ) as $subvalue ) {
if ( $this->meta_value_matches_attachment( $subvalue, $terms, $attachment_id ) ) {
return true;
}
}
}
return false;
}
private function search_postmeta_for_attachment( $attachment_id, $terms, $font_only = false ) {
global $wpdb;
$matches = [];
$attachment_id = (int) $attachment_id;
$where_parts = [];
$args = [];
foreach ( array_values( array_unique( array_filter( array_map( 'strval', (array) $terms ) ) ) ) as $term ) {
$where_parts[] = 'pm.meta_value LIKE %s';
$args[] = '%' . $wpdb->esc_like( $term ) . '%';
}
if ( $attachment_id > 0 ) {
$id_string = (string) $attachment_id;
$where_parts[] = 'pm.meta_value = %s';
$args[] = $id_string;
$where_parts[] = 'pm.meta_value LIKE %s';
$args[] = '%i:' . $wpdb->esc_like( $id_string ) . ';%';
$where_parts[] = 'pm.meta_value LIKE %s';
$args[] = '%s:' . strlen( $id_string ) . ':"' . $wpdb->esc_like( $id_string ) . '"%';
$where_parts[] = 'pm.meta_value LIKE %s';
$args[] = '%"' . $wpdb->esc_like( $id_string ) . '"%';
$where_parts[] = 'pm.meta_value LIKE %s';
$args[] = '%,' . $wpdb->esc_like( $id_string ) . ',%';
}
if ( empty( $where_parts ) ) {
return $matches;
}
$sql = "SELECT p.ID, p.post_title, p.post_type, pm.meta_key, pm.meta_value
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
WHERE (" . implode( ' OR ', $where_parts ) . ")
AND p.post_status NOT IN ('trash', 'auto-draft')
AND pm.meta_key NOT IN (
'_wp_attached_file',
'_wp_attachment_metadata',
'_wp_attachment_backup_sizes',
'_wp_attachment_image_alt'
)";
if ( $font_only ) {
$sql .= " AND pm.meta_key IN ('_elementor_data', '_elementor_page_settings', '_elementor_css', '_elementor_controls_usage', '_wp_attachment_metadata')";
}
$rows = $wpdb->get_results( $wpdb->prepare( $sql, $args ) );
foreach ( $rows as $row ) {
$decoded = maybe_unserialize( $row->meta_value );
if ( ! $this->meta_value_matches_attachment( $decoded, $terms, $attachment_id ) ) {
continue;
}
$key = $row->ID . '|' . $row->meta_key;
$matches[ $key ] = $row;
}
return $matches;
}
private function search_options_for_terms( $terms ) {
global $wpdb;
$matches = [];
foreach ( (array) $terms as $term ) {
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT option_name
FROM {$wpdb->options}
WHERE option_value LIKE %s",
'%' . $wpdb->esc_like( $term ) . '%'
)
);
foreach ( $rows as $row ) {
$matches[ $row->option_name ] = $row->option_name;
}
}
return array_values( $matches );
}
private function get_attachment_usage( $attachment_id, $custom_font_files = [] ) {
global $wpdb;
$used_in = [];
$attachment_id = (int) $attachment_id;
$filename = $this->get_attachment_filename( $attachment_id );
$site_logo = (int) get_theme_mod( 'custom_logo' );
$site_icon = (int) get_option( 'site_icon' );
$is_font = $this->is_font_attachment( $attachment_id, $filename );
$terms = $this->get_search_terms_for_attachment( $attachment_id, $filename );
if ( $attachment_id === $site_logo ) {
$this->add_usage_label( $used_in, 'Site Logo' );
}
if ( $attachment_id === $site_icon ) {
$this->add_usage_label( $used_in, 'Site Favicon' );
}
if ( isset( $custom_font_files[ $attachment_id ] ) ) {
foreach ( (array) $custom_font_files[ $attachment_id ] as $font_usage ) {
$this->add_usage_label( $used_in, $font_usage );
}
}
$featured_in = $wpdb->get_results(
$wpdb->prepare(
"SELECT p.ID, p.post_title, p.post_type
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
WHERE pm.meta_key = '_thumbnail_id'
AND pm.meta_value = %d
AND p.post_status NOT IN ('trash', 'auto-draft')",
$attachment_id
)
);
foreach ( $featured_in as $post ) {
$this->add_usage_label(
$used_in,
sprintf( 'Featured Image: %s (%s)', $post->post_title ?: '(no title)', $post->post_type )
);
}
$posts = $this->search_posts_for_terms( $terms );
foreach ( $posts as $post ) {
if ( 'wp_custom_css' === $post->post_type ) {
$this->add_usage_label( $used_in, 'Custom CSS (Customizer)' );
} else {
$this->add_usage_label(
$used_in,
sprintf( 'Content: %s (%s)', $post->post_title ?: '(no title)', $post->post_type )
);
}
}
$postmeta_matches = $this->search_postmeta_for_attachment( $attachment_id, $terms, $is_font );
$ignored_meta_keys = [
'_wp_attached_file',
'_wp_attachment_metadata',
'_wp_attachment_backup_sizes',
'_wp_attachment_image_alt',
];
foreach ( $postmeta_matches as $match ) {
// Ignore attachment housekeeping meta.
if ( in_array( $match->meta_key, $ignored_meta_keys, true ) ) {
continue;
}
// Ignore the attachment matching against its own record.
if ( (int) $match->ID === $attachment_id && 'attachment' === $match->post_type ) {
continue;
}
if ( in_array( $match->meta_key, [ '_elementor_data', '_elementor_page_settings' ], true ) ) {
$elementor_labels = $this->get_elementor_usage_labels(
$match->ID,
$match->post_title,
$match->post_type,
$match->meta_key,
$terms,
$attachment_id,
$is_font
);
if ( ! empty( $elementor_labels ) ) {
foreach ( $elementor_labels as $elementor_label ) {
$this->add_usage_label( $used_in, $elementor_label );
}
} else {
$this->add_usage_label(
$used_in,
sprintf( 'Elementor: %s (%s)', $match->post_title ?: '(no title)', $match->post_type )
);
}
} else {
$this->add_usage_label(
$used_in,
sprintf( 'Meta (%s): %s (%s)', $match->meta_key, $match->post_title ?: '(no title)', $match->post_type )
);
}
}
if ( $is_font ) {
$option_matches = $this->search_options_for_terms( $terms );
foreach ( $option_matches as $option_name ) {
$this->add_usage_label( $used_in, 'Option: ' . $option_name );
}
}
$woo_gallery = $wpdb->get_results(
$wpdb->prepare(
"SELECT p.ID, p.post_title, pm.meta_value
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
WHERE pm.meta_key = '_product_image_gallery'
AND pm.meta_value LIKE %s
AND p.post_status NOT IN ('trash', 'auto-draft')",
'%' . $wpdb->esc_like( (string) $attachment_id ) . '%'
)
);
foreach ( $woo_gallery as $post ) {
$ids = array_map( 'intval', array_filter( explode( ',', (string) $post->meta_value ) ) );
if ( in_array( $attachment_id, $ids, true ) ) {
$this->add_usage_label( $used_in, 'Product Gallery: ' . ( $post->post_title ?: '(no title)' ) );
}
}
return array_values( array_unique( $used_in ) );
}
private function get_thumb_url( $attachment_id, $filename = '' ) {
$img_src = wp_get_attachment_image_src( $attachment_id, 'thumbnail' );
$thumb = $img_src ? $img_src[0] : '';
if ( $thumb ) {
return esc_url_raw( $thumb );
}
$filetype = wp_check_filetype( $filename );
$ext = isset( $filetype['ext'] ) ? strtolower( $filetype['ext'] ) : '';
if ( in_array( $ext, [ 'ttf', 'woff', 'woff2', 'eot', 'otf' ], true ) ) {
return includes_url( 'images/media/default.png' );
}
if ( $ext && file_exists( ABSPATH . WPINC . '/images/media/' . $ext . '.png' ) ) {
return includes_url( 'images/media/' . $ext . '.png' );
}
return includes_url( 'images/media/default.png' );
}
private function get_ajax_error_message( $exception, $fallback ) {
$message = $fallback;
if ( $exception instanceof Throwable ) {
$raw = trim( wp_strip_all_tags( $exception->getMessage() ) );
if ( '' !== $raw ) {
$message = $raw;
}
}
return $message;
}
private function verify_ajax_request() {
nocache_headers();
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => __( 'Permission denied.', 'angie-snippets' ) ], 403 );
}
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), self::NONCE_ACTION ) ) {
wp_send_json_error( [ 'message' => __( 'Security check failed. Please refresh the page and try again.', 'angie-snippets' ) ], 403 );
}
}
public function ajax_scan_media() {
try {
@set_time_limit( 120 ); // Extend execution time to avoid timeouts on large libraries
$this->verify_ajax_request();
$offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0;
$limit = isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : self::BATCH_SIZE;
$from_date = isset( $_POST['date_from'] ) ? sanitize_text_field( wp_unslash( $_POST['date_from'] ) ) : '';
$to_date = isset( $_POST['date_to'] ) ? sanitize_text_field( wp_unslash( $_POST['date_to'] ) ) : '';
$search_term = isset( $_POST['search_term'] ) ? $this->normalize_search_term( wp_unslash( $_POST['search_term'] ) ) : '';
$search_mode = isset( $_POST['search_mode'] ) ? $this->normalize_search_mode( wp_unslash( $_POST['search_mode'] ) ) : 'contains';
$total = $this->get_total_attachments( $from_date, $to_date, $search_term, $search_mode );
$attachments = $this->get_attachment_batch( $offset, $limit, $from_date, $to_date, $search_term, $search_mode );
$custom_font_files = $this->get_elementor_custom_font_map();
$results = [];
foreach ( $attachments as $attachment ) {
$filename = $this->get_attachment_filename( $attachment->ID );
$file_path = get_attached_file( $attachment->ID );
$file_size = ( $file_path && file_exists( $file_path ) ) ? size_format( filesize( $file_path ) ) : 'Unknown';
$used_in = $this->get_attachment_usage( $attachment->ID, $custom_font_files );
$results[] = [
'id' => (int) $attachment->ID,
'title' => $attachment->post_title ?: '(no title)',
'filename' => $filename ?: '(unknown file)',
'thumbnail' => $this->get_thumb_url( $attachment->ID, $filename ),
'date' => date_i18n( get_option( 'date_format' ), strtotime( $attachment->post_date ) ),
'size' => $file_size,
'status' => empty( $used_in ) ? 'unused' : 'used',
'used_in' => $used_in,
];
}
$next_offset = $offset + count( $attachments );
$complete = $next_offset >= $total || empty( $attachments );
wp_send_json_success(
[
'items' => $results,
'offset' => $offset,
'next_offset' => $next_offset,
'total' => $total,
'complete' => $complete,
]
);
} catch ( Throwable $e ) {
wp_send_json_error( [ 'message' => $this->get_ajax_error_message( $e, __( 'Scan failed on the server.', 'angie-snippets' ) ) ], 500 );
}
}
public function ajax_export_zip() {
try {
@set_time_limit( 120 ); // Extend execution time to avoid timeouts on large exports
$this->verify_ajax_request();
if ( ! class_exists( 'ZipArchive' ) ) {
wp_send_json_error( [ 'message' => __( 'ZipArchive is not available on this server.', 'angie-snippets' ) ], 500 );
}
$ids = isset( $_POST['ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['ids'] ) ) : [];
$ids = array_values( array_filter( $ids ) );
if ( empty( $ids ) ) {
wp_send_json_error( [ 'message' => __( 'No media items were selected.', 'angie-snippets' ) ], 400 );
}
$upload_dir = wp_upload_dir();
if ( ! empty( $upload_dir['error'] ) || empty( $upload_dir['basedir'] ) || empty( $upload_dir['baseurl'] ) ) {
wp_send_json_error( [ 'message' => __( 'Uploads directory is not available.', 'angie-snippets' ) ], 500 );
}
$backup_dir = trailingslashit( $upload_dir['basedir'] ) . 'media-usage-scanner-backups';
if ( ! wp_mkdir_p( $backup_dir ) ) {
wp_send_json_error( [ 'message' => __( 'Could not create the backup folder in uploads.', 'angie-snippets' ) ], 500 );
}
$stamp = gmdate( 'Y-m-d-H-i-s' );
$zip_filename = 'media-usage-delete-backup-' . $stamp . '-' . wp_generate_password( 6, false, false ) . '.zip';
$zip_path = trailingslashit( $backup_dir ) . $zip_filename;
$zip_url = trailingslashit( $upload_dir['baseurl'] ) . 'media-usage-scanner-backups/' . $zip_filename;
$zip = new ZipArchive();
if ( true !== $zip->open( $zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) ) {
wp_send_json_error( [ 'message' => __( 'Could not create the ZIP file.', 'angie-snippets' ) ], 500 );
}
$added_count = 0;
$missing_ids = [];
$used_names = [];
foreach ( $ids as $id ) {
$id = (int) $id;
if ( get_post_type( $id ) !== 'attachment' ) {
$missing_ids[] = $id;
continue;
}
$file_path = get_attached_file( $id );
if ( ! $file_path || ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
$missing_ids[] = $id;
continue;
}
$entry = wp_basename( $file_path );
if ( isset( $used_names[ $entry ] ) ) {
$pathinfo = pathinfo( $entry );
$name = isset( $pathinfo['filename'] ) ? $pathinfo['filename'] : 'file';
$ext = isset( $pathinfo['extension'] ) ? '.' . $pathinfo['extension'] : '';
$entry = $name . '-' . $id . $ext;
}
$used_names[ $entry ] = true;
if ( $zip->addFile( $file_path, $entry ) ) {
$added_count++;
} else {
$missing_ids[] = $id;
}
}
$zip->close();
if ( ! file_exists( $zip_path ) ) {
wp_send_json_error( [ 'message' => __( 'ZIP file was not created successfully.', 'angie-snippets' ) ], 500 );
}
wp_send_json_success(
[
'download_url' => esc_url_raw( $zip_url ),
'filename' => $zip_filename,
'added_count' => $added_count,
'missing_ids' => $missing_ids,
]
);
} catch ( Throwable $e ) {
wp_send_json_error( [ 'message' => $this->get_ajax_error_message( $e, __( 'ZIP export failed on the server.', 'angie-snippets' ) ) ], 500 );
}
}
public function ajax_drop_zip() {
try {
$this->verify_ajax_request();
$filename = isset( $_POST['filename'] ) ? sanitize_file_name( wp_unslash( $_POST['filename'] ) ) : '';
if ( '' === $filename ) {
wp_send_json_error( [ 'message' => __( 'No filename provided.', 'angie-snippets' ) ], 400 );
}
// Only allow filenames that match our own ZIP naming pattern for safety
if ( ! preg_match( '/^media-usage-delete-backup-[\d\-]+-[a-zA-Z0-9]+\.zip$/', $filename ) ) {
wp_send_json_error( [ 'message' => __( 'Invalid filename.', 'angie-snippets' ) ], 400 );
}
$upload_dir = wp_upload_dir();
$zip_path = trailingslashit( $upload_dir['basedir'] ) . 'media-usage-scanner-backups/' . $filename;
// Resolve real path and confirm it's inside the backup folder
$real_zip = realpath( $zip_path );
$real_backup = realpath( trailingslashit( $upload_dir['basedir'] ) . 'media-usage-scanner-backups' );
if ( ! $real_zip || ! $real_backup || strpos( $real_zip, $real_backup ) !== 0 ) {
wp_send_json_error( [ 'message' => __( 'File not found or access denied.', 'angie-snippets' ) ], 403 );
}
if ( @unlink( $real_zip ) ) {
wp_send_json_success( [ 'message' => __( 'ZIP file deleted from server.', 'angie-snippets' ) ] );
} else {
wp_send_json_error( [ 'message' => __( 'Could not delete the ZIP file.', 'angie-snippets' ) ], 500 );
}
} catch ( Throwable $e ) {
wp_send_json_error( [ 'message' => $this->get_ajax_error_message( $e, __( 'ZIP cleanup failed.', 'angie-snippets' ) ) ], 500 );
}
}
public function ajax_delete_media() {
try {
@set_time_limit( 120 ); // Extend execution time to avoid timeouts on large deletions
$this->verify_ajax_request();
$ids = isset( $_POST['ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['ids'] ) ) : [];
$ids = array_values( array_filter( $ids ) );
$deleted_count = 0;
$skipped = [];
$custom_font_files = $this->get_elementor_custom_font_map();
foreach ( $ids as $id ) {
$id = (int) $id;
if ( get_post_type( $id ) !== 'attachment' ) {
$skipped[] = $id;
continue;
}
if ( $id === (int) get_theme_mod( 'custom_logo' ) || $id === (int) get_option( 'site_icon' ) ) {
$skipped[] = $id;
continue;
}
$used_in = $this->get_attachment_usage( $id, $custom_font_files );
if ( ! empty( $used_in ) ) {
$skipped[] = $id;
continue;
}
if ( wp_delete_attachment( $id, true ) ) {
$deleted_count++;
} else {
$skipped[] = $id;
}
}
wp_send_json_success(
[
'deleted' => $deleted_count,
'skipped' => $skipped,
]
);
} catch ( Throwable $e ) {
wp_send_json_error( [ 'message' => $this->get_ajax_error_message( $e, __( 'Delete failed on the server.', 'angie-snippets' ) ) ], 500 );
}
}
private function get_inline_css() {
return <<<CSS
.media-scanner-wrap{
max-width:1240px;
margin:20px auto;
background:#fff;
padding:20px;
box-shadow:0 1px 3px rgba(0,0,0,.1);
}
.angie-scanner-actions{
margin:20px 0;
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:16px;
flex-wrap:wrap;
}
.angie-scan-filters{
display:flex;
gap:12px;
flex-wrap:wrap;
align-items:flex-end;
}
.angie-scan-filters label{
display:flex;
flex-direction:column;
gap:6px;
font-size:12px;
color:#50575e;
}
.angie-scan-filters input[type="date"]{
min-width:170px;
}
.angie-search-pane{
display:flex;
gap:12px;
flex-wrap:wrap;
align-items:flex-end;
}
.angie-search-pane input[type="search"]{
min-width:260px;
}
.angie-search-pane select{
min-width:150px;
}
.angie-scan-buttons{
display:flex;
align-items:center;
gap:10px;
flex-wrap:wrap;
}
.scan-progress-media-scanner{
font-size:12px;
color:#50575e;
min-height:18px;
}
.nav-tab-wrapper{
margin-bottom:15px;
}
.wp-list-table.media .check-column{
width:34px;
}
.wp-list-table.media .column-thumbnail{
width:88px;
}
.wp-list-table.media .column-title{
width:320px;
}
.wp-list-table.media .column-status{
width:95px;
white-space:nowrap;
}
.wp-list-table.media td.column-status,
.wp-list-table.media th.column-status{
text-align:left;
padding-left:12px;
padding-top:14px;
vertical-align:top;
}
.wp-list-table.media .column-status .angie-badge{
margin-left:0;
}
.wp-list-table.media .column-used-in{
width:auto;
}
.wp-list-table.media td.column-used-in,
.wp-list-table.media th.column-used-in{
padding-top:14px;
vertical-align:top;
}
.wp-list-table.media td.column-used-in .description{
display:block;
margin:0;
line-height:1.45;
}
.media-icon-preview{
width:60px;
height:60px;
display:flex;
align-items:center;
justify-content:center;
background:#f0f0f1;
border:1px solid #c3c4c7;
overflow:hidden;
}
.media-icon-preview img{
max-width:100%;
max-height:100%;
height:auto;
width:auto;
display:block;
}
.filename{
color:#666;
font-size:12px;
font-family:monospace;
word-break:break-word;
}
.meta{
color:#888;
font-size:11px;
}
.angie-badge{
display:inline-block;
padding:4px 8px;
border-radius:4px;
font-size:11px;
font-weight:600;
text-transform:uppercase;
}
.angie-badge.used{
background:#e5f5fa;
color:#0085ba;
border:1px solid #bce0ee;
}
.angie-badge.unused{
background:#fbeaea;
color:#d63638;
border:1px solid #f2cfcf;
}
.used-in-list{
margin:0;
padding-left:16px;
font-size:12px;
line-height:1.45;
color:#50575e;
}
.used-in-list li{
margin-bottom:2px;
word-break:break-word;
}
.spinner.is-active{
float:none;
margin:0;
}
.angie-results-note{
margin:10px 0 0;
}
#scan-results-body-media-scanner td,
#scan-results-body-media-scanner th{
vertical-align:top;
}
.wp-list-table.media td.column-used-in .description{
display:block;
margin:0;
padding-left:16px;
line-height:1.45;
}
CSS;
}
private function get_inline_js() {
return <<<'JS'
document.addEventListener('DOMContentLoaded', function() {
const cfg = window.angie_media_scanner || {};
const scanBtn = document.getElementById('scan-media-btn-media-scanner');
const deleteBtn = document.getElementById('delete-selected-btn-media-scanner');
const resultsContainer = document.getElementById('scan-results-container-media-scanner');
const resultsBody = document.getElementById('scan-results-body-media-scanner');
const resultCount = document.getElementById('result-count-media-scanner');
const selectAllCb = document.getElementById('cb-select-all-1');
const spinner = document.querySelector('.angie-scanner-actions .spinner');
const tabs = document.querySelectorAll('.nav-tab-wrapper .nav-tab');
const note = document.querySelector('.angie-results-note');
const progress = document.getElementById('scan-progress-media-scanner');
const dateFrom = document.getElementById('scan-date-from-media-scanner');
const dateTo = document.getElementById('scan-date-to-media-scanner');
const searchTerm = document.getElementById('scan-search-term-media-scanner');
const searchMode = document.getElementById('scan-search-mode-media-scanner');
let allMediaData = [];
let currentTab = 'all';
let currentFilters = { date_from: '', date_to: '', search_term: '', search_mode: 'contains' };
let isScanning = false;
if (!scanBtn || !resultsBody || !selectAllCb || !deleteBtn) return;
if (note) {
note.textContent = cfg.results_notice || '';
}
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function getAjaxUrl() {
const direct = typeof cfg.ajax_url === 'string' ? cfg.ajax_url.trim() : '';
const globalAjax = typeof window.ajaxurl === 'string' ? window.ajaxurl.trim() : '';
const fallback = typeof cfg.ajax_url_fallback === 'string' ? cfg.ajax_url_fallback.trim() : '';
return direct || globalAjax || fallback;
}
function getSelectedItems() {
const checked = document.querySelectorAll('.media-cb:checked');
const ids = Array.from(checked).map(cb => String(cb.value));
return allMediaData.filter(item => ids.includes(String(item.id)));
}
function triggerBrowserDownload(url, filenameHint) {
const a = document.createElement('a');
a.href = url;
if (filenameHint) {
a.setAttribute('download', filenameHint);
}
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
async function parseAjaxResponse(res) {
const rawText = await res.text();
let json = null;
try {
json = rawText ? JSON.parse(rawText) : null;
} catch (e) {
if (!res.ok) {
throw new Error((cfg.http_error || 'The server returned an unexpected HTTP status.') + ' ' + res.status + '. ' + rawText.slice(0, 180));
}
throw new Error((cfg.invalid_json || 'The server did not return valid JSON.') + ' ' + rawText.slice(0, 180));
}
if (!res.ok) {
const serverMessage = json && json.data && typeof json.data.message === 'string' ? json.data.message : '';
throw new Error(((cfg.http_error || 'The server returned an unexpected HTTP status.') + ' ' + res.status + '. ' + serverMessage).trim());
}
return json;
}
async function postAjax(params) {
const ajaxUrl = getAjaxUrl();
if (!ajaxUrl) {
throw new Error('admin-ajax.php URL is missing.');
}
let res;
try {
res = await fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'Cache-Control': 'no-cache'
},
body: params.toString(),
credentials: 'same-origin',
cache: 'no-store'
});
} catch (err) {
throw new Error((cfg.network_error || 'Request could not reach admin-ajax.php.') + ' ' + (err && err.message ? err.message : ''));
}
return parseAjaxResponse(res);
}
async function exportZip(items) {
const ids = items.map(item => String(item.id));
const params = new URLSearchParams();
params.append('action', cfg.export_action);
params.append('nonce', cfg.nonce);
ids.forEach(id => params.append('ids[]', id));
const json = await postAjax(params);
if (!json || !json.success || !json.data || !json.data.download_url) {
throw new Error((json && json.data && json.data.message) ? json.data.message : (cfg.unknown_error || 'Unknown error'));
}
triggerBrowserDownload(json.data.download_url, json.data.filename || '');
// Clean up the ZIP from the server after triggering the download
if (json.data.filename) {
try {
const dropParams = new URLSearchParams();
dropParams.append('action', cfg.drop_zip_action);
dropParams.append('nonce', cfg.nonce);
dropParams.append('filename', json.data.filename);
await postAjax(dropParams);
} catch (e) {
// Non-critical: ZIP cleanup failed silently, file stays on server
console.warn('ZIP cleanup failed:', e);
}
}
return json.data;
}
function renderResults() {
resultsContainer.style.display = 'block';
resultsBody.innerHTML = '';
const filtered = allMediaData.filter(item => {
if (currentTab === 'unused') return item.status === 'unused';
if (currentTab === 'used') return item.status === 'used';
return true;
});
resultCount.textContent = filtered.length;
if (currentTab === 'unused') {
deleteBtn.style.display = 'inline-block';
selectAllCb.disabled = false;
} else {
deleteBtn.style.display = 'none';
selectAllCb.disabled = true;
selectAllCb.checked = false;
}
if (filtered.length === 0) {
resultsBody.innerHTML = '<tr><td colspan="5">No items found in this view.</td></tr>';
updateDeleteButton();
return;
}
filtered.forEach(item => {
const isUsed = item.status === 'used';
const statusBadge = isUsed
? '<span class="angie-badge used">Used</span>'
: '<span class="angie-badge unused">Unused</span>';
const checkboxHtml = !isUsed
? '<input type="checkbox" name="media[]" value="' + escapeHtml(item.id) + '" class="media-cb">'
: '<input type="checkbox" disabled>';
let usedInList = '';
if (item.used_in && item.used_in.length > 0) {
usedInList = '<ul class="used-in-list">';
item.used_in.forEach(place => {
usedInList += '<li>' + escapeHtml(place) + '</li>';
});
usedInList += '</ul>';
} else {
usedInList = '<span class="description">Not found in content analysis.</span>';
}
const tr = document.createElement('tr');
tr.innerHTML =
'<th scope="row" class="check-column">' + checkboxHtml + '</th>' +
'<td class="column-thumbnail">' +
'<div class="media-icon-preview">' +
'<img src="' + escapeHtml(item.thumbnail) + '" alt="">' +
'</div>' +
'</td>' +
'<td class="column-title">' +
'<strong>' + escapeHtml(item.title) + '</strong><br>' +
'<span class="filename">' + escapeHtml(item.filename) + '</span><br>' +
'<span class="meta">' + escapeHtml(item.date) + ' • ' + escapeHtml(item.size) + '</span>' +
'</td>' +
'<td class="column-status">' + statusBadge + '</td>' +
'<td class="column-used-in">' + usedInList + '</td>';
resultsBody.appendChild(tr);
});
updateDeleteButton();
}
function updateDeleteButton() {
const checked = document.querySelectorAll('.media-cb:checked');
deleteBtn.disabled = checked.length === 0;
deleteBtn.textContent = checked.length > 0
? 'Export ZIP + Delete Selected (' + checked.length + ')'
: 'Export ZIP + Delete Selected';
}
async function fetchBatch(offset) {
const params = new URLSearchParams();
params.append('action', cfg.scan_action);
params.append('nonce', cfg.nonce);
params.append('offset', String(offset));
params.append('limit', String(cfg.batch_size || 10));
params.append('date_from', currentFilters.date_from || '');
params.append('date_to', currentFilters.date_to || '');
params.append('search_term', currentFilters.search_term || '');
params.append('search_mode', currentFilters.search_mode || 'contains');
return postAjax(params);
}
async function runScan() {
if (isScanning) return;
isScanning = true;
allMediaData = [];
currentTab = 'all';
currentFilters = {
date_from: dateFrom ? dateFrom.value : '',
date_to: dateTo ? dateTo.value : '',
search_term: searchTerm ? searchTerm.value.trim() : '',
search_mode: searchMode ? searchMode.value : 'contains'
};
tabs.forEach(t => t.classList.remove('nav-tab-active'));
const allTab = document.querySelector('.nav-tab[data-tab="all"]');
if (allTab) allTab.classList.add('nav-tab-active');
scanBtn.disabled = true;
deleteBtn.disabled = true;
selectAllCb.checked = false;
resultsContainer.style.display = 'none';
if (spinner) {
spinner.classList.add('is-active');
}
if (progress) {
progress.textContent = cfg.scanning || 'Scanning...';
}
let offset = 0;
let total = 0;
let complete = false;
try {
while (!complete) {
const res = await fetchBatch(offset);
if (!res || !res.success || !res.data) {
throw new Error((res && res.data && res.data.message) ? res.data.message : (cfg.unknown_error || 'Unknown error'));
}
const data = res.data;
total = parseInt(data.total || 0, 10);
offset = parseInt(data.next_offset || 0, 10);
complete = !!data.complete;
if (Array.isArray(data.items) && data.items.length) {
allMediaData = allMediaData.concat(data.items);
}
renderResults();
if (progress) {
progress.textContent = (cfg.scanning || 'Scanning...') + ' ' + allMediaData.length + ' / ' + total;
}
}
if (progress) {
progress.textContent = (cfg.scan_complete || 'Scan complete.') + ' ' + allMediaData.length + ' / ' + total;
}
} catch (err) {
console.error(err);
alert((cfg.scan_failed || 'Scan failed.') + ' ' + (err && err.message ? err.message : ''));
if (progress) {
progress.textContent = err && err.message ? err.message : '';
}
} finally {
isScanning = false;
scanBtn.disabled = false;
if (spinner) {
spinner.classList.remove('is-active');
}
renderResults();
}
}
scanBtn.addEventListener('click', function() {
runScan();
});
[dateFrom, dateTo, searchTerm, searchMode].forEach(function(field) {
if (!field) return;
field.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !isScanning) {
e.preventDefault();
runScan();
}
});
});
tabs.forEach(tab => {
tab.addEventListener('click', function(e) {
e.preventDefault();
tabs.forEach(t => t.classList.remove('nav-tab-active'));
this.classList.add('nav-tab-active');
currentTab = this.dataset.tab || 'all';
renderResults();
});
});
selectAllCb.addEventListener('change', function() {
const cbs = document.querySelectorAll('.media-cb:not(:disabled)');
cbs.forEach(cb => cb.checked = selectAllCb.checked);
updateDeleteButton();
});
resultsBody.addEventListener('change', function(e) {
if (e.target.classList.contains('media-cb')) {
updateDeleteButton();
}
});
deleteBtn.addEventListener('click', async function() {
if (!confirm(cfg.confirm || 'Are you sure?')) return;
const items = getSelectedItems();
if (!items.length) return;
const DELETE_BATCH_SIZE = 5;
try {
if (progress) {
progress.textContent = cfg.preparing_zip || 'Preparing ZIP backup...';
}
const exportData = await exportZip(items);
const ids = items.map(item => String(item.id));
deleteBtn.disabled = true;
// Split ids into batches of DELETE_BATCH_SIZE to avoid server timeouts
let totalDeleted = 0;
let totalSkipped = [];
for (let i = 0; i < ids.length; i += DELETE_BATCH_SIZE) {
const batchIds = ids.slice(i, i + DELETE_BATCH_SIZE);
const processed = i + batchIds.length;
deleteBtn.textContent = (cfg.deleting || 'Deleting...') + ' ' + processed + ' / ' + ids.length;
if (progress) {
progress.textContent = (cfg.deleting || 'Deleting...') + ' ' + processed + ' / ' + ids.length;
}
const params = new URLSearchParams();
params.append('action', cfg.delete_action);
params.append('nonce', cfg.nonce);
batchIds.forEach(id => params.append('ids[]', id));
const json = await postAjax(params);
if (!json || !json.success) {
throw new Error((json && json.data && json.data.message) ? json.data.message : (cfg.unknown_error || 'Unknown error'));
}
totalDeleted += parseInt(json.data.deleted || 0, 10);
if (Array.isArray(json.data.skipped)) {
totalSkipped = totalSkipped.concat(json.data.skipped.map(String));
}
}
allMediaData = allMediaData.filter(item => totalSkipped.includes(String(item.id)) || !ids.includes(String(item.id)));
renderResults();
let message = 'ZIP backup downloaded. Successfully deleted ' + totalDeleted + ' items.';
if (exportData && typeof exportData.added_count !== 'undefined') {
message = 'ZIP backup prepared with ' + exportData.added_count + ' file' + (parseInt(exportData.added_count, 10) === 1 ? '' : 's') + '. Successfully deleted ' + totalDeleted + ' items.';
}
if (totalSkipped.length) {
message += ' ' + totalSkipped.length + ' items were skipped because usage was detected or deletion failed.';
}
alert(message);
if (progress) {
progress.textContent = '';
}
} catch (err) {
console.error(err);
alert((cfg.delete_failed || 'Delete failed.') + ' ' + (err && err.message ? err.message : ''));
if (progress) {
progress.textContent = err && err.message ? err.message : '';
}
updateDeleteButton();
}
});
});
JS;
}
}
new Media_Scanner();
}Utilizando o Media Scanner
Depois do snippet criado, irá aparecer o menu Media Scanner abaixo de Biblioteca de Mídia. Clique em Media Scanner, escolha o período e clique no botão para fazer o scaneamento. Se você desejar que o scaneamento seja feito em todo a biblioteca, basta clicar no botão deixando o período em branco.
Conclusão
Agora com o Media Scanner ficou bem fácil manter o seu WordPress sempre “enxuto”, evitando imagens e arquivos desnecessários. Espero que esse código tenha te ajudado. Até a próxima.





