Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/wp-admin/includes/class-wp-posts-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,10 @@ public function inline_edit() {
'sort_column' => 'menu_order, post_title',
);

if ( current_user_can( $post_type_object->cap->read_private_posts ) ) {
$dropdown_args['post_status'] = array( 'publish', 'private' );
}

if ( $bulk ) {
$dropdown_args['show_option_no_change'] = __( '— No Change —' );
$dropdown_args['id'] = 'bulk_edit_post_parent';
Expand Down
5 changes: 5 additions & 0 deletions src/wp-admin/includes/meta-boxes.php
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,11 @@ function page_attributes_meta_box( $post ) {
'echo' => 0,
);

$post_type_object = get_post_type_object( $post->post_type );
if ( current_user_can( $post_type_object->cap->read_private_posts ) ) {
$dropdown_args['post_status'] = array( 'publish', 'private' );
}

/**
* Filters the arguments used to generate a Pages drop-down element.
*
Expand Down
192 changes: 180 additions & 12 deletions src/wp-includes/abilities-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -396,33 +396,201 @@ function wp_get_ability( string $name ): ?WP_Ability {
}

/**
* Retrieves all registered abilities.
* Retrieves registered abilities, optionally filtered by the given arguments.
*
* Returns an array of all ability instances currently registered in the system.
* Use this for discovery, debugging, or building administrative interfaces.
* When called without arguments, returns all registered abilities. When called
* with an $args array, returns only abilities that match every specified condition.
*
* Example:
* Filtering pipeline (executed in order):
*
* 1. Declarative filters (`category`, `namespace`, `meta`) — per-item, AND logic between
* arg types, OR logic within multi-value `category` arrays.
* 2. `match_callback` — per-item, caller-scoped. Return true to include, false to exclude.
* 3. `wp_get_abilities_match` filter — per-item, ecosystem-scoped. Plugins can enforce
* universal inclusion rules regardless of what the caller passed.
* 4. `result_callback` — on the full matched array, caller-scoped. Sort, slice, or reshape.
* 5. `wp_get_abilities_result` filter — on the full array, ecosystem-scoped.
*
* Steps 1–3 run inside a single loop over the registry — no extra iteration.
*
* Examples:
*
* // Prints information about all available abilities.
* // All abilities (unchanged behaviour).
* $abilities = wp_get_abilities();
* foreach ( $abilities as $ability ) {
* echo $ability->get_label() . ': ' . $ability->get_description() . "\n";
* }
*
* // Filter by category.
* $abilities = wp_get_abilities( array( 'category' => 'content' ) );
*
* // Filter by multiple categories (OR logic).
* $abilities = wp_get_abilities( array( 'category' => array( 'content', 'settings' ) ) );
*
* // Filter by namespace.
* $abilities = wp_get_abilities( array( 'namespace' => 'woocommerce' ) );
*
* // Filter by meta.
* $abilities = wp_get_abilities( array( 'meta' => array( 'show_in_rest' => true ) ) );
*
* // Combine filters (AND logic between arg types).
* $abilities = wp_get_abilities( array(
* 'category' => 'content',
* 'namespace' => 'core',
* 'meta' => array( 'show_in_rest' => true ),
* ) );
*
* // Caller-scoped per-item callback.
* $abilities = wp_get_abilities( array(
* 'match_callback' => function ( WP_Ability $ability ) {
* return current_user_can( 'manage_options' );
* },
* ) );
*
* // Caller-scoped result callback (sort + paginate).
* $abilities = wp_get_abilities( array(
* 'result_callback' => function ( array $abilities ) {
* usort( $abilities, fn( $a, $b ) => strcasecmp( $a->get_label(), $b->get_label() ) );
* return array_slice( $abilities, 0, 10 );
* },
* ) );
*
* @since 6.9.0
* @since 7.1.0 Added the `$args` parameter for filtering support.
*
* @see WP_Abilities_Registry::get_all_registered()
*
* @return WP_Ability[] An array of registered WP_Ability instances. Returns an empty
* array if no abilities are registered or if the registry is unavailable.
* @param array $args {
* Optional. Arguments to filter the returned abilities. Default empty array (returns all).
*
* @type string|string[] $category Filter by category slug. A single string or an array of
* slugs — abilities matching any of the given slugs are
* included (OR logic within this arg type).
* @type string $namespace Filter by ability namespace prefix. Pass the namespace
* without a trailing slash, e.g. `'woocommerce'` matches
* `'woocommerce/create-order'`.
* @type array $meta Filter by meta key/value pairs. All conditions must
* match (AND logic). Supports nested arrays for structured
* meta, e.g. `array( 'mcp' => array( 'public' => true ) )`.
* @type callable $match_callback Optional. A callback invoked per ability after declarative
* filters. Receives a WP_Ability instance, returns bool.
* Return true to include, false to exclude.
* @type callable $result_callback Optional. A callback invoked once on the full matched
* array. Receives WP_Ability[], must return WP_Ability[].
* Use for sorting, slicing, or reshaping the result.
* }
* @return WP_Ability[] An array of registered WP_Ability instances matching the given args.
* Returns an empty array if no abilities are registered, the registry is
* unavailable, or no abilities match the given args.
*/
function wp_get_abilities(): array {
function wp_get_abilities( array $args = array() ): array {
$registry = WP_Abilities_Registry::get_instance();
if ( null === $registry ) {
return array();
}

return $registry->get_all_registered();
$abilities = $registry->get_all_registered();

// Bail early when no filtering is requested.
if ( empty( $args ) ) {
return $abilities;
}

$category = isset( $args['category'] ) ? (array) $args['category'] : array();
$namespace = isset( $args['namespace'] ) && is_string( $args['namespace'] ) ? rtrim( $args['namespace'], '/' ) . '/' : '';
$meta = isset( $args['meta'] ) && is_array( $args['meta'] ) ? $args['meta'] : array();
$match_callback = isset( $args['match_callback'] ) && is_callable( $args['match_callback'] ) ? $args['match_callback'] : null;
$result_callback = isset( $args['result_callback'] ) && is_callable( $args['result_callback'] ) ? $args['result_callback'] : null;

$matched = array();

foreach ( $abilities as $ability ) {
// Step 1a: Filter by category (OR logic within the arg).
if ( ! empty( $category ) && ! in_array( $ability->get_category(), $category, true ) ) {
continue;
}

// Step 1b: Filter by namespace prefix.
if ( '' !== $namespace && ! str_starts_with( $ability->get_name(), $namespace ) ) {
continue;
}

// Step 1c: Filter by meta key/value pairs (AND logic, supports nested arrays).
if ( ! empty( $meta ) && ! wp_get_abilities_match_meta( $ability->get_meta(), $meta ) ) {
continue;
}

// Step 2: Caller-scoped per-item callback.
$include = true;
if ( null !== $match_callback ) {
$include = (bool) call_user_func( $match_callback, $ability );
}

/**
* Filters whether an individual ability should be included in the result set.
*
* Fires after the declarative filters and the caller-scoped match_callback.
* Plugins can use this to enforce universal inclusion rules regardless of
* what the caller passed in $args.
*
* @since 7.1.0
*
* @param bool $include Whether to include the ability. Default true (after declarative filters pass).
* @param WP_Ability $ability The ability instance being evaluated.
* @param array $args The full $args array passed to wp_get_abilities().
*/
$include = (bool) apply_filters( 'wp_get_abilities_match', $include, $ability, $args );

if ( $include ) {
$matched[] = $ability;
}
}

// Step 4: Caller-scoped result callback.
if ( null !== $result_callback ) {
$matched = (array) call_user_func( $result_callback, $matched );
}

/**
* Filters the full list of matched abilities after all per-item filtering is complete.
*
* Fires after the caller-scoped result_callback. Plugins can use this to sort,
* paginate, or reshape the final result set universally.
*
* @since 7.1.0
*
* @param WP_Ability[] $matched The matched abilities after all filtering.
* @param array $args The full $args array passed to wp_get_abilities().
*/
return (array) apply_filters( 'wp_get_abilities_result', $matched, $args );
}

/**
* Checks whether an ability's meta array matches a set of required key/value conditions.
*
* All conditions must match (AND logic). Supports nested arrays for structured meta,
* e.g. `array( 'mcp' => array( 'public' => true ) )`.
*
* @since 7.1.0
* @access private
*
* @param array $meta The ability's meta array.
* @param array $conditions The required key/value conditions to match against.
* @return bool True if all conditions match, false otherwise.
*/
function wp_get_abilities_match_meta( array $meta, array $conditions ): bool {
foreach ( $conditions as $key => $value ) {
if ( ! array_key_exists( $key, $meta ) ) {
return false;
}

if ( is_array( $value ) ) {
if ( ! is_array( $meta[ $key ] ) || ! wp_get_abilities_match_meta( $meta[ $key ], $value ) ) {
return false;
}
} elseif ( $meta[ $key ] !== $value ) {
return false;
}
}

return true;
}

/**
Expand Down
101 changes: 101 additions & 0 deletions src/wp-includes/general-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,107 @@ function wp_login_form( $args = array() ) {
}
}

/**
* Outputs or returns the lost password form for use anywhere on a WordPress site.
*
* @since 7.1.0
*
* @param array $args {
* Optional. Array of arguments to control the form output. Default empty array.
*
* @type bool $echo Whether to display the form or return the output. Default true.
* @type string $redirect URL to redirect to after submitting the form. Default empty string.
* @type string $form_id ID attribute for the form element. Default 'lostpasswordform'.
* @type string $id_username ID attribute for the username input. Default 'user_login'.
* @type string $id_submit ID attribute for the submit button. Default 'wp-submit'.
* @type string $label_username Label for the username input. Default 'Username or Email Address'.
* @type string $label_submit Label for the submit button. Default 'Get New Password'.
* }
* @return void|string Void if 'echo' argument is true, lost password form HTML if 'echo' is false.
*/
function wp_lostpassword_form( $args = array() ) {
$defaults = array(
'echo' => true,
'redirect' => '',
'form_id' => 'lostpasswordform',
'id_username' => 'user_login',
'id_submit' => 'wp-submit',
'label_username' => __( 'Username or Email Address' ),
'label_submit' => __( 'Get New Password' ),
);

/**
* Filters the default lost password form arguments.
*
* @since 7.1.0
*
* @see wp_lostpassword_form()
*
* @param array $defaults An array of default lost password form arguments.
*/
$args = wp_parse_args( $args, apply_filters( 'lostpassword_form_defaults', $defaults ) );

$user_login = '';
if ( isset( $_POST['user_login'] ) && is_string( $_POST['user_login'] ) ) {
$user_login = wp_unslash( $_POST['user_login'] );
}

/**
* Filters content to display at the top of the lost password form.
*
* The filter evaluates just following the opening form tag element.
*
* @since 7.1.0
*
* @param string $content Content to display. Default empty.
* @param array $args Array of lost password form arguments.
*/
$lostpassword_form_top = apply_filters( 'lostpassword_form_top', '', $args );

/**
* Filters content to display at the bottom of the lost password form.
*
* The filter evaluates just preceding the closing form tag element.
*
* @since 7.1.0
*
* @param string $content Content to display. Default empty.
* @param array $args Array of lost password form arguments.
*/
$lostpassword_form_bottom = apply_filters( 'lostpassword_form_bottom', '', $args );

ob_start();
?>
<form name="<?php echo esc_attr( $args['form_id'] ); ?>" id="<?php echo esc_attr( $args['form_id'] ); ?>" action="<?php echo esc_url( network_site_url( 'wp-login.php?action=lostpassword', 'login_post' ) ); ?>" method="post">
<?php echo $lostpassword_form_top; ?>
<p>
<label for="<?php echo esc_attr( $args['id_username'] ); ?>"><?php echo esc_html( $args['label_username'] ); ?></label>
<input type="text" name="user_login" id="<?php echo esc_attr( $args['id_username'] ); ?>" class="input ltr" value="<?php echo esc_attr( $user_login ); ?>" size="20" autocapitalize="off" autocomplete="username" required="required" />
</p>
<?php
/**
* Fires inside the lost password form tags, before the hidden fields.
*
* @since 2.1.0
*/
do_action( 'lostpassword_form' );
?>
<input type="hidden" name="redirect_to" value="<?php echo esc_attr( $args['redirect'] ); ?>" />
<p class="submit">
<input type="submit" name="wp-submit" id="<?php echo esc_attr( $args['id_submit'] ); ?>" class="button button-primary button-large" value="<?php echo esc_attr( $args['label_submit'] ); ?>" />
</p>
<?php echo $lostpassword_form_bottom; ?>
</form>
<?php
$form = ob_get_clean();

if ( $args['echo'] ) {
echo $form;
} else {
return $form;
}
}

/**
* Returns the URL that allows the user to reset the lost password.
*
Expand Down
Loading
Loading