Skip to content

Theme Directory: map the moderation capabilities against the theme - #855

Closed
obenland wants to merge 4 commits into
WordPress:trunkfrom
obenland:update/theme-cap-mapping
Closed

Theme Directory: map the moderation capabilities against the theme#855
obenland wants to merge 4 commits into
WordPress:trunkfrom
obenland:update/theme-cap-mapping

Conversation

@obenland

@obenland obenland commented Sep 1, 2026

Copy link
Copy Markdown
Member

suspend_theme and reinstate_theme are always called with the theme's post ID, but wporg_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. The theme_configure_categorization_options branch 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's edit_others_posts alongside the primitive, since moderating a theme acts on a record somebody else owns. Returning a fresh array also retires the unset( $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:

  • author is 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 it WP_Query also returns private entries readable by the current user, and the result is written to a 15 minute object cache that the unauthenticated themes/1.0/commercial-shops endpoint then serves.

Testing

New tests/Theme_Moderation_Capabilities_Test.php covers 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_package guards the workflow Save_Version_Status_Test depends 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.

npm run themes:test

AGENTS.md

Also adds a short Security Trust Model section. 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

`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>
Copilot AI lite review requested due to automatic review settings September 1, 2026 17:55
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

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 props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props obenland.

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a repopackage and edit_others_posts in addition to the moderation primitive.
  • Restrict moderation primitives to “Editors and higher” by removing author from the roles granted suspend_themes / reinstate_themes.
  • Ensure Themes_API::get_commercial_shops() only queries published theme_shop posts 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.

obenland and others added 2 commits September 1, 2026 13:02
`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>
@bazza bazza closed this in 35f551d Sep 1, 2026
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.

2 participants