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 0000000000000..21875839c7a77 --- /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 new file mode 100644 index 0000000000000..c595c462fe435 --- /dev/null +++ b/lib/compat/wordpress-6.8/rest-api.php @@ -0,0 +1,22 @@ +register_routes(); +} + +add_action( 'rest_api_init', 'gutenberg_register_post_counts_routes' ); + diff --git a/lib/load.php b/lib/load.php index c26160eba2b67..6f024c1eaa01c 100644 --- a/lib/load.php +++ b/lib/load.php @@ -46,6 +46,10 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.7/class-gutenberg-rest-server.php'; require __DIR__ . '/compat/wordpress-6.7/rest-api.php'; + // WordPress 6.8 compat. + 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. require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php'; require_once __DIR__ . '/class-wp-rest-edit-site-export-controller-gutenberg.php'; 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 0000000000000..0fae05522e3ad --- /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(). + } +}