Repository navigation
Abilities API: Add a core/content-query ability - #12195
jorgefilipecosta wants to merge 33 commits into
Conversation
|
Hi there! 👋 Thank you for your contribution to WordPress! 💖 It looks like this is your first pull request to No one monitors this repository for new pull requests. Pull requests must be attached to a Trac ticket to be considered for inclusion in WordPress Core. To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description. Pull requests are never merged on GitHub. The WordPress codebase continues to be managed through the SVN repository that this GitHub repository mirrors. Please feel free to open pull requests to work on any contribution you are making. More information about how GitHub pull requests can be used to contribute to WordPress can be found in the Core Handbook. Please include automated tests. Including tests in your pull request is one way to help your patch be considered faster. To learn about WordPress' test suites, visit the Automated Testing page in the handbook. If you have not had a chance, please review the Contribute with Code page in the WordPress Core Handbook. The Developer Hub also documents the various coding standards that are followed:
Thank you, |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
87c0275 to
3cf4957
Compare
103a8d6 to
8123116
Compare
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Thanks @jorgefilipecosta! I went through this against the companion plugin PR WordPress/ai#739. The Core class is a truthful representation of what is proposed there. After accounting for the expected adaptations (no namespace, no On sequencing: the AI plugin PR should go first. Since #739 is still open, we are matching a proposal, not merged code, so this PR should wait on that merge to avoid the two drifting. One small thing to add: the Trac ticket reference is missing. There should already be one for this ability, like the sibling PRs have (users links #64657 and settings links #64605). Could you add it to the description and the test |
peterwilsoncc
left a comment
There was a problem hiding this comment.
I've added a few notes inline. LMK if this is no longer the canonical URL as there are a few linked to the ticket.
| /** This filter is documented in wp-includes/post-template.php. */ | ||
| // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying the core content filter to mirror REST rendering. | ||
| $content = apply_filters( 'the_content', $post->post_content ); |
There was a problem hiding this comment.
I think the phpcs ignore will need to be on the same line so the docs parser recognises the docblock reference.
| /** This filter is documented in wp-includes/post-template.php. */ | |
| // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying the core content filter to mirror REST rendering. | |
| $content = apply_filters( 'the_content', $post->post_content ); | |
| /** This filter is documented in wp-includes/post-template.php. */ | |
| $content = apply_filters( 'the_content', $post->post_content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying the core content filter to mirror REST rendering. |
| $response = $this->server->dispatch( $this->run_request( array( 'post_type' => 'post' ) ) ); | ||
| $data = $response->get_data(); | ||
|
|
||
| $this->assertSame( 200, $response->get_status() ); |
There was a problem hiding this comment.
This and other tests containing multiple assertions will need a unique $message param for each assertion to assist with debugging at a later date.
🔢 This applies to multiple tests but I won't repeat myself.
| ) | ||
| ); | ||
| } finally { | ||
| array_pop( $wp_current_filter ); |
There was a problem hiding this comment.
$wp_current_filter is restored as part of the default teardown so you can remove the finally array_pop pattern in Core.
🔢 This applies to other tests so I won't repeat myself.
See:
wordpress-develop/tests/phpunit/includes/abstract-testcase.php
Lines 416 to 432 in 74bb933
|
|
||
| $this->assertContains( $post_id, $ids, 'The custom post type should be queryable through the content ability.' ); | ||
| } finally { | ||
| unregister_post_type( 'wpai_content_cpt' ); |
There was a problem hiding this comment.
Post types set up by the test suite are automatically removed
🔢 Applies to other tests so I won't repeat myself
see
wordpress-develop/tests/phpunit/includes/abstract-testcase.php
Lines 336 to 350 in 74bb933
|
|
||
| $post_type_object = $exposed[ $post_type ]; | ||
| if ( $requires_edit ) { | ||
| return current_user_can( $this->post_type_cap( $post_type_object, 'edit_posts' ) ); // phpcs:ignore WordPress.WP.Capabilities.Undetermined -- Capability is resolved from the post type's capability object. |
There was a problem hiding this comment.
I'm unclear of the benefit $this->post_type_cap provides. If the post type doesn't exist or isn't exposed the permission check will fail above.
| // `orderby` is left unset, which orders by `post_date` descending, matching the | ||
| // default of the REST posts controller. |
There was a problem hiding this comment.
Multi line comment format -- https://developer.wordpress.org/coding-standards/inline-documentation-standards/php/#5-2-multi-line-comments
🔢 Applies in a few places so I won't repeat.
| return $total; | ||
| } | ||
|
|
||
| $count_args = $query_args; |
There was a problem hiding this comment.
If $query_args has no found rows set, the query generated here will too, it will need to be unset.
|
In addition to addressing the feedback from @peterwilsoncc, we need to sync the latest fixes applied in the AI plugin. Most importantly: |
core/content-query ability
Adds a read-only `core/content` ability that retrieves one or more posts of a post type exposed to abilities via a new `show_in_abilities` post type argument (enabled for `post` and `page` by default). Fetch a single post by ID or by slug, or query multiple posts filtered by post type, status, author, or parent, selecting a support-aware set of fields per post. Permissions follow the REST posts model: a coarse status/capability gate plus an authoritative per-post read_post check, with password-protected content withheld from users who cannot edit the post and a uniform not-found response to avoid leaking the existence of posts.
Mirrors the refinements from the core/settings review that also apply to core/content:
- Memoize the exposed post types so the input schema and the permission/execute
callbacks derive from a single walk of the registered post types.
- Default the input schema to an empty object so the type:object default serializes as {}.
- Harden input/value handling (type guards, a capability resolver, and a non-negative
integer helper) against loosely-typed request data.
Convert WP_Content_Abilities from a static class to a final, instance-based one, matching WP_Settings_Abilities and the canonical abilities pattern: register() is now invoked via ( new WP_Content_Abilities() )->register() from wp_register_core_abilities(). The externally-invoked entry points (register, check_permission, execute_get_content, return_raw_title_format) stay public; register_get_content() and the shared helpers become private; CATEGORY and the per-page bounds become private consts; FIELDS becomes a private instance property; and the cached exposed post types become instance state. Behaviour is unchanged. The per-page assertions in the test read the now-private constants by value.
… modes. Replace the flat anyOf(id|post_type) input schema with a oneOf of two modes, each with additionalProperties:false: - Get a single post by id (optionally guarded by post_type), plus fields. - Query a set of posts by post_type plus slug/status/author/parent/page/per_page, plus fields. Invalid combinations (e.g. per_page alongside id) now fail validation instead of being silently ignored. Update wpRegisterCoreContentAbility accordingly and add coverage for the id-mode rejecting query-only params and accepting a post_type guard.
670cd55 to
b8ffb86
Compare
Renames the ability from `core/read-content` to `core/content-query` to match the AI plugin (WordPress/ai#1002), and ports the plugin's lean projection test, which checks the query arguments the ability builds instead of counting post meta queries. Also drops the trailing period from the hook documentation references, which the PHPStan hook docblock rule reads as part of the file path.
Declares `meta.public` instead of `meta.show_in_rest`, matching the other core abilities since [62737]. The flag still seeds `show_in_rest`, so REST exposure is unchanged.
…ility Core's coding standards do not run the `PrefixAllGlobals` sniff, so the ignores only separated each "This filter is documented in" reference from its `apply_filters()` call, which keeps the docs parser from linking them.
The test case restores `$wp_current_filter` after each test, so the helpers no longer need a `try`/`finally` to pop the faked action.
The test case unregisters non-default post types and statuses before each test, so the `try`/`finally` blocks that removed them by hand can go. The `tearDown()` reset of `show_in_abilities` on `post` and `page` stays: those built-in objects are only recreated in the next `set_up()`, after the next class's `set_up_before_class()` may already have registered the core abilities.
`setUp()` registers the `content` category when it is missing, but nothing removed it, so it leaked into later classes. In a random test order that broke `Tests_Abilities_API_WpRegisterAbilityCategory::test_get_all_categories`, which expects only the categories it registers.
`get_post_type_capabilities()` always sets `edit_posts` and `read_private_posts` for registered post types, and the REST posts controller reads them directly too, so the guard isn't needed. This also drops the `WordPress.WP.Capabilities.Undetermined` ignores, since core's PHPCS ruleset doesn't run that sniff.
get_post_type_capabilities() always sets the capabilities these checks read for registered post types, and the REST posts controller reads them directly too, so the guard isn't needed. This matches the change made in review of WordPress/wordpress-develop#12195.
get_post_type_capabilities() always sets the capabilities these checks read for registered post types, and the REST posts controller reads them directly too, so the guard isn't needed. This matches the change made in review of WordPress/wordpress-develop#12195.
…ery ability The content query now resolves the requested post with `get_content_by_id()` and the post type with `get_exposed_post_type()`, and builds its `fields` input and post output schemas with `get_fields_input_schema()` and `get_content_output_schema()`, as the AI plugin does so its content write abilities can reuse them (WordPress/ai#1025). `get_content_by_id()` parses the ID the way the integer filters are parsed, so only an integer or an unsigned integer string resolves a post.
…ery ability The query took an author user ID as its filter and returned the author as `author`, an object with the user ID and display name. It now takes and returns `author_slug`, the user's nicename, which `core/users-query` returns as `slug`, as the AI plugin does (WordPress/ai#1025). The slug must match exactly one user, or the filter fails as `content_invalid_filter`. A user the caller may not see in `core/users-query` is reported like a missing one, so the filter reveals no hidden user.
…urns a non-string `get_title()` returned whatever the `the_title` filters returned, so a filter that returns a non-string failed its string return type and turned the query into an `ability_callback_exception`. The rendered excerpt and content are already guarded this way, as the AI plugin does (WordPress/ai#1025).
…ister the core abilities `set_up_before_class()` removed the test suite's unhook callbacks and added the core registration callbacks, and only `tear_down_after_class()` put the unhook callbacks back. The first test of a run snapshots the hooks that every test is reset to, so when this class ran first, the changed hooks leaked into the rest of the run: later tests registered the core abilities whenever the registry initialized, and `test_get_existing_ability_using_callback` failed because the `site` category was not registered. The hooks are now restored as soon as the core abilities are registered.
WordPress/wordpress-develop#12195 now has the same ID lookup, post type check, shared schemas, author_slug filter and field, and title guard as the query here, so the markers that said what core does instead were wrong. Remove them, and put back core's wording in the comments they were added to. The class docblock now says that only the write abilities and the helpers they alone use are missing from core.
…uery ability `check_read_permission()` calls `get_exposed_post_type()` for every post a query returns, up to 100 per page, and each call rebuilt the map of every exposed post type. It now reads the post type object and checks `show_in_abilities`, the same truthy rule `get_post_types()` applies, as the AI plugin does (WordPress/ai#1025). `get_exposed_post_types()` was then only used for the post type names at registration, which `get_post_types()` returns directly, so it is removed.
The `get_author_by_slug()` docblock said the lookup reveals no hidden user, as in `core/users-query`. A user who can edit others' posts of the post type, such as an editor, resolves any slug, since they may make any user the author, while `core/users-query` hides users like subscribers from them. The docblock now says so, as the AI plugin does (WordPress/ai#1025).
…docblock The docblock said the parser keeps `author => 0` from dropping the author filter, but the author filter is now `author_slug`, resolved by `get_author_by_slug()`. `parse_filter_int()` only reads the `id` input and the `parent` filter, so the docblock now describes those, as the AI plugin does (WordPress/ai#1025).
Part of WordPress/ai#40. Ports the
core/content-queryability from the AI plugin, added in WordPress/ai#739, renamed fromcore/read-contentin WordPress/ai#1002, and updated with the read changes from WordPress/ai#1025.core/content-queryability, in a newcontentcategory, through the internalWP_Content_Abilitiesclass.show_in_abilitiespost type argument that controls which post types the ability can read. It defaults tofalse;postandpageset it totrue.id, or bypost_typeandslug, or a page of posts of one type filtered bystatus,author_slug,parent, orinclude, withfieldsto choose what each post returns.publishneed edit access, orread_private_postsforprivate.Testing Instructions
Verify unit tests are passing:
Trac ticket: https://core.trac.wordpress.org/ticket/64606
Use of AI Tools
AI assistance: Yes. Claude Code was used to sync this PR with the plugin and apply the review feedback; I reviewed the changes.
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.