Skip to content

Micellanous Network Media Library / block editor compability issues #108

Description

@goldenapples

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_postedit_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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions