Skip to content

Add core/media-query ability - #1098

Draft
jorgefilipecosta wants to merge 29 commits into
developfrom
add/core-media-query
Draft

jorgefilipecosta wants to merge 29 commits into
developfrom
add/core-media-query

Conversation

@jorgefilipecosta

@jorgefilipecosta jorgefilipecosta commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

What?

Closes #1062

Adds core/media-query, a read-only ability for media library items.

Why?

Agents can read posts, users, and settings through abilities, but not the media library.

How?

  • The ability lives in includes/Abilities/Media/Media.php, behind the Custom Abilities experiment.
  • It follows the read path of /wp/v2/media: the posts controller's get_items(), get_item(), and permission checks, and the attachments controller's query and fields. Error codes are REST's with media_ in place of rest_.
  • REST's quirks are kept:
    • totals count items the caller cannot read;
    • exclude is ignored when include is given;
    • MIME types the site does not allow are ignored;
    • the generated caption of a password-protected item stays gated, even for editors, while its rendered description is not.
  • Two modes: one item by id, or a collection that returns { media, total, total_pages }.
    • Filters: search, media_type, mime_type, parent, author, include, exclude and status.
    • Sorting: orderby and order.
    • Pages: page and per_page.
  • fields picks the fields, with a lean default set, and id is always returned. title, caption and description are flattened to *_raw and *_rendered. Asking for a raw field reads in the edit context, so a collection only lists the items the caller can edit.
  • The tests port the core attachments controller's read tests under their core names. They add the ability's modes, string input, and roles from administrator to logged out, plus an e2e spec through the client-side Abilities API.

Differences from REST:

  • Callers must be logged in.
  • Attachments hidden from REST, which have no REST route, are denied.
  • A missing item is denied like an unreadable one.
  • Not supported:
    • the date filters, slug, offset, author_exclude, parent_exclude;
    • the taxonomy filters;
    • search_columns, search_semantics, orderby=include_slugs;
    • context, _fields and password.
  • Not returned:
    • guid, type, class_list, featured_media, comment_status, ping_status, meta, template, permalink_template, generated_slug;
    • taxonomy terms and register_rest_field() fields;
    • the edit-only media processing fields (missing_image_sizes, exif_orientation, image_quality, image_output_format, image_save_progressive).
  • Errors:
    • a status the caller may not query is media_invalid_param (REST's rest_invalid_param);
    • permission failures are ability_invalid_permissions;
    • the input is validated and permissions are checked before statuses, so a request that fails more than one check can report a different error first.
  • link and source_url are always strings.
  • No _links, embedding, headers, or rest_* filters.

Testing Instructions

  1. Enable the Custom Abilities experiment.
  2. In the block editor console, run the snippet from this comment with IDs from your site.
  3. npm run test:php -- --filter 'MediaTest|Gated_AbilitiesTest'
  4. npm run test:e2e -- tests/e2e/specs/abilities/core-media-query.spec.js

Changelog Entry

Added - New core/media-query ability that mirrors the REST API media endpoint.

Open WordPress Playground Preview

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.78457% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.27%. Comparing base (da700e0) to head (6914c20).

Files with missing lines Patch % Lines
includes/Abilities/Media/Media.php 96.76% 20 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop    #1098      +/-   ##
=============================================
+ Coverage      81.53%   82.27%   +0.73%     
- Complexity      3071     3231     +160     
=============================================
  Files            129      131       +2     
  Lines          12253    12875     +622     
=============================================
+ Hits            9991    10593     +602     
- Misses          2262     2282      +20     
Flag Coverage Δ
unit 82.27% <96.78%> (+0.73%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jorgefilipecosta

Copy link
Copy Markdown
Member Author

Tested in the browser console of the block editor, with the Custom Abilities and Excerpt Generation experiments on (the second one loads the abilities client modules):

const { ready } = await import( '@wordpress/core-abilities' );
await ready;
const { executeAbility } = await import( '@wordpress/abilities' );

const run = ( input ) =>
	executeAbility( 'core/media-query', input ).catch( ( error ) => ( { error: error.code } ) );

// A post and two attachments, the first one attached to the post.
const postId = 610;
const attachedId = 611;
const unattachedId = 612;

// A collection with the default fields.
console.log( 'list', await run( { include: [ attachedId, unattachedId ] } ) );

// One item, with the requested fields.
console.log( 'item', await run( { id: attachedId, fields: [ 'title_rendered', 'caption_raw', 'alt_text', 'post', 'filename', 'media_details' ] } ) );

// Filters, sorting and pages: the second of two matches.
console.log( 'filtered', await run( { media_type: [ 'image' ], mime_type: [ 'image/png' ], search: 'demo', orderby: 'title', order: 'asc', per_page: 1, page: 2, fields: [ 'id', 'title_rendered' ] } ) );

// The items attached to a post.
console.log( 'parent', await run( { parent: [ postId ], fields: [ 'id', 'post' ] } ) );

// Errors.
console.log( 'id with a query', await run( { id: attachedId, per_page: 1 } ) ); // ability_invalid_input
console.log( 'page too far', await run( { include: [ attachedId ], page: 99 } ) ); // media_post_invalid_page_number
console.log( 'relevance without a search', await run( { orderby: 'relevance' } ) ); // media_no_search_term_defined
console.log( 'missing item', await run( { id: 999999999 } ) ); // rest_ability_cannot_execute

Output:

  • list: both items with the default fields (id, date, slug, title_rendered, alt_text, media_type, mime_type, source_url), total: 2, total_pages: 1.
  • item: { id, title_rendered, caption_raw, alt_text, media_details, post, filename }, with post: 610.
  • filtered: the unattached image, total: 2, total_pages: 2.
  • parent: only the attached image.
  • The four errors come back with the codes in the comments.

@jeffpaul

jeffpaul commented Oct 4, 2026

Copy link
Copy Markdown
Member

@jorgefilipecosta note that I'm holding off on requesting reviews until this comes out of draft status

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a core/media-query ability (read-only Media Library coverage)

2 participants