` inserted via cross-window drag-bridge,
* plugin importers, or hand-edits with no `wp-image-N` class.
* - Gallery items that store the id in `data-id` / `data-
* attachment-id`.
* - Classic-editor `[caption id="attachment_N"]` shortcodes
* that render through `the_content` without re-emitting an
* `wp-image-N` class.
*
* Server has the full post object + WP core's URL→id resolver, so
* one round-trip per detail-view fetches an authoritative list.
*
* Filterable via `desktop_mode_my_wordpress_attached_media` for
* plugins (page builders, ACF, custom field handlers) to append
* their own references.
*
* @package WPDesktopMode
* @since 0.21.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Register `desktop_mode_attached_media` on every public post type.
*
* @since 0.21.0
*/
function desktop_mode_my_wordpress_register_attached_media_field() {
$types = get_post_types(
array(
'show_in_rest' => true,
'public' => true,
),
'names'
);
foreach ( $types as $type ) {
if ( 'attachment' === $type ) {
continue;
}
register_rest_field(
$type,
'desktop_mode_attached_media',
array(
'get_callback' => static function ( $post ) {
$id = isset( $post['id'] ) ? (int) $post['id'] : 0;
return desktop_mode_my_wordpress_post_attached_media( $id );
},
'schema' => array(
'description' => __( 'Attachment ids referenced by this post — featured image plus every attachment found in post_content (block-class scan + raw `
` URL resolution).', 'desktop-mode' ),
'type' => 'array',
'items' => array( 'type' => 'integer' ),
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
)
);
}
}
add_action( 'rest_api_init', 'desktop_mode_my_wordpress_register_attached_media_field' );
/**
* Resolve every attachment referenced by `$post_id`. Featured
* image + a multi-pass scan of `post_content`:
*
* 1. `wp-image-N` class on any element.
* 2. `id="attachment_N"` (classic-editor caption shortcode).
* 3. `data-id="N"` and `data-attachment-id="N"` (gallery
* formats, Jetpack tiled-gallery, several SEO plugins).
* 4. Raw `
` URLs resolved via `attachment_url_to_postid()`.
* Resolver results are cached in-process so a post embedding
* the same image twice only takes one DB hit per URL.
*
* @since 0.21.0
*
* @param int $post_id Post id.
* @return int[] Attachment ids, deduped, no order guarantee.
*/
function desktop_mode_my_wordpress_post_attached_media( $post_id ) {
$post_id = (int) $post_id;
if ( $post_id <= 0 ) {
return array();
}
$post = get_post( $post_id );
if ( ! $post ) {
return array();
}
$ids = array();
// Featured image.
$thumb = (int) get_post_thumbnail_id( $post_id );
if ( $thumb > 0 ) {
$ids[ $thumb ] = true;
}
$content = isset( $post->post_content ) ? (string) $post->post_content : '';
if ( '' !== $content ) {
// Pass 1: explicit class / id / data-attribute patterns.
$patterns = array(
'/wp-image-(\d+)/',
'/id="attachment_(\d+)"/',
"/data-id=['\"](\d+)['\"]/",
"/data-attachment-id=['\"](\d+)['\"]/",
);
foreach ( $patterns as $pattern ) {
if ( preg_match_all( $pattern, $content, $m ) ) {
foreach ( $m[1] as $raw ) {
$id = (int) $raw;
if ( $id > 0 ) {
$ids[ $id ] = true;
}
}
}
}
// Pass 2: resolve `
` URLs. Catches images that
// none of the above patterns tagged — the cross-window
// drag-bridge insertion path being the canonical example.
if ( preg_match_all( '/
]+src=["\']([^"\']+)["\']/', $content, $m ) ) {
$seen_urls = array();
foreach ( $m[1] as $url ) {
$url = (string) $url;
if ( '' === $url || isset( $seen_urls[ $url ] ) ) {
continue;
}
$seen_urls[ $url ] = true;
$resolved = desktop_mode_my_wordpress_resolve_attachment_url( $url );
if ( $resolved > 0 ) {
$ids[ $resolved ] = true;
}
}
}
}
$out = array_map( 'intval', array_keys( $ids ) );
/**
* Filter the per-post attached-media id list.
*
* Plugins that store attachment references outside `post_content`
* (ACF image fields, page-builder block storage, post-meta
* galleries) can append their ids here.
*
* @since 0.21.0
*
* @param int[] $out Attachment ids resolved by the core scan.
* @param int $post_id Subject post id.
*/
$filtered = apply_filters( 'desktop_mode_my_wordpress_attached_media', $out, $post_id );
if ( ! is_array( $filtered ) ) {
return $out;
}
// Sanitize: positive integers, deduped.
$dedup = array();
foreach ( $filtered as $id ) {
$id = (int) $id;
if ( $id > 0 ) {
$dedup[ $id ] = true;
}
}
return array_map( 'intval', array_keys( $dedup ) );
}
/**
* In-request URL→attachment-id cache. `attachment_url_to_postid`
* runs a SQL query per call; a post that embeds the same image
* multiple times (gallery + cover + content) would otherwise pay
* for each occurrence.
*
* @since 0.21.0
*
* @param string $url Image URL pulled from `
`.
* @return int Attachment id, or 0 when no match.
*/
function desktop_mode_my_wordpress_resolve_attachment_url( $url ) {
static $cache = array();
$url = (string) $url;
if ( '' === $url ) {
return 0;
}
if ( isset( $cache[ $url ] ) ) {
return (int) $cache[ $url ];
}
// `attachment_url_to_postid()` does a literal `_wp_attached_file`
// meta lookup — it doesn't account for WP-generated variants:
//
// - Sized intermediates `image-300x200.jpg` — strip `-WxH`.
// - `-scaled.jpg` WP autogenerates this for uploads
// past the big-image threshold and
// stores it as `_wp_attached_file`,
// while editors emit the original
// URL in `
`. Try BOTH the
// scaled→original strip AND the
// original→scaled append.
//
// Strip query strings from every candidate so cache-buster URLs
// (`?ver=…`) still resolve.
$strip_query = static function ( $u ) {
$pos = strpos( $u, '?' );
return false === $pos ? $u : substr( $u, 0, $pos );
};
$clean = $strip_query( $url );
$candidates = array( $clean );
// `image-300x200.jpg` → `image.jpg`
if ( preg_match( '/^(.*)-\d+x\d+(\.[a-zA-Z0-9]+)$/', $clean, $m ) ) {
$candidates[] = $m[1] . $m[2];
}
// `image-scaled.jpg` → `image.jpg`
if ( preg_match( '/^(.*)-scaled(\.[a-zA-Z0-9]+)$/', $clean, $m ) ) {
$candidates[] = $m[1] . $m[2];
}
// `image.jpg` → `image-scaled.jpg` (post emits original, meta
// stores scaled — the most common drag-bridge insertion path).
if ( preg_match( '/^(.*)(\.[a-zA-Z0-9]+)$/', $clean, $m )
&& ! preg_match( '/-scaled$/', $m[1] )
) {
$candidates[] = $m[1] . '-scaled' . $m[2];
}
$resolved = 0;
foreach ( $candidates as $candidate ) {
$id = (int) attachment_url_to_postid( $candidate );
if ( $id > 0 ) {
$resolved = $id;
break;
}
}
$cache[ $url ] = $resolved;
return $resolved;
}