diff --git a/backport-changelog/6.8/7773.md b/backport-changelog/6.8/7773.md new file mode 100644 index 00000000000000..73cb8288a5d29a --- /dev/null +++ b/backport-changelog/6.8/7773.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7773 + +* https://github.com/WordPress/gutenberg/pull/66294 diff --git a/lib/compat/wordpress-6.8/class-gutenberg-rest-post-counts-controller.php b/lib/compat/wordpress-6.8/class-gutenberg-rest-post-counts-controller.php index ed72f37dcc3f47..fd7be90c1f1f13 100644 --- a/lib/compat/wordpress-6.8/class-gutenberg-rest-post-counts-controller.php +++ b/lib/compat/wordpress-6.8/class-gutenberg-rest-post-counts-controller.php @@ -75,7 +75,7 @@ public function get_item_permissions_check( $request ) { ); } - if ( ! current_user_can( $post_type->cap->edit_posts ) ) { + if ( ! current_user_can( $post_type->cap->read ) ) { return new WP_Error( 'rest_cannot_read', __( 'Sorry, you are not allowed to read post counts for this post type.' ), @@ -112,11 +112,24 @@ public function get_item( $request ) { * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $item, $request ) { - $data = array(); - $fields = $this->get_fields_for_response( $request ); - foreach ( $fields as $field ) { - if ( property_exists( $item, $field ) ) { - $data[ $field ] = (int) $item->$field; + $data = array(); + + if ( ! empty( $item ) ) { + /* + * The fields comprise all non-internal post statuses, + * including any custom statuses that may be registered. + * 'trash' is an exception, so if it exists, it is added separately. + */ + $post_stati = get_post_stati( array( 'internal' => false ) ); + + if ( get_post_status_object( 'trash' ) ) { + $post_stati[] = 'trash'; + } + // Include all public statuses in the response if there is a count. + foreach ( $post_stati as $status ) { + if ( isset( $item->$status ) ) { + $data[ $status ] = (int) $item->$status; + } } } @@ -124,19 +137,7 @@ public function prepare_item_for_response( $item, $request ) { $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); - $response = rest_ensure_response( $data ); - - /** - * Filters post type counts data for the REST API. - * Allows modification of the post type counts data right before it is returned. - * - * @since 6.8.0 - * - * @param WP_REST_Response $response The response object. - * @param object $item The original post counts object. - * @param WP_REST_Request $request Request used to generate the response. - */ - return apply_filters( 'rest_prepare_post_counts', $response, $item, $request ); + return rest_ensure_response( $data ); } /** @@ -151,32 +152,24 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $this->schema ); } - /* - * The fields comprise all non-internal post stati, - * including any custom statuses that may be registered. - * 'trash' is an exception, so if it exists, it is added separately. - */ - $post_statuses = get_post_stati( array( 'internal' => false ) ); - - if ( get_post_status_object( 'trash' ) ) { - $post_statuses[] = 'trash'; - } - $schema_properties = array(); - foreach ( $post_statuses as $post_status ) { - $schema_properties[ $post_status ] = array( - // translators: %s: Post status. - 'description' => sprintf( __( 'The number of posts with the status %s.' ), $post_status ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ); - } - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'post-counts', - 'type' => 'object', - 'properties' => $schema_properties, + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'post-counts', + 'type' => 'object', + /* + * Use a pattern matcher for post status keys. + * This allows for custom post statuses to be included, + * which can be registered after the schema is generated. + */ + 'patternProperties' => array( + '^\w+$' => array( + 'description' => __( 'The number of posts for a given status.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + ), + 'additionalProperties' => false, ); $this->schema = $schema; diff --git a/phpunit/class-gutenberg-rest-post-counts-controller-test.php b/phpunit/class-gutenberg-rest-post-counts-controller-test.php index 5b012deef10e39..681ed004f8e514 100644 --- a/phpunit/class-gutenberg-rest-post-counts-controller-test.php +++ b/phpunit/class-gutenberg-rest-post-counts-controller-test.php @@ -61,14 +61,6 @@ public function test_register_routes() { $this->assertArrayHasKey( '/wp/v2/counts/(?P[\w-]+)', $routes ); } - public function test_context_param() { - $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/counts/post' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); - $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); - } - /** * @covers Gutenberg_REST_Post_Counts_Controller::get_item_schema */ @@ -76,15 +68,10 @@ public function test_get_item_schema() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/counts/post' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $properties = $data['schema']['properties']; + $properties = $data['schema']['patternProperties']; - $this->assertCount( 6, $properties ); - $this->assertArrayHasKey( 'publish', $properties ); - $this->assertArrayHasKey( 'future', $properties ); - $this->assertArrayHasKey( 'draft', $properties ); - $this->assertArrayHasKey( 'pending', $properties ); - $this->assertArrayHasKey( 'private', $properties ); - $this->assertArrayHasKey( 'trash', $properties ); + $this->assertCount( 1, $properties ); + $this->assertArrayHasKey( '^\w+$', $properties ); } /** @@ -110,6 +97,7 @@ public function test_get_item_response() { */ public function test_get_item() { wp_set_current_user( self::$admin_id ); + register_post_status( 'post_counts_status', array( 'public' => true ) ); $published = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $future = self::factory()->post->create( @@ -122,6 +110,7 @@ public function test_get_item() { $pending = self::factory()->post->create( array( 'post_status' => 'pending' ) ); $private = self::factory()->post->create( array( 'post_status' => 'private' ) ); $trashed = self::factory()->post->create( array( 'post_status' => 'trash' ) ); + $custom = self::factory()->post->create( array( 'post_status' => 'post_counts_status' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/counts/post' ); $response = rest_get_server()->dispatch( $request ); @@ -133,6 +122,7 @@ public function test_get_item() { $this->assertSame( 1, $data['pending'], 'Pending post count mismatch.' ); $this->assertSame( 1, $data['private'], 'Private post count mismatch.' ); $this->assertSame( 1, $data['trash'], 'Trashed post count mismatch.' ); + $this->assertSame( 1, $data['post_counts_status'], 'Custom post count mismatch.' ); wp_delete_post( $published, true ); wp_delete_post( $future, true ); @@ -140,6 +130,26 @@ public function test_get_item() { wp_delete_post( $pending, true ); wp_delete_post( $private, true ); wp_delete_post( $trashed, true ); + wp_delete_post( $custom, true ); + unset( $GLOBALS['wp_post_statuses']['post_counts_status'] ); + } + + /** + * @covers WP_REST_Post_Counts_Controller::get_item + */ + public function test_get_item_with_sanitized_custom_post_status() { + wp_set_current_user( self::$admin_id ); + register_post_status( '#<>post-me_AND9!', array( 'public' => true ) ); + + $custom = self::factory()->post->create( array( 'post_status' => 'post-me_and9' ) ); + $request = new WP_REST_Request( 'GET', '/wp/v2/counts/post' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 1, $data['post-me_and9'], 'Custom post count mismatch.' ); + + wp_delete_post( $custom, true ); + unset( $GLOBALS['wp_post_statuses']['post-me_and9'] ); } /** @@ -209,4 +219,11 @@ public function test_update_item() { public function test_prepare_item() { // Controller does not implement test_prepare_item(). } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Controller does not implement context_param(). + } }