From 9505fde90f8606fa02cdd04321e4e7bdfefb566d Mon Sep 17 00:00:00 2001 From: ramon Date: Mon, 9 Dec 2024 17:13:44 +1100 Subject: [PATCH] Alternative to https://github.com/WordPress/gutenberg/pull/66294 Extend post type controller to return total post count --- ...ss-gutenberg-rest-posts-controller-6-8.php | 164 ++++++++++++++ lib/load.php | 2 +- ...s-gutenberg-rest-posts-controller-test.php | 207 ++++++++++++++++++ 3 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 lib/compat/wordpress-6.8/class-gutenberg-rest-posts-controller-6-8.php create mode 100644 phpunit/class-gutenberg-rest-posts-controller-test.php diff --git a/lib/compat/wordpress-6.8/class-gutenberg-rest-posts-controller-6-8.php b/lib/compat/wordpress-6.8/class-gutenberg-rest-posts-controller-6-8.php new file mode 100644 index 00000000000000..6271ff9a72ef44 --- /dev/null +++ b/lib/compat/wordpress-6.8/class-gutenberg-rest-posts-controller-6-8.php @@ -0,0 +1,164 @@ +namespace, + '/' . $this->rest_base . '/count', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_count' ), + 'permission_callback' => array( $this, 'get_count_permissions_check' ), + ), + 'schema' => array( $this, 'get_count_schema' ), + ) + ); + } + + /** + * Retrieves post counts for the post type. + * + * @since 6.8.0 + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_count() { + $counts = wp_count_posts( $this->post_type ); + $data = array(); + + if ( ! empty( $counts ) ) { + /* + * 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( $counts->$status ) ) { + $data[ $status ] = (int) $counts->$status; + } + } + } + return rest_ensure_response( $data ); + } + + /** + * Checks if a given request has access to read post counts. + * + * @since 6.8.0 + * + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_count_permissions_check() { + $post_type = get_post_type_object( $this->post_type ); + + 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.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Retrieves the post counts schema, conforming to JSON Schema. + * + * @since 6.8.0 + * + * @return array Item schema data. + */ + public function get_count_schema() { + return 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, + ); + } + + /** + * Add Block Editor default rendering mode setting to the response. + * + * @param WP_Post_Type $item Post type object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $item, $request ) { + $response = parent::prepare_item_for_response( $item, $request ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + + // Property will only exist if the post type supports the block editor. + if ( 'edit' === $context && property_exists( $item, 'default_rendering_mode' ) ) { + /** + * Filters the block editor rendering mode for a post type. + * + * @since 6.8.0 + * @param string $default_rendering_mode Default rendering mode for the post type. + * @param WP_Post_Type $post_type Post type name. + * @return string Default rendering mode for the post type. + */ + $rendering_mode = apply_filters( 'post_type_default_rendering_mode', $item->default_rendering_mode, $item ); + + /** + * Filters the block editor rendering mode for a specific post type. + * Applied after the generic `post_type_default_rendering_mode` filter. + * + * The dynamic portion of the hook name, `$item->name`, refers to the post type slug. + * + * @since 6.8.0 + * @param string $default_rendering_mode Default rendering mode for the post type. + * @param WP_Post_Type $post_type Post type object. + * @return string Default rendering mode for the post type. + */ + $rendering_mode = apply_filters( "post_type_{$item->name}_default_rendering_mode", $rendering_mode, $item ); + + // Validate the filtered rendering mode. + if ( ! in_array( $rendering_mode, gutenberg_post_type_rendering_modes(), true ) ) { + $rendering_mode = 'post-only'; + } + + $response->data['default_rendering_mode'] = $rendering_mode; + } + + return rest_ensure_response( $response ); + } +} diff --git a/lib/load.php b/lib/load.php index 8a71081e3bd7b8..5f3274a29715ec 100644 --- a/lib/load.php +++ b/lib/load.php @@ -45,7 +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-posts-controller.php'; + require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-posts-controller-6-8.php'; require __DIR__ . '/compat/wordpress-6.8/rest-api.php'; // Plugin specific code. diff --git a/phpunit/class-gutenberg-rest-posts-controller-test.php b/phpunit/class-gutenberg-rest-posts-controller-test.php new file mode 100644 index 00000000000000..da34fb9b053810 --- /dev/null +++ b/phpunit/class-gutenberg-rest-posts-controller-test.php @@ -0,0 +1,207 @@ +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(); + } + + public function tear_down() { + parent::tear_down(); + } + + /** + * @covers Gutenberg_Test_REST_Posts_Controller::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/posts/count', $routes ); + } + + /** + * @covers Gutenberg_Test_REST_Posts_Controller::get_count_schema + */ + public function test_get_count_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/count' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['patternProperties']; + + $this->assertCount( 1, $properties ); + $this->assertArrayHasKey( '^\w+$', $properties ); + } + + /** + * @covers Gutenberg_Test_REST_Posts_Controller::get_count + */ + public function test_get_count_response() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/count' ); + $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 ); + } + + /** + * @covers Gutenberg_Test_REST_Posts_Controller::get_count + */ + public function test_get_count() { + 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( + array( + 'post_status' => 'future', + 'post_date' => gmdate( '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' ) ); + $custom = self::factory()->post->create( array( 'post_status' => 'post_counts_status' ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/count' ); + $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.' ); + $this->assertSame( 1, $data['post_counts_status'], 'Custom 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 ); + wp_delete_post( $custom, true ); + unset( $GLOBALS['wp_post_statuses']['post_counts_status'] ); + } + + /** + * @covers Gutenberg_Test_REST_Posts_Controller::get_count + */ + public function test_get_count_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/posts/count' ); + $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'] ); + } + + /** + * @covers Gutenberg_Test_REST_Posts_Controller::get_count_permissions_check + */ + public function test_get_item_invalid_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/count' ); + $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(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Controller does not implement context_param(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item() { + // Controller does not implement get_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item_schema() { + // Controller does not implement get_item_schema(). + } +}