Hi Iftiar,
As promised, below is the reference implementation I mentioned.
This is an earlier implementation I built for my own WordPress projects. The main idea behind it was to be conservative about what counts as a media reference:
- A known, explicit media reference is treated as a valid reference.
- A plain numeric value that happens to match an attachment ID is NOT considered a reference by itself.
- URLs and actual file paths are treated as references when they are found in supported locations.
- WooCommerce featured images and product galleries are checked explicitly.
- Elementor
_elementor_data is checked for actual image URLs/paths, including JSON-escaped URLs.
- WordPress
wp-image-ID references and [gallery ids="..."] are checked explicitly.
- Generic post meta is only considered a reference when an actual URL/path is found; arbitrary numeric IDs are ignored.
- The attachment’s own metadata and the post being deleted are excluded from the reference search.
I originally built this mainly to solve false-positive media deletion in WooCommerce, Elementor, and custom-field-heavy sites.
One important caveat: this implementation was designed for correctness of the reference logic rather than large-scale performance. In particular, the content/meta searches can become expensive when executed synchronously for many attachments on a large site. That performance concern was one of the reasons I started looking for a dedicated bulk-cleanup solution instead of continuing with this implementation.
So please consider the code below as a reference for the detection logic and test cases, rather than as a production-ready scanner.
I hope it gives you some useful ideas for 2.2.0 and future versions.
<?php
/**
* Sedaghat Smart Media Delete
*
* Purpose:
* When deleting a Post, delete its attached Attachments only when
* no other valid and meaningful references to those Attachments exist.
*
* Scope:
* - WordPress
* - WooCommerce
* - Woodmart
* - Elementor / Elementor Pro
* - Custom Fields
*
* Valid references:
* - _thumbnail_id
* - _product_image_gallery
* - wp-image-ID in post_content
* - [gallery ids="..."] in post_content
* - Actual image URL
* - Actual file path
* - Actual URL / Path inside Elementor _elementor_data
*
* A plain numeric value inside a Custom Field is never considered
* a valid media reference by itself.
*/
add_action(
'before_delete_post',
'sedaghat_smart_delete_attached_media',
10,
2
);
function sedaghat_smart_delete_attached_media( $post_id, $post ) {
if ( ! $post instanceof WP_Post ) {
return;
}
if ( 'attachment' === $post->post_type ) {
return;
}
$attachments = get_attached_media( '', $post_id );
if ( empty( $attachments ) ) {
return;
}
foreach ( $attachments as $attachment ) {
$attachment_id = (int) $attachment->ID;
if ( $attachment_id <= 0 ) {
continue;
}
if ( ! sedaghat_attachment_has_other_references( $attachment_id, $post_id ) ) {
wp_delete_attachment( $attachment_id, true );
}
}
}
/**
* Check whether an Attachment has any valid reference elsewhere.
*/
function sedaghat_attachment_has_other_references(
$attachment_id,
$deleting_post_id
) {
$attachment_id = (int) $attachment_id;
$deleting_post_id = (int) $deleting_post_id;
if ( $attachment_id <= 0 ) {
return false;
}
$references = sedaghat_get_attachment_reference_data( $attachment_id );
/**
* 1. Actual URL / Path inside post_content
*/
if (
sedaghat_attachment_used_in_post_content(
$references,
$deleting_post_id
)
) {
return true;
}
/**
* 2. Known ID-based references inside post_content
*
* - wp-image-ID
* - [gallery ids="..."]
*/
if (
sedaghat_attachment_id_used_in_post_content(
$attachment_id,
$deleting_post_id
)
) {
return true;
}
/**
* 3. Elementor references
*
* Elementor stores page data in _elementor_data as JSON.
* Image URLs are commonly stored as JSON-escaped URLs.
*/
if (
sedaghat_attachment_used_in_elementor_data(
$references,
$deleting_post_id
)
) {
return true;
}
/**
* 4. Other valid postmeta references
*
* Only actual URLs / Paths are considered here.
* A plain numeric value or raw ID is never considered a reference.
*/
if (
sedaghat_attachment_used_in_postmeta(
$attachment_id,
$references,
$deleting_post_id
)
) {
return true;
}
return false;
}
/**
* Build actual reference data for an Attachment.
*
* Includes:
* - Original URL
* - Original file path
* - Original attachment metadata
* - Generated image-size files
*/
function sedaghat_get_attachment_reference_data( $attachment_id ) {
$urls = array();
$paths = array();
$attachment_id = (int) $attachment_id;
if ( $attachment_id <= 0 ) {
return array(
'urls' => array(),
'paths' => array(),
);
}
/**
* Original Attachment URL
*/
$attachment_url = wp_get_attachment_url( $attachment_id );
if ( is_string( $attachment_url ) && '' !== $attachment_url ) {
$urls[] = $attachment_url;
}
/**
* Original file path
*/
$attached_file = get_post_meta(
$attachment_id,
'_wp_attached_file',
true
);
if ( is_string( $attached_file ) && '' !== $attached_file ) {
$paths[] = $attached_file;
$upload_dir = wp_get_upload_dir();
if (
! empty( $upload_dir['baseurl'] ) &&
! empty( $upload_dir['basedir'] )
) {
$paths[] = wp_normalize_path( $attached_file );
$urls[] = trailingslashit( $upload_dir['baseurl'] )
. ltrim(
str_replace(
'\\',
'/',
$attached_file
),
'/'
);
}
}
/**
* Attachment Metadata
*/
$metadata = wp_get_attachment_metadata( $attachment_id );
if ( is_array( $metadata ) ) {
$upload_dir = wp_get_upload_dir();
/**
* Original file from metadata
*/
if (
! empty( $metadata['file'] ) &&
is_string( $metadata['file'] )
) {
$metadata_file = wp_normalize_path(
$metadata['file']
);
$paths[] = $metadata_file;
if ( ! empty( $upload_dir['baseurl'] ) ) {
$urls[] = trailingslashit(
$upload_dir['baseurl']
)
. ltrim(
$metadata_file,
'/'
);
}
}
/**
* Generated image-size files
*/
if (
! empty( $metadata['sizes'] ) &&
is_array( $metadata['sizes'] ) &&
! empty( $metadata['file'] )
) {
$directory = dirname(
wp_normalize_path(
$metadata['file']
)
);
if ( '.' === $directory ) {
$directory = '';
}
foreach ( $metadata['sizes'] as $size ) {
if (
empty( $size['file'] ) ||
! is_string( $size['file'] )
) {
continue;
}
$size_file = wp_normalize_path(
$size['file']
);
$relative_path = '';
if ( '' !== $directory ) {
$relative_path = trailingslashit(
$directory
)
. ltrim(
$size_file,
'/'
);
} else {
$relative_path = $size_file;
}
$paths[] = $relative_path;
if ( ! empty( $upload_dir['baseurl'] ) ) {
$urls[] = trailingslashit(
$upload_dir['baseurl']
)
. ltrim(
$relative_path,
'/'
);
}
}
}
}
/**
* Normalize URLs
*/
$urls = array_values(
array_unique(
array_filter(
array_map(
'esc_url_raw',
$urls
)
)
)
);
/**
* Normalize Paths
*/
$paths = array_values(
array_unique(
array_filter(
array_map(
function ( $path ) {
return wp_normalize_path(
str_replace(
'\\',
'/',
$path
)
);
},
$paths
)
)
)
);
return array(
'urls' => $urls,
'paths' => $paths,
);
}
/**
* Check for actual URL / Path references inside post_content.
*/
function sedaghat_attachment_used_in_post_content(
$references,
$deleting_post_id
) {
global $wpdb;
$deleting_post_id = (int) $deleting_post_id;
$search_values = array();
if ( ! empty( $references['urls'] ) ) {
$search_values = array_merge(
$search_values,
$references['urls']
);
}
if ( ! empty( $references['paths'] ) ) {
$search_values = array_merge(
$search_values,
$references['paths']
);
}
if ( empty( $search_values ) ) {
return false;
}
$conditions = array(
'p.ID <> %d',
"p.post_type <> 'attachment'",
);
$params = array(
$deleting_post_id,
);
foreach ( $search_values as $value ) {
if ( '' === $value ) {
continue;
}
$conditions[] = 'p.post_content LIKE %s';
$params[] = '%'
. $wpdb->esc_like( $value )
. '%';
}
if ( count( $conditions ) <= 2 ) {
return false;
}
$sql = "
SELECT p.ID
FROM {$wpdb->posts} AS p
WHERE "
. implode(
' AND ',
$conditions
)
. "
LIMIT 1
";
$found = $wpdb->get_var(
$wpdb->prepare(
$sql,
$params
)
);
return ! empty( $found );
}
/**
* Check known ID-based references inside post_content.
*
* - wp-image-ID
* - [gallery ids="..."]
*/
function sedaghat_attachment_id_used_in_post_content(
$attachment_id,
$deleting_post_id
) {
global $wpdb;
$attachment_id = (int) $attachment_id;
$deleting_post_id = (int) $deleting_post_id;
if ( $attachment_id <= 0 ) {
return false;
}
/**
* wp-image-ID
*/
$wp_image_class = 'wp-image-' . $attachment_id;
$sql = "
SELECT ID
FROM {$wpdb->posts}
WHERE ID <> %d
AND post_type <> 'attachment'
AND post_content LIKE %s
LIMIT 1
";
$found = $wpdb->get_var(
$wpdb->prepare(
$sql,
$deleting_post_id,
'%'
. $wpdb->esc_like(
$wp_image_class
)
. '%'
)
);
if ( ! empty( $found ) ) {
return true;
}
/**
* [gallery ids="..."]
*/
$gallery_candidates = $wpdb->get_results(
$wpdb->prepare(
"
SELECT ID, post_content
FROM {$wpdb->posts}
WHERE ID <> %d
AND post_type <> 'attachment'
AND post_content LIKE %s
",
$deleting_post_id,
'%[gallery%'
)
);
if ( empty( $gallery_candidates ) ) {
return false;
}
foreach ( $gallery_candidates as $candidate ) {
if ( empty( $candidate->post_content ) ) {
continue;
}
if (
! preg_match_all(
'/\[gallery\b[^\]]*\]/i',
$candidate->post_content,
$matches
)
) {
continue;
}
foreach ( $matches[0] as $shortcode ) {
if (
! preg_match(
'/\bids\s*=\s*(["\'])(.*?)\1/i',
$shortcode,
$id_match
)
) {
continue;
}
$ids = array_map(
'absint',
array_map(
'trim',
explode(
',',
$id_match[2]
)
)
);
if (
in_array(
$attachment_id,
$ids,
true
)
) {
return true;
}
}
}
return false;
}
/**
* Check Elementor references.
*
* Elementor stores page data in:
*
* _elementor_data
*
* as JSON.
*
* Example:
*
* "image":{
* "url":"http:\/\/staging.localhost\/wp-content\/uploads\/2026\/09\/001.jpg",
* "id":10019,
* "source":"library"
* }
*
* A numeric ID alone is NOT checked.
* Only the actual Attachment URL / Path is checked.
*/
function sedaghat_attachment_used_in_elementor_data(
$references,
$deleting_post_id
) {
global $wpdb;
$deleting_post_id = (int) $deleting_post_id;
$search_values = array();
/**
* Actual URLs
*/
if ( ! empty( $references['urls'] ) ) {
foreach ( $references['urls'] as $url ) {
if ( ! is_string( $url ) || '' === $url ) {
continue;
}
/**
* Normal URL
*/
$search_values[] = $url;
/**
* JSON-escaped URL
*
* Example:
* http://example.com/wp-content/...
*
* becomes:
* http:\/\/example.com\/wp-content\/...
*/
$search_values[] = str_replace(
'/',
'\\/',
$url
);
}
}
/**
* Actual Paths
*/
if ( ! empty( $references['paths'] ) ) {
foreach ( $references['paths'] as $path ) {
if ( ! is_string( $path ) || '' === $path ) {
continue;
}
/**
* Normal path
*/
$search_values[] = $path;
/**
* JSON-escaped path
*/
$search_values[] = str_replace(
'/',
'\\/',
$path
);
}
}
$search_values = array_values(
array_unique(
array_filter(
$search_values
)
)
);
if ( empty( $search_values ) ) {
return false;
}
$conditions = array();
$params = array();
foreach ( $search_values as $value ) {
$conditions[] = 'pm.meta_value LIKE %s';
$params[] = '%'
. $wpdb->esc_like( $value )
. '%';
}
/**
* Only _elementor_data is checked here.
*
* This prevents arbitrary numeric IDs or generic Custom Fields
* from being treated as media references.
*/
$sql = "
SELECT pm.meta_id
FROM {$wpdb->postmeta} AS pm
INNER JOIN {$wpdb->posts} AS p
ON p.ID = pm.post_id
WHERE pm.meta_key = '_elementor_data'
AND (
"
. implode(
' OR ',
$conditions
)
. "
)
AND pm.post_id <> %d
AND pm.post_id <> %d
AND NOT (
p.post_type = 'revision'
AND p.post_parent = %d
)
LIMIT 1
";
$params[] = $deleting_post_id;
$params[] = $deleting_post_id;
$params[] = $deleting_post_id;
$found = $wpdb->get_var(
$wpdb->prepare(
$sql,
$params
)
);
return ! empty( $found );
}
/**
* Check other postmeta fields.
*
* Valid references:
* - Actual URL
* - Actual Path
*
* Invalid references:
* - Numeric ID
* - Numeric string
* - JSON containing only a numeric ID
* - Serialized array containing only a numeric ID
*/
function sedaghat_attachment_used_in_postmeta(
$attachment_id,
$references,
$deleting_post_id
) {
global $wpdb;
$attachment_id = (int) $attachment_id;
$deleting_post_id = (int) $deleting_post_id;
if ( $attachment_id <= 0 ) {
return false;
}
/**
* _thumbnail_id
*/
$sql = "
SELECT pm.meta_id
FROM {$wpdb->postmeta} AS pm
INNER JOIN {$wpdb->posts} AS p
ON p.ID = pm.post_id
WHERE pm.meta_key = '_thumbnail_id'
AND pm.meta_value = %s
AND pm.post_id <> %d
AND pm.post_id <> %d
AND NOT (
p.post_type = 'revision'
AND p.post_parent = %d
)
LIMIT 1
";
$found = $wpdb->get_var(
$wpdb->prepare(
$sql,
(string) $attachment_id,
$attachment_id,
$deleting_post_id,
$deleting_post_id
)
);
if ( ! empty( $found ) ) {
return true;
}
/**
* _product_image_gallery
*/
$gallery_pattern = '(^|,)[[:space:]]*'
. preg_quote(
(string) $attachment_id,
'/'
)
. '[[:space:]]*(,|$)';
$sql = "
SELECT pm.meta_id
FROM {$wpdb->postmeta} AS pm
INNER JOIN {$wpdb->posts} AS p
ON p.ID = pm.post_id
WHERE pm.meta_key = '_product_image_gallery'
AND pm.meta_value REGEXP %s
AND pm.post_id <> %d
AND pm.post_id <> %d
AND NOT (
p.post_type = 'revision'
AND p.post_parent = %d
)
LIMIT 1
";
$found = $wpdb->get_var(
$wpdb->prepare(
$sql,
$gallery_pattern,
$attachment_id,
$deleting_post_id,
$deleting_post_id
)
);
if ( ! empty( $found ) ) {
return true;
}
/**
* Actual URL / Path inside other postmeta fields
*/
$search_values = array();
if ( ! empty( $references['urls'] ) ) {
$search_values = array_merge(
$search_values,
$references['urls']
);
}
if ( ! empty( $references['paths'] ) ) {
$search_values = array_merge(
$search_values,
$references['paths']
);
}
if ( empty( $search_values ) ) {
return false;
}
$conditions = array();
$params = array();
foreach ( $search_values as $value ) {
if ( '' === $value ) {
continue;
}
$conditions[] = 'pm.meta_value LIKE %s';
$params[] = '%'
. $wpdb->esc_like( $value )
. '%';
}
if ( empty( $conditions ) ) {
return false;
}
$where = "
(
"
. implode(
' OR ',
$conditions
)
. "
)
AND pm.post_id <> %d
AND pm.post_id <> %d
AND NOT (
p.post_type = 'revision'
AND p.post_parent = %d
)
";
$params[] = $attachment_id;
$params[] = $deleting_post_id;
$params[] = $deleting_post_id;
$sql = "
SELECT pm.meta_id
FROM {$wpdb->postmeta} AS pm
INNER JOIN {$wpdb->posts} AS p
ON p.ID = pm.post_id
WHERE "
. $where
. "
LIMIT 1
";
$found = $wpdb->get_var(
$wpdb->prepare(
$sql,
$params
)
);
return ! empty( $found );
}
Best,
Hamed