Theme Directory: map the moderation capabilities against the theme - #855
Theme Directory: map the moderation capabilities against the theme#855obenland wants to merge 4 commits into
Conversation
`suspend_theme` and `reinstate_theme` are called with the theme's post ID, but the mapping resolved them straight to a primitive without ever reading that context, so the answer did not depend on which theme was passed. Resolve the post, require it to be a repopackage, and require the post type's `edit_others_posts` alongside the primitive, since moderating a theme acts on a record somebody else owns. Also drops `author` from the two role lists, matching the comment above them, and sets an explicit `post_status` on the commercial shops query so an unpublished entry cannot reach the shared cache the public API reads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
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. |
Roles on the multisite blogs are assigned individually rather than granted on sign-up, and a lot of the estate leans on that. Worth writing down, since it is not derivable from the code and it is the premise behind a recurring class of misread privilege-escalation findings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates Theme Directory capability resolution so theme moderation meta caps (suspend_theme / reinstate_theme) are evaluated against a specific theme post, preventing object-independent authorization decisions in a production-critical area.
Changes:
- Make
wporg_themes_map_meta_cap()resolve moderation meta caps using the provided theme post context, requiring arepopackageandedit_others_postsin addition to the moderation primitive. - Restrict moderation primitives to “Editors and higher” by removing
authorfrom the roles grantedsuspend_themes/reinstate_themes. - Ensure
Themes_API::get_commercial_shops()only queries publishedtheme_shopposts to avoid caching private entries for unauthenticated consumers. - Add PHPUnit coverage for the moderation capability mapping behavior and key regression workflow.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| wordpress.org/public_html/wp-content/plugins/theme-directory/tests/Theme_Moderation_Capabilities_Test.php | Adds tests covering object-scoped moderation capability mapping and regressions. |
| wordpress.org/public_html/wp-content/plugins/theme-directory/class-themes-api.php | Limits commercial shops query to published posts to prevent caching private entries. |
| wordpress.org/public_html/wp-content/plugins/theme-directory/admin-edit.php | Fixes moderation meta cap mapping to require valid theme context and appropriate primitives; tightens role grants. |
| AGENTS.md | Documents a security trust model consideration for capability checks across WordPress.org properties. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`get_post( 0 )` falls back to the global `$post`, so a context of `array( 0 )` was answered against whatever post happened to be current rather than refused. Key the guard on an empty context instead, and resolve the post type object before dereferencing it, since `get_post()` does not require the type to be registered. Stops the test teardown revoking the moderation primitives too: granting them to Editors and Administrators is the site's normal state, and these tests run without a transaction, so revoking left the state behind in the database. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every caller of these caps runs at admin render time or on `admin_action_*`, both well after `init`, so if the plugin is active the post type is registered by the time the mapping is asked anything. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
suspend_themeandreinstate_themeare always called with the theme's post ID, butwporg_themes_map_meta_cap()resolved them straight to their primitive without ever reading that context — so the answer did not depend on which theme was passed, and a call carrying no theme at all was answered as though one had been. Thetheme_configure_categorization_optionsbranch a few lines below already has the shape this should have.This resolves the post, requires it to be a
repopackage, and requires the post type'sedit_others_postsalongside the primitive, since moderating a theme acts on a record somebody else owns. Returning a fresh array also retires theunset( $caps[ array_search( $cap, $caps ) ] )idiom, which silently does nothing when the meta cap is not present in$caps.Two smaller things in the same area:
authoris dropped from both role lists, so they match the comment above them ("Editors and higher"). The persisted grant has already been cleared on the site.Themes_API::get_commercial_shops()gets an explicit'post_status' => 'publish'. Without itWP_Queryalso returns private entries readable by the current user, and the result is written to a 15 minute object cache that the unauthenticatedthemes/1.0/commercial-shopsendpoint then serves.Testing
New
tests/Theme_Moderation_Capabilities_Test.phpcovers both directions: an Author cannot moderate a theme (their own or anybody else's), an Editor still can, and a call carrying no theme or a non-theme post is refused.test_owner_still_edits_their_unpublished_packageguards the workflowSave_Version_Status_Testdepends on, which an earlier and broader version of this patch broke.Against the unpatched file, four of the six fail. Full suite: 102 tests, no failures.
AGENTS.md
Also adds a short
Security Trust Modelsection. Roles on the WordPress.org multisite blogs are assigned individually rather than granted on sign-up, and much of the estate leans on that — capability checks are often coarse because on most sites there is no untrusted principal below the people who run them. It is not derivable from the code, and it varies by site, so it is worth writing down for anyone reviewing this area.🤖 Generated with Claude Code