diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php index c7d10fca217ef..cf1ff8bd476d2 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -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'; diff --git a/src/wp-admin/includes/meta-boxes.php b/src/wp-admin/includes/meta-boxes.php index 0884c110b65bd..e5425a1cd6f41 100644 --- a/src/wp-admin/includes/meta-boxes.php +++ b/src/wp-admin/includes/meta-boxes.php @@ -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. * diff --git a/src/wp-includes/abilities-api.php b/src/wp-includes/abilities-api.php index 73ba658f3f10d..76deefb1320ec 100644 --- a/src/wp-includes/abilities-api.php +++ b/src/wp-includes/abilities-api.php @@ -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; } /** diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 47e2aeb2ebb05..e2de8969d65f3 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -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(); + ?> +
+ +

+ + +

+ + +

+ +

+ +
+ get_meta_item( 'show_in_rest' ); - } + $query_args = array( + 'meta' => array( 'show_in_rest' => true ), ); - // Filter by ability category if specified. - $category = $request['category']; - if ( ! empty( $category ) ) { - $abilities = array_filter( - $abilities, - static function ( $ability ) use ( $category ) { - return $ability->get_category() === $category; - } - ); - // Reset array keys after filtering. - $abilities = array_values( $abilities ); + if ( ! empty( $request['category'] ) ) { + $query_args['category'] = $request['category']; + } + + if ( ! empty( $request['namespace'] ) ) { + $query_args['namespace'] = $request['namespace']; } + $abilities = wp_get_abilities( $query_args ); + $page = $request['page']; $per_page = $request['per_page']; $offset = ( $page - 1 ) * $per_page; @@ -432,12 +426,18 @@ public function get_collection_params(): array { 'minimum' => 1, 'maximum' => 100, ), - 'category' => array( + 'category' => array( 'description' => __( 'Limit results to abilities in specific ability category.' ), 'type' => 'string', 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ), + 'namespace' => array( + 'description' => __( 'Limit results to abilities in a specific namespace.' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ), ); } } diff --git a/src/wp-login.php b/src/wp-login.php index abedea82c3589..ad01d08b91bf3 100644 --- a/src/wp-login.php +++ b/src/wp-login.php @@ -886,35 +886,15 @@ function wp_login_viewport_meta() { $errors ); - $user_login = ''; - - if ( isset( $_POST['user_login'] ) && is_string( $_POST['user_login'] ) ) { - $user_login = wp_unslash( $_POST['user_login'] ); - } + wp_lostpassword_form( + array( + 'echo' => true, + 'redirect' => $redirect_to, + ) + ); ?> -
-

- - -

- - -

- -

-
-