Skip to content

Users: Guard against roles missing a capabilities array in WP_User_Query and WP_Roles - #12931

Open
melbos wants to merge 1 commit into
WordPress:trunkfrom
melbos:fix/62600-user-query-role-missing-capabilities
Open

Users: Guard against roles missing a capabilities array in WP_User_Query and WP_Roles#12931
melbos wants to merge 1 commit into
WordPress:trunkfrom
melbos:fix/62600-user-query-role-missing-capabilities

Conversation

@melbos

@melbos melbos commented Aug 7, 2026

Copy link
Copy Markdown

A role stored without a capabilities key, or with a non-array value for it, causes two separate problems, not just one:

  1. A fatal TypeError in WP_User_Query::prepare_query() when querying by capability, since array_filter() is called directly on $role_data['capabilities']. This is the crash originally reported on the ticket, via wp_dropdown_users() on the classic Author meta box.
  2. The same unguarded assumption exists in WP_Roles::init_roles(), which runs on effectively every request that initializes roles — not just capability-filtered queries. Neither add check is capabilities key is set #8351 nor Added condition to check if is an array() for $role_data['capabilities'] #8823 addresses this second call site.

This can happen when a plugin registers a role without capabilities and is later deactivated, leaving the malformed role behind in the site's user_roles option.

This PR builds on the investigation already done in #8351 (by @geekofshire) and #8823 (by @umeshnevase) — thank you both. It combines the is_array() robustness from #8823 with the more readable guard-clause shape from #8351, extends the same fix to WP_Roles::init_roles(), and adds the unit test coverage that @johnbillion asked for and that both prior PRs were still missing.

Reproduction

Reproduced against a real WordPress install (not just the unit tests) by creating a role with no capabilities key and triggering the exact code path from the original report (wp_dropdown_users() with a capability filter):

Before the fix (wp-content/debug.log):

[07-Aug-2026 10:00:41 UTC] PHP Warning:  Undefined array key "capabilities" in wp-includes/class-wp-roles.php on line 310
[07-Aug-2026 10:00:41 UTC] PHP Warning:  Undefined array key "capabilities" in wp-includes/class-wp-roles.php on line 310
[07-Aug-2026 10:00:41 UTC] PHP Warning:  Undefined array key "capabilities" in wp-includes/class-wp-user-query.php on line 485
[07-Aug-2026 10:00:41 UTC] PHP Fatal error:  Uncaught TypeError: array_filter(): Argument #1 ($array) must be of type array, null given in wp-includes/class-wp-user-query.php:485
Stack trace:
#0 wp-includes/class-wp-user-query.php(485): array_filter(NULL)
#1 wp-includes/class-wp-user-query.php(79): WP_User_Query->prepare_query(Array)
#2 wp-includes/user.php(879): WP_User_Query->__construct(Array)
#3 wp-includes/user.php(1810): get_users(Array)
#4 ...: wp_dropdown_users(Array)
  thrown in wp-includes/class-wp-user-query.php on line 485

After the fix, same broken role, same trigger: no warnings, no errors, debug.log isn't even created, and the query returns results correctly instead of crashing.

Testing

  • Added two unit tests covering both failure modes (missing key, and a non-array value). Verified they fail at the correct line (class-wp-user-query.php:485, matching the original report) without the fix, and pass with it.
  • Ran the full user and capabilities test groups (1,343 tests, 4,530 assertions) to confirm no regressions.

Trac ticket: https://core.trac.wordpress.org/ticket/62600

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Sonnet 5
Used for: Investigating the root cause (including finding the second, unreported crash site in WP_Roles::init_roles()), implementing the fix in both files, writing and running the unit tests, and reproducing the bug against a live WordPress install to verify the before/after behavior shown above. All changes were reviewed and directed by me.


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.

…has no capabilities array

A role stored without a 'capabilities' key, or with a non-array value for
it, causes two separate problems:

- A fatal TypeError in WP_User_Query::prepare_query() when querying by
  capability (e.g. wp_dropdown_users() on the classic Author meta box),
  since array_filter() is called directly on $role_data['capabilities'].
- The same unguarded assumption in WP_Roles::init_roles(), which runs on
  effectively every request that initializes roles, not just
  capability-filtered queries.

This can happen when a plugin registers a role without capabilities and is
later deactivated, leaving the malformed role behind in the site's
user_roles option.

Both call sites now treat a missing or non-array 'capabilities' value as
an empty array instead of assuming it is always present and always an
array.

Props geekofshire, umeshnevase for the initial investigation and patches
in WordPress#8351 and WordPress#8823, which this builds on and extends to also cover
WP_Roles::init_roles().

Includes unit tests covering both failure modes.

Trac ticket: https://core.trac.wordpress.org/ticket/62600
@github-actions

github-actions Bot commented Aug 7, 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 mebo.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The 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

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

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.

1 participant