Skip to content

Commit

Permalink
Alternative to #66294
Browse files Browse the repository at this point in the history
Extend post type controller to return total post count
  • Loading branch information
ramonjd committed Dec 9, 2024
1 parent cc8e558 commit 9505fde
Show file tree
Hide file tree
Showing 3 changed files with 372 additions and 1 deletion.
164 changes: 164 additions & 0 deletions lib/compat/wordpress-6.8/class-gutenberg-rest-posts-controller-6-8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php
/**
* REST API: Gutenberg_REST_Posts_Controller_6_8 class
*
* @package gutenberg
*/

/**
* Gutenberg_REST_Posts_Controller_6_8 class
*
* Adds a /counts route to return total posts count.
*/
class Gutenberg_REST_Posts_Controller_6_8 extends Gutenberg_REST_Posts_Controller_6_7 {
/**
* Registers the routes for attachments.
*
* @see register_rest_route()
*/
public function register_routes() {
parent::register_routes();

register_rest_route(
$this->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 );
}
}
2 changes: 1 addition & 1 deletion lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
207 changes: 207 additions & 0 deletions phpunit/class-gutenberg-rest-posts-controller-test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php
/**
* Unit tests for Gutenberg_Test_REST_Posts_Controller.
*
* @package gutenberg
* @subpackage REST API
*/

/**
* @group restapi
*/
class Gutenberg_Test_REST_Posts_Controller extends WP_Test_REST_Controller_Testcase {
/**
* @var int
*/
protected static $admin_id;

public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$admin_id = $factory->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().
}
}

0 comments on commit 9505fde

Please sign in to comment.