From 6123cb22060a25fa2a0977968d2aa2024bc6c1e4 Mon Sep 17 00:00:00 2001 From: ramon Date: Tue, 22 Oct 2024 11:22:12 +1100 Subject: [PATCH 01/11] First commit: create a new endpoint to retrieve post counts by status. --- ...-gutenberg-rest-post-counts-controller.php | 217 ++++++++++++++++++ lib/compat/wordpress-6.8/rest-api.php | 11 + lib/load.php | 1 + ...nberg-rest-post-counts-controller-test.php | 213 +++++++++++++++++ 4 files changed, 442 insertions(+) create mode 100644 lib/compat/wordpress-6.8/class-gutenberg-rest-post-counts-controller.php create mode 100644 phpunit/class-gutenberg-rest-post-counts-controller-test.php 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 new file mode 100644 index 00000000000000..21875839c7a770 --- /dev/null +++ b/lib/compat/wordpress-6.8/class-gutenberg-rest-post-counts-controller.php @@ -0,0 +1,217 @@ +namespace = 'wp/v2'; + $this->rest_base = 'counts'; + } + + /** + * Registers the routes for post counts. + * + * @since 6.8.0 + * + * @see register_rest_route() + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + 'args' => array( + 'post_type' => array( + 'description' => __( 'An alphanumeric identifier for the post type.' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Checks if a given request has access to read post counts. + * + * @since 6.8.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_item_permissions_check( $request ) { + $post_type = get_post_type_object( $request['post_type'] ); + + if ( ! $post_type ) { + return new WP_Error( + 'rest_invalid_post_type', + __( 'Invalid post type.' ), + array( 'status' => 404 ) + ); + } + + if ( ! current_user_can( $post_type->cap->edit_posts ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to read post counts for this post type.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Retrieves post counts for a specific post type. + * + * @since 6.8.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $post_type = $request['post_type']; + $counts = wp_count_posts( $post_type ); + + if ( ! $counts ) { + return new WP_Error( + 'rest_post_counts_error', + __( 'Could not retrieve post counts.' ), + array( 'status' => 500 ) + ); + } + + $data = $this->prepare_item_for_response( $counts, $request ); + return rest_ensure_response( $data ); + } + + /** + * Prepares post counts for response. + * + * @since 6.8.0 + * + * @param object $item Post counts data. + * @param WP_REST_Request $request Request object. + * @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 ] = intval( $item->$field ); + } + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $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 ); + } + + /** + * Retrieves the post counts schema, conforming to JSON Schema. + * + * @since 6.8.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'post-counts', + 'type' => 'object', + 'properties' => array( + 'publish' => array( + 'description' => __( 'The number of published posts.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'future' => array( + 'description' => __( 'The number of future scheduled posts.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'draft' => array( + 'description' => __( 'The number of draft posts.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'pending' => array( + 'description' => __( 'The number of pending posts.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'private' => array( + 'description' => __( 'The number of private posts.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'trash' => array( + 'description' => __( 'The number of trashed posts.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'auto-draft' => array( + 'description' => __( 'The number of auto-draft posts.' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/lib/compat/wordpress-6.8/rest-api.php b/lib/compat/wordpress-6.8/rest-api.php index b94e42d5f2ccd0..77a3b48785db22 100644 --- a/lib/compat/wordpress-6.8/rest-api.php +++ b/lib/compat/wordpress-6.8/rest-api.php @@ -87,3 +87,14 @@ function gutenberg_add_default_template_types_to_index( WP_REST_Response $respon } add_filter( 'rest_index', 'gutenberg_add_default_template_types_to_index' ); + +/** + * Registers the Post Counts REST API routes. + */ +function gutenberg_register_post_counts_routes() { + $post_counts_controller = new Gutenberg_REST_Post_Counts_Controller(); + $post_counts_controller->register_routes(); +} + +add_action( 'rest_api_init', 'gutenberg_register_post_counts_routes' ); + diff --git a/lib/load.php b/lib/load.php index 26af78f3173c53..a322fe430940ae 100644 --- a/lib/load.php +++ b/lib/load.php @@ -45,6 +45,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.8/block-comments.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-comment-controller-6-8.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php'; + require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-post-counts-controller.php'; require __DIR__ . '/compat/wordpress-6.8/rest-api.php'; // Plugin specific code. diff --git a/phpunit/class-gutenberg-rest-post-counts-controller-test.php b/phpunit/class-gutenberg-rest-post-counts-controller-test.php new file mode 100644 index 00000000000000..0fae05522e3adb --- /dev/null +++ b/phpunit/class-gutenberg-rest-post-counts-controller-test.php @@ -0,0 +1,213 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + + if ( is_multisite() ) { + grant_super_admin( self::$admin_id ); + } + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + } + + public function set_up() { + parent::set_up(); + + register_post_type( + 'private-cpt', + array( + 'public' => false, + 'publicly_queryable' => false, + 'show_ui' => true, + 'show_in_menu' => true, + 'show_in_rest' => true, + 'rest_base' => 'private-cpts', + 'capability_type' => 'post', + ) + ); + } + + public function tear_down() { + unregister_post_type( 'private-cpt' ); + parent::tear_down(); + } + + /** + * @covers Gutenberg_REST_Post_Counts_Controller::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/counts/(?P[\w-]+)', $routes ); + } + + public function test_context_param() { + // Single. + $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', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + } + + /** + * @covers Gutenberg_REST_Post_Counts_Controller::et_item_schema + */ + 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']; + + $this->assertCount( 7, $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->assertArrayHasKey( 'auto-draft', $properties ); + } + + /** + * @covers Gutenberg_REST_Post_Counts_Controller::get_item + */ + public function test_get_item_response() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/counts/post' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertArrayHasKey( 'publish', $data ); + $this->assertArrayHasKey( 'future', $data ); + $this->assertArrayHasKey( 'draft', $data ); + $this->assertArrayHasKey( 'pending', $data ); + $this->assertArrayHasKey( 'private', $data ); + $this->assertArrayHasKey( 'trash', $data ); + $this->assertArrayHasKey( 'auto-draft', $data ); + } + + /** + * @covers Gutenberg_REST_Post_Counts_Controller::get_item + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + + $published = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $future = self::factory()->post->create( array( + 'post_status' => 'future', + 'post_date' => date('Y-m-d H:i:s', strtotime('+1 day')) + ) ); + $draft = self::factory()->post->create( array( 'post_status' => 'draft' ) ); + $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' ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/counts/post' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 1, $data['publish'], 'Published post count mismatch.' ); + $this->assertSame( 1, $data['future'], 'Future post count mismatch.' ); + $this->assertSame( 1, $data['draft'], 'Draft post count mismatch.' ); + $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.' ); + + wp_delete_post( $published, true ); + wp_delete_post( $future, true ); + wp_delete_post( $draft, true ); + wp_delete_post( $pending, true ); + wp_delete_post( $private, true ); + wp_delete_post( $trashed, true ); + } + + /** + * @covers Gutenberg_REST_Post_Counts_Controller::get_item_permissions_check + */ + public function test_get_item_private_post_type() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/counts/private-cpt' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + } + + /** + * @covers Gutenberg_REST_Post_Counts_Controller::get_item_permissions_check + */ + public function test_get_item_invalid_post_type() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/counts/invalid-type' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_post_type', $response, 404 ); + } + + /** + * @covers Gutenberg_REST_Post_Counts_Controller::get_item_permissions_check + */ + public function test_get_item_invalid_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/counts/post' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_items() { + // Controller does not implement delete_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Controller does not implement delete_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Controller does not implement test_create_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Controller does not implement test_update_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Controller does not implement test_prepare_item(). + } +} From e3641f4d128225ddd202788068eeafc55e66fb27 Mon Sep 17 00:00:00 2001 From: ramon Date: Tue, 22 Oct 2024 12:29:31 +1100 Subject: [PATCH 02/11] Linty McGinty --- ...-gutenberg-rest-post-counts-controller.php | 10 ++++---- ...nberg-rest-post-counts-controller-test.php | 24 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) 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 21875839c7a770..31bf3c108a2226 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 @@ -37,7 +37,7 @@ public function register_routes() { $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( - 'args' => array( + 'args' => array( 'post_type' => array( 'description' => __( 'An alphanumeric identifier for the post type.' ), 'type' => 'string', @@ -96,7 +96,7 @@ public function get_item_permissions_check( $request ) { */ public function get_item( $request ) { $post_type = $request['post_type']; - $counts = wp_count_posts( $post_type ); + $counts = wp_count_posts( $post_type ); if ( ! $counts ) { return new WP_Error( @@ -120,7 +120,7 @@ public function get_item( $request ) { * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $item, $request ) { - $data = array(); + $data = array(); $fields = $this->get_fields_for_response( $request ); foreach ( $fields as $field ) { @@ -130,8 +130,8 @@ public function prepare_item_for_response( $item, $request ) { } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); $response = rest_ensure_response( $data ); diff --git a/phpunit/class-gutenberg-rest-post-counts-controller-test.php b/phpunit/class-gutenberg-rest-post-counts-controller-test.php index 0fae05522e3adb..0df45d3cd4006b 100644 --- a/phpunit/class-gutenberg-rest-post-counts-controller-test.php +++ b/phpunit/class-gutenberg-rest-post-counts-controller-test.php @@ -18,7 +18,7 @@ class Gutenberg_Test_REST_Post_Counts_Controller extends WP_Test_REST_Controller public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$admin_id = $factory->user->create( array( - 'role' => 'administrator', + 'role' => 'administrator', ) ); @@ -74,9 +74,9 @@ public function test_context_param() { * @covers Gutenberg_REST_Post_Counts_Controller::et_item_schema */ 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(); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/counts/post' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); $properties = $data['schema']['properties']; $this->assertCount( 7, $properties ); @@ -94,9 +94,9 @@ public function test_get_item_schema() { */ public function test_get_item_response() { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/counts/post' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/counts/post' ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertSame( 200, $response->get_status() ); $this->assertArrayHasKey( 'publish', $data ); @@ -115,10 +115,12 @@ public function test_get_item() { wp_set_current_user( self::$admin_id ); $published = self::factory()->post->create( array( 'post_status' => 'publish' ) ); - $future = self::factory()->post->create( array( - 'post_status' => 'future', - 'post_date' => date('Y-m-d H:i:s', strtotime('+1 day')) - ) ); + $future = self::factory()->post->create( + array( + 'post_status' => 'future', + 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), + ) + ); $draft = self::factory()->post->create( array( 'post_status' => 'draft' ) ); $pending = self::factory()->post->create( array( 'post_status' => 'pending' ) ); $private = self::factory()->post->create( array( 'post_status' => 'private' ) ); @@ -126,7 +128,7 @@ public function test_get_item() { $request = new WP_REST_Request( 'GET', '/wp/v2/counts/post' ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertSame( 1, $data['publish'], 'Published post count mismatch.' ); $this->assertSame( 1, $data['future'], 'Future post count mismatch.' ); From faa66bb0036e11fddb063fb7b5535475479877ad Mon Sep 17 00:00:00 2001 From: ramon Date: Tue, 22 Oct 2024 12:31:19 +1100 Subject: [PATCH 03/11] Linty McGinty --- lib/compat/wordpress-6.8/rest-api.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/compat/wordpress-6.8/rest-api.php b/lib/compat/wordpress-6.8/rest-api.php index 77a3b48785db22..6277b5b7e1ef19 100644 --- a/lib/compat/wordpress-6.8/rest-api.php +++ b/lib/compat/wordpress-6.8/rest-api.php @@ -97,4 +97,3 @@ function gutenberg_register_post_counts_routes() { } add_action( 'rest_api_init', 'gutenberg_register_post_counts_routes' ); - From bcf76be63472c4dc78817688f98adb230f37f8f6 Mon Sep 17 00:00:00 2001 From: ramon Date: Tue, 22 Oct 2024 12:33:34 +1100 Subject: [PATCH 04/11] use gmdate instead of date --- phpunit/class-gutenberg-rest-post-counts-controller-test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit/class-gutenberg-rest-post-counts-controller-test.php b/phpunit/class-gutenberg-rest-post-counts-controller-test.php index 0df45d3cd4006b..c58543121b5366 100644 --- a/phpunit/class-gutenberg-rest-post-counts-controller-test.php +++ b/phpunit/class-gutenberg-rest-post-counts-controller-test.php @@ -118,7 +118,7 @@ public function test_get_item() { $future = self::factory()->post->create( array( 'post_status' => 'future', - 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), ) ); $draft = self::factory()->post->create( array( 'post_status' => 'draft' ) ); From 3895345bd42df9ad74e41fc985cdcf5ddd36a169 Mon Sep 17 00:00:00 2001 From: ramon Date: Mon, 28 Oct 2024 14:37:33 +1100 Subject: [PATCH 05/11] Because there can be custom post statuses, use get_post_stati to return all globally registered custom post statuses --- ...-gutenberg-rest-post-counts-controller.php | 79 ++++++------------- ...nberg-rest-post-counts-controller-test.php | 4 +- 2 files changed, 25 insertions(+), 58 deletions(-) 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 31bf3c108a2226..35240ec64e2240 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 @@ -97,16 +97,8 @@ public function get_item_permissions_check( $request ) { public function get_item( $request ) { $post_type = $request['post_type']; $counts = wp_count_posts( $post_type ); + $data = $this->prepare_item_for_response( $counts, $request ); - if ( ! $counts ) { - return new WP_Error( - 'rest_post_counts_error', - __( 'Could not retrieve post counts.' ), - array( 'status' => 500 ) - ); - } - - $data = $this->prepare_item_for_response( $counts, $request ); return rest_ensure_response( $data ); } @@ -122,10 +114,9 @@ public function get_item( $request ) { 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 ] = intval( $item->$field ); + $data[ $field ] = (int) $item->$field; } } @@ -160,54 +151,32 @@ 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' ), + 'readonly' => true, + ); + } + $schema = array( '$schema' => 'http://json-schema.org/draft-04/schema#', 'title' => 'post-counts', 'type' => 'object', - 'properties' => array( - 'publish' => array( - 'description' => __( 'The number of published posts.' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'future' => array( - 'description' => __( 'The number of future scheduled posts.' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'draft' => array( - 'description' => __( 'The number of draft posts.' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'pending' => array( - 'description' => __( 'The number of pending posts.' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'private' => array( - 'description' => __( 'The number of private posts.' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'trash' => array( - 'description' => __( 'The number of trashed posts.' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'auto-draft' => array( - 'description' => __( 'The number of auto-draft posts.' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - ), + 'properties' => $schema_properties, ); $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 c58543121b5366..f72afca2afa425 100644 --- a/phpunit/class-gutenberg-rest-post-counts-controller-test.php +++ b/phpunit/class-gutenberg-rest-post-counts-controller-test.php @@ -79,14 +79,13 @@ public function test_get_item_schema() { $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 7, $properties ); + $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->assertArrayHasKey( 'auto-draft', $properties ); } /** @@ -105,7 +104,6 @@ public function test_get_item_response() { $this->assertArrayHasKey( 'pending', $data ); $this->assertArrayHasKey( 'private', $data ); $this->assertArrayHasKey( 'trash', $data ); - $this->assertArrayHasKey( 'auto-draft', $data ); } /** From 71404bdae7b91df778c78062f8cb5efeb27650ea Mon Sep 17 00:00:00 2001 From: ramon Date: Mon, 28 Oct 2024 14:55:25 +1100 Subject: [PATCH 06/11] Allow embed --- .../class-gutenberg-rest-post-counts-controller.php | 2 +- phpunit/class-gutenberg-rest-post-counts-controller-test.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 35240ec64e2240..ed72f37dcc3f47 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 @@ -167,7 +167,7 @@ public function get_item_schema() { // translators: %s: Post status. 'description' => sprintf( __( 'The number of posts with the status %s.' ), $post_status ), 'type' => 'integer', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); } diff --git a/phpunit/class-gutenberg-rest-post-counts-controller-test.php b/phpunit/class-gutenberg-rest-post-counts-controller-test.php index f72afca2afa425..30e7c72d580262 100644 --- a/phpunit/class-gutenberg-rest-post-counts-controller-test.php +++ b/phpunit/class-gutenberg-rest-post-counts-controller-test.php @@ -62,12 +62,11 @@ public function test_register_routes() { } public function test_context_param() { - // Single. $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', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } /** From 17ad33860665e622a6f0ebdd64a0fe77ae3ca9e6 Mon Sep 17 00:00:00 2001 From: ramon Date: Fri, 1 Nov 2024 10:47:20 +1100 Subject: [PATCH 07/11] Update annotation. --- phpunit/class-gutenberg-rest-post-counts-controller-test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit/class-gutenberg-rest-post-counts-controller-test.php b/phpunit/class-gutenberg-rest-post-counts-controller-test.php index 30e7c72d580262..5b012deef10e39 100644 --- a/phpunit/class-gutenberg-rest-post-counts-controller-test.php +++ b/phpunit/class-gutenberg-rest-post-counts-controller-test.php @@ -70,7 +70,7 @@ public function test_context_param() { } /** - * @covers Gutenberg_REST_Post_Counts_Controller::et_item_schema + * @covers Gutenberg_REST_Post_Counts_Controller::get_item_schema */ public function test_get_item_schema() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/counts/post' ); From 927ac875d003648c38169d16d4ea6b054e0c613c Mon Sep 17 00:00:00 2001 From: ramon Date: Tue, 12 Nov 2024 22:01:01 +1100 Subject: [PATCH 08/11] Backport log Sync with changes from Core backport --- backport-changelog/6.8/7773.md | 3 + ...-gutenberg-rest-post-counts-controller.php | 81 +++++++++---------- ...nberg-rest-post-counts-controller-test.php | 49 +++++++---- 3 files changed, 73 insertions(+), 60 deletions(-) create mode 100644 backport-changelog/6.8/7773.md 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(). + } } From 36c68716c43781bb791ae1a51a593d33851c9c0f Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 27 Dec 2024 13:14:55 +1100 Subject: [PATCH 09/11] Update class-gutenberg-rest-post-counts-controller.php --- ...-gutenberg-rest-post-counts-controller.php | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) 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 fd7be90c1f1f13..2f735e58997c7b 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 @@ -67,7 +67,7 @@ public function register_routes() { public function get_item_permissions_check( $request ) { $post_type = get_post_type_object( $request['post_type'] ); - if ( ! $post_type ) { + if ( empty( $post_type ) ) { return new WP_Error( 'rest_invalid_post_type', __( 'Invalid post type.' ), @@ -152,24 +152,35 @@ 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. + * The caveat is that all custom post statuses + * must be registered at the highest priority, otherwise + * the endpoint will not return them. + */ + $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' ), + 'readonly' => true, + ); + } + $schema = array( - '$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, + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'post-counts', + 'type' => 'object', + 'properties' => $schema_properties, ); $this->schema = $schema; From e0a5fdcd334dbe4742ec3c65b2d534e7bee2d205 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 27 Dec 2024 13:15:13 +1100 Subject: [PATCH 10/11] Update class-gutenberg-rest-post-counts-controller-test.php --- ...s-gutenberg-rest-post-counts-controller-test.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/phpunit/class-gutenberg-rest-post-counts-controller-test.php b/phpunit/class-gutenberg-rest-post-counts-controller-test.php index 681ed004f8e514..6f21663d8df392 100644 --- a/phpunit/class-gutenberg-rest-post-counts-controller-test.php +++ b/phpunit/class-gutenberg-rest-post-counts-controller-test.php @@ -68,10 +68,15 @@ 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']['patternProperties']; - - $this->assertCount( 1, $properties ); - $this->assertArrayHasKey( '^\w+$', $properties ); + $properties = $data['schema']['properties']; + + $this->assertCount( 6, $properties ); + $this->assertArrayHasKey( 'publish', $properties ); + $this->assertArrayHasKey( 'future', $properties ); + $this->assertArrayHasKey( 'draft', $properties ); + $this->assertArrayHasKey( 'trash', $properties ); + $this->assertArrayHasKey( 'private', $properties ); + $this->assertArrayHasKey( 'pending', $properties ); } /** From d4a729b990c6f24d50022e47bc8bb52c85bd3b9a Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 27 Dec 2024 13:15:37 +1100 Subject: [PATCH 11/11] Update load.php --- lib/load.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/load.php b/lib/load.php index a322fe430940ae..ddf4f4464f94a0 100644 --- a/lib/load.php +++ b/lib/load.php @@ -45,6 +45,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.8/block-comments.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-comment-controller-6-8.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php'; + require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-hierarchical-sort.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-post-counts-controller.php'; require __DIR__ . '/compat/wordpress-6.8/rest-api.php';