This plugin hasn't been substantially rewritten since the introduction of the block editor, and there are a few code paths that bypass the plugin in some of the REST routes used by the block editor. I ran an audit over the plugin code to see any serious incompatbilities, and got these results - mostly edge cases, but there are a few things that would be good to address or fix.
Network Media Library — block-editor / REST API compatibility issues
The plugin predates the block editor, and several hooks assume the classic editor and
admin-ajax. On modern WordPress (6.7+) the editor and REST API drive most media flows,
and a number of paths either break, leak the switched site, or emit wrong/missing
responsive-image markup. Findings below were reviewed against WordPress core source.
Line references are against v1.6.0. Each High/Medium item is independent and
could be filed as its own issue.
| # |
Severity |
Area |
One-liner |
| 1 |
High |
REST |
rest_pre_dispatch switches to the media site but never restores it |
| 2 |
High |
REST / featured image |
WP_Error::get_data() fatal in the featured-media after-callback |
| 3 |
High |
Capabilities |
edit_post collapsed to create_posts, over-permitting attachment edits |
| 4 |
Medium |
Responsive images |
Relies on a the_content filter core dropped in 5.5 → wrong/missing srcset |
| 5 |
Medium |
Responsive images |
wp_get_attachment_image_src filter doesn't cover srcset/metadata |
| 6 |
Medium |
REST render |
REST-rendered content / block-renderer get no media-site switch |
| 7 |
Medium |
REST embed |
_embed'd wp:featuredmedia link built un-switched (broken embed) |
| 8 |
Medium |
ACF |
is_admin() guard breaks image/file fields in ACF block previews |
| 9 |
Medium |
ACF |
gallery / attachment post_object/relationship fields not resolved |
| 10 |
Low |
REST |
Greedy '/wp/v2/media' prefix match catches sibling routes |
| 11 |
Low |
Robustness |
No try/finally around switch/restore (leak on exception) |
| 12 |
Low |
ACF |
format_value cache keyed by field name collides across rows/blocks |
| 13 |
Low |
Deprecation |
wp_make_content_images_responsive() deprecated since 5.5 |
Environment
- WordPress 6.7+ multisite, block (FSE) theme.
- Network Media Library v1.6.0, media site selected via
network-media-library/site_id.
1. (High) rest_pre_dispatch switches to the media site but never restores it
Location: rest_pre_dispatch closure, ~lines 296–319 (switch_to_media_site() at ~313).
What happens: When a /wp/v2/media request matches, the closure calls
switch_to_media_site() with no matching restore_current_blog() anywhere in the request
lifecycle. The current blog therefore stays on the media site for the remainder of the PHP
request — through callback execution, response preparation, and any later filters.
Why it matters: Anything that runs after dispatch and reads/writes site data uses the
wrong site. This is most dangerous for /batch requests that mix media and non-media
sub-requests, and for any rest_post_dispatch/shutdown work — a wrong-site data
read or write, not just a cosmetic glitch.
Suggested fix: Pair the switch with a restore — e.g. set a request-scoped flag in
rest_pre_dispatch and restore_current_blog() on rest_post_dispatch (scoped per
sub-request so batched mixed routes don't leak).
2. (High) WP_Error::get_data() fatal in the featured-media after-callback
Location: rest_request_after_callbacks closure, ~lines 334–360 ($response->get_data() ~354, update_post_meta() ~349).
What happens: The closure calls $response->get_data() / $response->set_data(), but
WP_Error exposes only get_error_data() — there is no get_data()/set_data(). Core
applies rest_request_after_callbacks unconditionally, before converting a WP_Error
to a response (class-wp-rest-server.php:1255 runs the filter; the is_wp_error() →
error_to_response() conversion is at ~1256–1258). So when a post save that includes a
truthy featured_media returns a WP_Error for any reason, $response is a WP_Error
and the call fatals.
Why it matters: The REST save returns a 500 instead of the intended 4xx; the editor
shows a generic failure. Additionally, update_post_meta('_thumbnail_id', …) at ~349 runs
before the response type is inspected, so a featured image can be persisted for a save
the client believes failed (data-integrity side effect).
Suggested fix: Early-return on error and defer the side effect:
if ( is_wp_error( $response ) ) {
return $response;
}
// …only then inspect/persist the featured image, once the response is known successful.
Also fix the precedence at ~339 — (int) $request['featured_media'] ?? null parses as
((int) …) ?? null; parenthesize the coalesce before casting.
3. (High, security) edit_post collapsed to create_posts
Location: allow_media_library_access, ~lines 402–414.
What happens: When $cap === 'edit_post' for an attachment, the code substitutes the
attachment post type's create_posts cap (which core registers as upload_files), then
checks only user_can( $user_id, 'upload_files' ) and returns a satisfiable cap —
short-circuiting core's map_meta_cap, which for another user's attachment would require
edit_others_posts (capabilities.php ~261–263).
Why it matters: Any user who can upload (Author and above) can edit — and, where the
controller maps deletion to edit_post, delete — every attachment in the shared library,
across all sites, regardless of author. Reachable via POST /wp/v2/media/<id> (the
rest_pre_dispatch switch satisfies the get_current_blog_id() === get_site_id() guard).
Suggested fix: Don't collapse edit_post to create_posts. Let core's map_meta_cap
evaluate edit_post→edit_others_posts for the attachment on the media site, or at minimum
require edit_others_posts when the attachment author differs from the current user.
4. (Medium) Relies on a the_content filter core removed in 5.5 → wrong/missing srcset
Location: ~lines 439–454 (make_content_images_responsive + remove_filter/add_filter on the_content).
What happens: The plugin remove_filter()s wp_make_content_images_responsive, but
since WP 5.5 core registers wp_filter_content_tags (priority 12) on the_content
instead. The targeted filter no longer exists, so core's responsive-image processing runs
un-switched: wp_calculate_image_srcset queries the current (sub-)site, finds no
attachment, and returns false (no srcset) or wrong dimensions.
Suggested fix: Remove and re-add the current filter inside a switched wrapper:
remove_filter( 'the_content', 'wp_filter_content_tags', 12 );
add_filter( 'the_content', /* switched wrapper calling wp_filter_content_tags */, 12 );
(See also #13.)
5. (Medium) wp_get_attachment_image_src filter doesn't cover srcset/metadata
Location: wp_get_attachment_image_src filter, ~lines 171–191.
What happens: The filter switches sites only for wp_get_attachment_image_src, so the
src is correct, but srcset, sizes, and width/height come from
wp_calculate_image_srcset_meta / wp_get_attachment_metadata / wp_get_attachment_image_attributes,
which are read on the sub-site (no attachment) → missing/incorrect responsive markup;
some sizes 404.
Suggested fix: Also filter wp_calculate_image_srcset_meta, wp_get_attachment_metadata,
and wp_get_attachment_image_attributes (switch to the media site to recompute), so the
whole attribute/srcset computation happens against the media site.
6. (Medium) REST-rendered content / block-renderer get no media-site switch
Location: rest_pre_dispatch (296–319) only switches /wp/v2/media (+ regenerate-thumbnails); the the_content wrapper (439–454) is front-end only.
What happens: Content rendered via REST (e.g. /wp/v2/block-renderer/<block> used for
dynamic-block previews, or rendered-content responses) never enters a switched context, so
cross-site images come back with missing/incorrect responsive markup or broken URLs.
Suggested fix: Apply the switch to the content-rendering path itself (wrap
wp_filter_content_tags / the srcset hooks) rather than per-route, so it covers both the
front end and REST rendering.
7. (Medium) _embed'd wp:featuredmedia link built un-switched
Location: rest_pre_dispatch switches /wp/v2/media but not /wp/v2/posts / /wp/v2/pages; cf. core rest_get_route_for_post() (rest-api.php ~3183–3195) and the posts controller (~2233–2240).
What happens: Because the attachment exists only on the media site and the posts route
isn't switched, rest_get_route_for_post() returns '', so the wp:featuredmedia link
href becomes rest_url('') (a bare /wp-json/). _embed cannot resolve the featured
image — breaking the editor's and any headless client's featured-image embed.
Suggested fix: Add a rest_route_for_post filter that resolves attachment routes
against the media site, or filter rest_prepare_{post_type} to rewrite
_links['wp:featuredmedia'] / inject the embed.
8. (Medium) is_admin() guard breaks image/file fields in ACF block previews
Location: ACF_Value_Filter::filter_acf_attachment_load_value, ~line 494 (! is_media_site() && ! is_admin()).
What happens: The ACF block-renderer REST endpoint renders in an is_admin() context,
so the guard skips the media-site switch. Image/file fields render broken/empty in ACF block
previews in the editor, while the same field renders fine on the public front end.
Suggested fix: Resolve attachments on the media site whenever ! is_media_site(),
regardless of admin context (matching the wp_get_attachment_image_src filter, which
correctly does not gate on is_admin()).
9. (Medium) gallery and attachment relational ACF fields not resolved
Location: ACF_Value_Filter::__construct field-types array, ~lines 472–481 (only image, file).
What happens: gallery fields return IDs that don't resolve on the current site (empty
items), and post_object/relationship fields that point at attachments return
null/invalid objects.
Suggested fix: Add gallery with array-aware resolution; for relational fields, resolve
attachment-type targets on the media site.
10. (Low) Greedy '/wp/v2/media' prefix match
Location: ~lines 305–316 (strpos( $route, … ) === 0).
A prefix test matches unintended sibling routes, which then get switched to the media site
and have their post param nulled (wrong-site data / stripped param). Anchor with a regex,
e.g. preg_match( '#^/wp/v2/media(/|$)#', $route ).
11. (Low) No exception safety around switch/restore
Location: filter_post_gallery (201–211), make_content_images_responsive (439–451).
A throw between switch_to_media_site() and restore_current_blog() leaks the switch for
the rest of the request (filter_post_gallery also leaves its own filter removed). Use
try/finally.
12. (Low) ACF format_value cache keyed by field name
Location: ACF_Value_Filter $value + filter_acf_attachment_format_value, ~lines 509, 522–524.
The override stores the resolved value keyed by field name, so repeater rows / a field
reused twice / the same field across blocks collide (last-writer-wins), and it can warn /
return null when format_value runs for a field whose load_value branch was skipped.
Consider removing the format_value override and letting ACF format the already-resolved
value.
13. (Low) Deprecated wp_make_content_images_responsive()
Location: ~line 446.
Deprecated since WP 5.5; emits a deprecation notice on every render and bypasses modern tag
handling (loading, decoding, fetchpriority). Replace with wp_filter_content_tags()
inside the switched wrapper (ties into #4).
Verified working (checked, no change needed)
- REST uploads (
POST /wp/v2/media, drag-and-drop) correctly route to the media site via
rest_pre_dispatch; the legacy wp_ajax_upload-attachment / load-async-upload.php
hooks are effectively dead but harmless.
- Statically-rendered
core/image, Cover, and Media-&-Text base images resolve on the
front end because absolute media-site URLs are baked into saved content.
- The
wp_get_attachment_image_src static re-entrancy guard keeps switches balanced under
nested/SSR rendering.
- The List-mode
parse_request guard is admin-screen-scoped and does not leak into REST.
Method
Each hook was reviewed against WordPress core source (e.g. class-wp-rest-server.php,
class-wp-error.php, rest-api.php, capabilities.php, post.php) to confirm the
behavior and reachability before inclusion; findings that core or the plugin already handle
correctly were excluded.
This plugin hasn't been substantially rewritten since the introduction of the block editor, and there are a few code paths that bypass the plugin in some of the REST routes used by the block editor. I ran an audit over the plugin code to see any serious incompatbilities, and got these results - mostly edge cases, but there are a few things that would be good to address or fix.
Network Media Library — block-editor / REST API compatibility issues
The plugin predates the block editor, and several hooks assume the classic editor and
admin-ajax. On modern WordPress (6.7+) the editor and REST API drive most media flows,
and a number of paths either break, leak the switched site, or emit wrong/missing
responsive-image markup. Findings below were reviewed against WordPress core source.
Line references are against v1.6.0. Each High/Medium item is independent and
could be filed as its own issue.
rest_pre_dispatchswitches to the media site but never restores itWP_Error::get_data()fatal in the featured-media after-callbackedit_postcollapsed tocreate_posts, over-permitting attachment editsthe_contentfilter core dropped in 5.5 → wrong/missingsrcsetwp_get_attachment_image_srcfilter doesn't coversrcset/metadata_embed'dwp:featuredmedialink built un-switched (broken embed)is_admin()guard breaks image/file fields in ACF block previewsgallery/ attachmentpost_object/relationshipfields not resolved'/wp/v2/media'prefix match catches sibling routestry/finallyaround switch/restore (leak on exception)format_valuecache keyed by field name collides across rows/blockswp_make_content_images_responsive()deprecated since 5.5Environment
network-media-library/site_id.1. (High)
rest_pre_dispatchswitches to the media site but never restores itLocation:
rest_pre_dispatchclosure, ~lines 296–319 (switch_to_media_site()at ~313).What happens: When a
/wp/v2/mediarequest matches, the closure callsswitch_to_media_site()with no matchingrestore_current_blog()anywhere in the requestlifecycle. The current blog therefore stays on the media site for the remainder of the PHP
request — through callback execution, response preparation, and any later filters.
Why it matters: Anything that runs after dispatch and reads/writes site data uses the
wrong site. This is most dangerous for
/batchrequests that mix media and non-mediasub-requests, and for any
rest_post_dispatch/shutdown work — a wrong-site dataread or write, not just a cosmetic glitch.
Suggested fix: Pair the switch with a restore — e.g. set a request-scoped flag in
rest_pre_dispatchandrestore_current_blog()onrest_post_dispatch(scoped persub-request so batched mixed routes don't leak).
2. (High)
WP_Error::get_data()fatal in the featured-media after-callbackLocation:
rest_request_after_callbacksclosure, ~lines 334–360 ($response->get_data()~354,update_post_meta()~349).What happens: The closure calls
$response->get_data()/$response->set_data(), butWP_Errorexposes onlyget_error_data()— there is noget_data()/set_data(). Coreapplies
rest_request_after_callbacksunconditionally, before converting aWP_Errorto a response (
class-wp-rest-server.php:1255runs the filter; theis_wp_error()→error_to_response()conversion is at ~1256–1258). So when a post save that includes atruthy
featured_mediareturns aWP_Errorfor any reason,$responseis aWP_Errorand the call fatals.
Why it matters: The REST save returns a 500 instead of the intended 4xx; the editor
shows a generic failure. Additionally,
update_post_meta('_thumbnail_id', …)at ~349 runsbefore the response type is inspected, so a featured image can be persisted for a save
the client believes failed (data-integrity side effect).
Suggested fix: Early-return on error and defer the side effect:
Also fix the precedence at ~339 —
(int) $request['featured_media'] ?? nullparses as((int) …) ?? null; parenthesize the coalesce before casting.3. (High, security)
edit_postcollapsed tocreate_postsLocation:
allow_media_library_access, ~lines 402–414.What happens: When
$cap === 'edit_post'for an attachment, the code substitutes theattachment post type's
create_postscap (which core registers asupload_files), thenchecks only
user_can( $user_id, 'upload_files' )and returns a satisfiable cap —short-circuiting core's
map_meta_cap, which for another user's attachment would requireedit_others_posts(capabilities.php~261–263).Why it matters: Any user who can upload (Author and above) can edit — and, where the
controller maps deletion to
edit_post, delete — every attachment in the shared library,across all sites, regardless of author. Reachable via
POST /wp/v2/media/<id>(therest_pre_dispatchswitch satisfies theget_current_blog_id() === get_site_id()guard).Suggested fix: Don't collapse
edit_posttocreate_posts. Let core'smap_meta_capevaluate
edit_post→edit_others_postsfor the attachment on the media site, or at minimumrequire
edit_others_postswhen the attachment author differs from the current user.4. (Medium) Relies on a
the_contentfilter core removed in 5.5 → wrong/missing srcsetLocation: ~lines 439–454 (
make_content_images_responsive+remove_filter/add_filteronthe_content).What happens: The plugin
remove_filter()swp_make_content_images_responsive, butsince WP 5.5 core registers
wp_filter_content_tags(priority 12) onthe_contentinstead. The targeted filter no longer exists, so core's responsive-image processing runs
un-switched:
wp_calculate_image_srcsetqueries the current (sub-)site, finds noattachment, and returns
false(no srcset) or wrong dimensions.Suggested fix: Remove and re-add the current filter inside a switched wrapper:
(See also #13.)
5. (Medium)
wp_get_attachment_image_srcfilter doesn't cover srcset/metadataLocation:
wp_get_attachment_image_srcfilter, ~lines 171–191.What happens: The filter switches sites only for
wp_get_attachment_image_src, so thesrcis correct, butsrcset,sizes, and width/height come fromwp_calculate_image_srcset_meta/wp_get_attachment_metadata/wp_get_attachment_image_attributes,which are read on the sub-site (no attachment) → missing/incorrect responsive markup;
some sizes 404.
Suggested fix: Also filter
wp_calculate_image_srcset_meta,wp_get_attachment_metadata,and
wp_get_attachment_image_attributes(switch to the media site to recompute), so thewhole attribute/srcset computation happens against the media site.
6. (Medium) REST-rendered content / block-renderer get no media-site switch
Location:
rest_pre_dispatch(296–319) only switches/wp/v2/media(+ regenerate-thumbnails); thethe_contentwrapper (439–454) is front-end only.What happens: Content rendered via REST (e.g.
/wp/v2/block-renderer/<block>used fordynamic-block previews, or rendered-content responses) never enters a switched context, so
cross-site images come back with missing/incorrect responsive markup or broken URLs.
Suggested fix: Apply the switch to the content-rendering path itself (wrap
wp_filter_content_tags/ the srcset hooks) rather than per-route, so it covers both thefront end and REST rendering.
7. (Medium)
_embed'dwp:featuredmedialink built un-switchedLocation:
rest_pre_dispatchswitches/wp/v2/mediabut not/wp/v2/posts//wp/v2/pages; cf. corerest_get_route_for_post()(rest-api.php~3183–3195) and the posts controller (~2233–2240).What happens: Because the attachment exists only on the media site and the posts route
isn't switched,
rest_get_route_for_post()returns'', so thewp:featuredmedialinkhref becomes
rest_url('')(a bare/wp-json/)._embedcannot resolve the featuredimage — breaking the editor's and any headless client's featured-image embed.
Suggested fix: Add a
rest_route_for_postfilter that resolves attachment routesagainst the media site, or filter
rest_prepare_{post_type}to rewrite_links['wp:featuredmedia']/ inject the embed.8. (Medium)
is_admin()guard breaks image/file fields in ACF block previewsLocation:
ACF_Value_Filter::filter_acf_attachment_load_value, ~line 494 (! is_media_site() && ! is_admin()).What happens: The ACF block-renderer REST endpoint renders in an
is_admin()context,so the guard skips the media-site switch. Image/file fields render broken/empty in ACF block
previews in the editor, while the same field renders fine on the public front end.
Suggested fix: Resolve attachments on the media site whenever
! is_media_site(),regardless of admin context (matching the
wp_get_attachment_image_srcfilter, whichcorrectly does not gate on
is_admin()).9. (Medium)
galleryand attachment relational ACF fields not resolvedLocation:
ACF_Value_Filter::__constructfield-types array, ~lines 472–481 (onlyimage,file).What happens:
galleryfields return IDs that don't resolve on the current site (emptyitems), and
post_object/relationshipfields that point at attachments returnnull/invalid objects.
Suggested fix: Add
gallerywith array-aware resolution; for relational fields, resolveattachment-type targets on the media site.
10. (Low) Greedy
'/wp/v2/media'prefix matchLocation: ~lines 305–316 (
strpos( $route, … ) === 0).A prefix test matches unintended sibling routes, which then get switched to the media site
and have their
postparam nulled (wrong-site data / stripped param). Anchor with a regex,e.g.
preg_match( '#^/wp/v2/media(/|$)#', $route ).11. (Low) No exception safety around switch/restore
Location:
filter_post_gallery(201–211),make_content_images_responsive(439–451).A throw between
switch_to_media_site()andrestore_current_blog()leaks the switch forthe rest of the request (
filter_post_galleryalso leaves its own filter removed). Usetry/finally.12. (Low) ACF
format_valuecache keyed by field nameLocation:
ACF_Value_Filter$value+filter_acf_attachment_format_value, ~lines 509, 522–524.The override stores the resolved value keyed by field name, so repeater rows / a field
reused twice / the same field across blocks collide (last-writer-wins), and it can warn /
return null when
format_valueruns for a field whoseload_valuebranch was skipped.Consider removing the
format_valueoverride and letting ACF format the already-resolvedvalue.
13. (Low) Deprecated
wp_make_content_images_responsive()Location: ~line 446.
Deprecated since WP 5.5; emits a deprecation notice on every render and bypasses modern tag
handling (
loading,decoding,fetchpriority). Replace withwp_filter_content_tags()inside the switched wrapper (ties into #4).
Verified working (checked, no change needed)
POST /wp/v2/media, drag-and-drop) correctly route to the media site viarest_pre_dispatch; the legacywp_ajax_upload-attachment/load-async-upload.phphooks are effectively dead but harmless.
core/image, Cover, and Media-&-Text base images resolve on thefront end because absolute media-site URLs are baked into saved content.
wp_get_attachment_image_srcstatic re-entrancy guard keeps switches balanced undernested/SSR rendering.
parse_requestguard is admin-screen-scoped and does not leak into REST.Method
Each hook was reviewed against WordPress core source (e.g.
class-wp-rest-server.php,class-wp-error.php,rest-api.php,capabilities.php,post.php) to confirm thebehavior and reachability before inclusion; findings that core or the plugin already handle
correctly were excluded.