From 8b772572738a701bd355c3adfb8fadd035c83e32 Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 11 Oct 2024 09:39:36 -0600 Subject: [PATCH 01/19] Remove autosaves preload, reducing memory footprint --- src/wp-admin/edit-form-blocks.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-admin/edit-form-blocks.php b/src/wp-admin/edit-form-blocks.php index dea530e9fb9f2..1e360a49a2492 100644 --- a/src/wp-admin/edit-form-blocks.php +++ b/src/wp-admin/edit-form-blocks.php @@ -62,7 +62,6 @@ static function ( $classes ) { array( rest_get_route_for_post_type_items( 'page' ), 'OPTIONS' ), array( rest_get_route_for_post_type_items( 'wp_block' ), 'OPTIONS' ), array( rest_get_route_for_post_type_items( 'wp_template' ), 'OPTIONS' ), - sprintf( '%s/autosaves?context=edit', $rest_path ), '/wp/v2/settings', array( '/wp/v2/settings', 'OPTIONS' ), '/wp/v2/global-styles/themes/' . get_stylesheet(), From 1ab05aeac5ffccb5cfb2d490129a9bff25f3aa38 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 6 May 2025 15:39:52 -0600 Subject: [PATCH 02/19] Try: add per_page support for autosaves --- .../class-wp-rest-autosaves-controller.php | 24 +++++++- .../rest-api/rest-autosaves-controller.php | 56 +++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index 5545625df4609..78a70a5702994 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -304,10 +304,13 @@ public function get_items( $request ) { if ( is_wp_error( $parent ) ) { return $parent; } - - $response = array(); + $posts_per_page = 100; + $per_page = $request['per_page']; + if ( ! empty( $per_page ) ) { + $posts_per_page = $per_page; + } $parent_id = $parent->ID; - $revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false ) ); + $revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false, 'posts_per_page' => $posts_per_page ) ); foreach ( $revisions as $revision ) { if ( str_contains( $revision->post_name, "{$parent_id}-autosave" ) ) { @@ -317,6 +320,12 @@ public function get_items( $request ) { } return rest_ensure_response( $response ); + + // Add pagination headers. + $response->header( 'X-WP-Total', (int) $total_autosaves ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + return $response; } @@ -492,6 +501,15 @@ public function prepare_item_for_response( $item, $request ) { public function get_collection_params() { return array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + 'per_page' => array( + 'description' => __( 'Maximum number of autosaves to return.' ), + 'type' => 'integer', + 'default' => 100, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ), ); } } diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index c268b74518561..e611646c04b80 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -161,6 +161,7 @@ public function test_registered_query_params() { array( 'context', 'parent', + 'per_page', ), $keys ); @@ -179,6 +180,61 @@ public function test_get_items() { $this->check_get_autosave_response( $data[0], $this->post_autosave ); } + public function test_per_page_param() { + wp_set_current_user( self::$editor_id ); + + // Create additional autosaves for testing pagination + $autosave_ids = array( self::$autosave_post_id ); + for ( $i = 0; $i < 3; $i++ ) { + $autosave_ids[] = wp_create_post_autosave( + array( + 'post_content' => "Autosave content {$i}", + 'post_ID' => self::$post_id, + 'post_type' => 'post', + ) + ); + } + + // Test default per_page (should return all autosaves) + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( count( $autosave_ids ), $data ); + + // Test custom per_page parameter + $per_page = 2; + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'per_page', $per_page ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( $per_page, $data ); + + // Test per_page=1 + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'per_page', 1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( 1, $data ); + + // Test invalid per_page parameter (should use default) + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'per_page', -1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( count( $autosave_ids ), $data ); + + // Clean up additional autosaves + foreach ( $autosave_ids as $id ) { + if ( $id !== self::$autosave_post_id ) { + wp_delete_post_revision( $id ); + } + } + } + public function test_get_items_no_permission() { wp_set_current_user( 0 ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); From 2851b9cf7eedf7e6a7e90bfb18c66fd8dbbd7ac9 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 6 May 2025 17:04:13 -0600 Subject: [PATCH 03/19] add test for per_page parameter --- .../rest-api/rest-autosaves-controller.php | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 7815f8ced23c9..ab912234edbe3 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -920,4 +920,56 @@ public static function data_head_request_with_specified_fields_returns_success_r 'get_items request' => array( '/wp/v2/posts/%d' ), ); } + + public function test_per_page_param() { + wp_set_current_user( self::$editor_id ); + + // Create additional autosaves for testing pagination + $autosave_ids = array( self::$autosave_post_id ); + for ( $i = 0; $i < 3; $i++ ) { + $autosave_ids[] = wp_create_post_autosave( + array( + 'post_content' => "Autosave content {$i}", + 'post_ID' => self::$post_id, + 'post_type' => 'post', + ) + ); + } + + // Test default per_page (should return all autosaves) + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( count( $autosave_ids ), $data ); + + // Test custom per_page parameter + $per_page = 2; + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'per_page', $per_page ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( $per_page, $data ); + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_autosaves_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + + // Test per_page=1 + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'per_page', 1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( 1, $data ); + + // Test invalid per_page parameter (should use default) + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'per_page', -1 ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( count( $autosave_ids ), $data ); + } } From 8a7992c117d7d15fe037dfff205e4e84ab406ab3 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 8 May 2025 08:47:28 -0600 Subject: [PATCH 04/19] Add a test for the per_page parameter --- .../rest-api/rest-autosaves-controller.php | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index ab912234edbe3..b371bedadec54 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -24,6 +24,7 @@ class WP_Test_REST_Autosaves_Controller extends WP_Test_REST_Post_Type_Controlle protected static $child_draft_page_id; private $post_autosave; + protected static $extra_users; protected function set_post_data( $args = array() ) { $defaults = array( @@ -108,6 +109,15 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { 'post_author' => self::$editor_id, ) ); + self::$extra_users = array(); + for ( $i = 0; $i < 3; $i++ ) { + // Set a random user ID to ensure a new autosave is created. + self::$extra_users[] = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + } } public static function wpTearDownAfterClass() { @@ -117,6 +127,10 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$editor_id ); self::delete_user( self::$contributor_id ); + + foreach ( self::$extra_users as $user_id ) { + self::delete_user( $user_id ); + } } public function set_up() { @@ -921,12 +935,17 @@ public static function data_head_request_with_specified_fields_returns_success_r ); } + /** + * Test that the [per_page] parameter works as expected with the autosaves endpoint. + * + * @ticket 62057 + */ public function test_per_page_param() { - wp_set_current_user( self::$editor_id ); - + error_log( json_encode( self::$extra_users, JSON_PRETTY_PRINT ) ); // Create additional autosaves for testing pagination $autosave_ids = array( self::$autosave_post_id ); for ( $i = 0; $i < 3; $i++ ) { + wp_set_current_user( self::$extra_users[ $i ] ); $autosave_ids[] = wp_create_post_autosave( array( 'post_content' => "Autosave content {$i}", @@ -936,6 +955,8 @@ public function test_per_page_param() { ); } + error_log( json_encode( $autosave_ids, JSON_PRETTY_PRINT ) ); + // Test default per_page (should return all autosaves) $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); @@ -951,25 +972,5 @@ public function test_per_page_param() { $data = $response->get_data(); $this->assertSame( 200, $response->get_status() ); $this->assertCount( $per_page, $data ); - /** - * @ticket 56481 - */ - public function test_get_items_with_head_request_should_not_prepare_autosaves_data() { - $request = new WP_REST_Request( 'HEAD', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - - // Test per_page=1 - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - $request->set_param( 'per_page', 1 ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); - $this->assertCount( 1, $data ); - - // Test invalid per_page parameter (should use default) - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - $request->set_param( 'per_page', -1 ); - $data = $response->get_data(); - $this->assertSame( 200, $response->get_status() ); - $this->assertCount( count( $autosave_ids ), $data ); } } From 5064081df6c4c799f1af42dff63ea8977abc76c3 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 8 May 2025 08:48:37 -0600 Subject: [PATCH 05/19] Add tests for autosaves per_page parameter --- .../tests/rest-api/rest-autosaves-controller.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index b371bedadec54..896c8ec48e952 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -941,8 +941,6 @@ public static function data_head_request_with_specified_fields_returns_success_r * @ticket 62057 */ public function test_per_page_param() { - error_log( json_encode( self::$extra_users, JSON_PRETTY_PRINT ) ); - // Create additional autosaves for testing pagination $autosave_ids = array( self::$autosave_post_id ); for ( $i = 0; $i < 3; $i++ ) { wp_set_current_user( self::$extra_users[ $i ] ); @@ -955,8 +953,6 @@ public function test_per_page_param() { ); } - error_log( json_encode( $autosave_ids, JSON_PRETTY_PRINT ) ); - // Test default per_page (should return all autosaves) $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); @@ -972,5 +968,14 @@ public function test_per_page_param() { $data = $response->get_data(); $this->assertSame( 200, $response->get_status() ); $this->assertCount( $per_page, $data ); + + // Test getting one record + $per_page = 1; + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'per_page', $per_page ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( $per_page, $data ); } } From 64b9a5407d59300a75bba42a788bf97d316fc546 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 20 May 2025 07:56:32 -0600 Subject: [PATCH 06/19] phpcbf --- .../endpoints/class-wp-rest-autosaves-controller.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index 3d12ea97d03ee..a8b5f68d42872 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -311,14 +311,20 @@ public function get_items( $request ) { } $posts_per_page = 100; - $per_page = $request['per_page']; + $per_page = $request['per_page']; if ( ! empty( $per_page ) ) { $posts_per_page = $per_page; } $response = array(); $parent_id = $parent->ID; - $revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false, 'posts_per_page' => $posts_per_page ) ); + $revisions = wp_get_post_revisions( + $parent_id, + array( + 'check_enabled' => false, + 'posts_per_page' => $posts_per_page, + ) + ); foreach ( $revisions as $revision ) { if ( str_contains( $revision->post_name, "{$parent_id}-autosave" ) ) { @@ -513,7 +519,7 @@ public function prepare_item_for_response( $item, $request ) { */ public function get_collection_params() { return array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 'per_page' => array( 'description' => __( 'Maximum number of autosaves to return.' ), 'type' => 'integer', From beb385968b7ae0591cc86e9206576caa47c70cfb Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 20 May 2025 07:58:31 -0600 Subject: [PATCH 07/19] autosaves route test: add per_page to expected parameters --- tests/phpunit/tests/rest-api/rest-autosaves-controller.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 896c8ec48e952..1520ea49ab6c3 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -175,6 +175,7 @@ public function test_registered_query_params() { array( 'context', 'parent', + 'per_page', ), $keys ); From a1c0cbe2b6e15349f60b1d7a71e2c25461a1842c Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 20 May 2025 08:03:09 -0600 Subject: [PATCH 08/19] preload a single autosave --- src/wp-admin/edit-form-blocks.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-admin/edit-form-blocks.php b/src/wp-admin/edit-form-blocks.php index b596b5df54cdb..2f608995827bc 100644 --- a/src/wp-admin/edit-form-blocks.php +++ b/src/wp-admin/edit-form-blocks.php @@ -68,6 +68,7 @@ static function ( $classes ) { array( rest_get_route_for_post_type_items( 'page' ), 'OPTIONS' ), array( rest_get_route_for_post_type_items( 'wp_block' ), 'OPTIONS' ), array( rest_get_route_for_post_type_items( 'wp_template' ), 'OPTIONS' ), + sprintf( '%s/autosaves?context=edit&per_page=1', $rest_path ), '/wp/v2/settings', array( '/wp/v2/settings', 'OPTIONS' ), '/wp/v2/global-styles/themes/' . $active_theme . '?context=view', From 267d51d4e648c53047dcb93955da2053d25e4094 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 20 May 2025 08:07:43 -0600 Subject: [PATCH 09/19] phpcbf --- .../tests/rest-api/rest-autosaves-controller.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 1520ea49ab6c3..4f6f744eb40cf 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -109,7 +109,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { 'post_author' => self::$editor_id, ) ); - self::$extra_users = array(); + self::$extra_users = array(); for ( $i = 0; $i < 3; $i++ ) { // Set a random user ID to ensure a new autosave is created. self::$extra_users[] = $factory->user->create( @@ -955,27 +955,27 @@ public function test_per_page_param() { } // Test default per_page (should return all autosaves) - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertSame( 200, $response->get_status() ); $this->assertCount( count( $autosave_ids ), $data ); // Test custom per_page parameter $per_page = 2; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); $request->set_param( 'per_page', $per_page ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertSame( 200, $response->get_status() ); $this->assertCount( $per_page, $data ); // Test getting one record $per_page = 1; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); $request->set_param( 'per_page', $per_page ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertSame( 200, $response->get_status() ); $this->assertCount( $per_page, $data ); } From e06ecb1b66c978313256f62090d66158490b05d0 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 26 Aug 2025 16:55:09 -0600 Subject: [PATCH 10/19] remove autosave preload entirely --- src/wp-admin/edit-form-blocks.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-admin/edit-form-blocks.php b/src/wp-admin/edit-form-blocks.php index 2f608995827bc..b596b5df54cdb 100644 --- a/src/wp-admin/edit-form-blocks.php +++ b/src/wp-admin/edit-form-blocks.php @@ -68,7 +68,6 @@ static function ( $classes ) { array( rest_get_route_for_post_type_items( 'page' ), 'OPTIONS' ), array( rest_get_route_for_post_type_items( 'wp_block' ), 'OPTIONS' ), array( rest_get_route_for_post_type_items( 'wp_template' ), 'OPTIONS' ), - sprintf( '%s/autosaves?context=edit&per_page=1', $rest_path ), '/wp/v2/settings', array( '/wp/v2/settings', 'OPTIONS' ), '/wp/v2/global-styles/themes/' . $active_theme . '?context=view', From 120edbc49d9d05e27d45889aa51164e442a13564 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 13 Aug 2026 17:39:16 -0700 Subject: [PATCH 11/19] REST API: Allow the autosaves collection to be scoped by author. WordPress stores at most one autosave per author, and the block editor only ever reads the current user's, so returning every autosave for the post makes the editor download a collection to discard all but one row of it. Register an `author` parameter on the autosaves collection, shaped like the one on the posts controller, and answer it with `wp_get_post_autosave()` for each requested id. This replaces the `per_page` parameter tried earlier, which narrowed the revision query before autosaves were filtered out of it and so could return nothing at all on a post with revision history. The block editor preload is scoped to match the path the editor requests, rather than removed: the preload cache is keyed on the path, so an entry the client never asks for is served to nobody. See #62057. --- src/wp-admin/edit-form-blocks.php | 12 ++ .../class-wp-rest-autosaves-controller.php | 58 ++++---- .../rest-api/rest-autosaves-controller.php | 130 +++++++++++++++--- 3 files changed, 152 insertions(+), 48 deletions(-) diff --git a/src/wp-admin/edit-form-blocks.php b/src/wp-admin/edit-form-blocks.php index 49676fd197fcf..9d1c24eb65eb4 100644 --- a/src/wp-admin/edit-form-blocks.php +++ b/src/wp-admin/edit-form-blocks.php @@ -63,6 +63,18 @@ static function ( $classes ) { '/wp/v2/taxonomies?context=view', add_query_arg( 'context', 'edit', $rest_path ), sprintf( '/wp/v2/types/%s?context=edit', $post_type ), + /* + * The editor reads the current user's autosave rather than the whole + * collection, so this is scoped to match the path it requests. A preload + * entry the client never asks for is served to nobody. + */ + add_query_arg( + array( + 'context' => 'edit', + 'author' => get_current_user_id(), + ), + $rest_path . '/autosaves' + ), '/wp/v2/users/me', array( rest_get_route_for_post_type_items( 'attachment' ), 'OPTIONS' ), array( rest_get_route_for_post_type_items( 'page' ), 'OPTIONS' ), diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index 563d387bb48e1..b461444cb4d26 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -307,6 +307,7 @@ public function get_item( $request ) { * Contains the user's autosave, for empty if it doesn't exist. * * @since 5.0.0 + * @since 7.2.0 Added support for the `author` parameter. * * @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. @@ -322,21 +323,29 @@ public function get_items( $request ) { return new WP_REST_Response( array() ); } - $posts_per_page = 100; - $per_page = $request['per_page']; - if ( ! empty( $per_page ) ) { - $posts_per_page = $per_page; - } - $response = array(); $parent_id = $parent->ID; - $revisions = wp_get_post_revisions( - $parent_id, - array( - 'check_enabled' => false, - 'posts_per_page' => $posts_per_page, - ) - ); + + if ( ! empty( $request['author'] ) ) { + /* + * WordPress stores at most one autosave per author, so scoping by + * author asks for a well defined set of records. Query for those + * directly rather than reading every revision of the post and + * discarding all but the autosaves. + */ + foreach ( $request['author'] as $author_id ) { + $autosave = wp_get_post_autosave( $parent_id, $author_id ); + + if ( $autosave ) { + $data = $this->prepare_item_for_response( $autosave, $request ); + $response[] = $this->prepare_response_for_collection( $data ); + } + } + + return rest_ensure_response( $response ); + } + + $revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false ) ); foreach ( $revisions as $revision ) { if ( str_contains( $revision->post_name, "{$parent_id}-autosave" ) ) { @@ -346,12 +355,6 @@ public function get_items( $request ) { } return rest_ensure_response( $response ); - - // Add pagination headers. - $response->header( 'X-WP-Total', (int) $total_autosaves ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); - - return $response; } @@ -531,15 +534,14 @@ public function prepare_item_for_response( $item, $request ) { */ public function get_collection_params() { return array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - 'per_page' => array( - 'description' => __( 'Maximum number of autosaves to return.' ), - 'type' => 'integer', - 'default' => 100, - 'minimum' => 1, - 'maximum' => 100, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + 'author' => array( + 'description' => __( 'Limit result set to autosaves assigned to specific authors.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), ), ); } diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 19df7534980bd..83cc3e019e529 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -173,9 +173,9 @@ public function test_registered_query_params() { sort( $keys ); $this->assertSame( array( + 'author', 'context', 'parent', - 'per_page', ), $keys ); @@ -967,15 +967,16 @@ public static function data_head_request_with_specified_fields_returns_success_r } /** - * Test that the [per_page] parameter works as expected with the autosaves endpoint. + * Creates an autosave on the shared post for each of the extra users. * - * @ticket 62057 + * @return int[] Author id keyed to the autosave id created for them. */ - public function test_per_page_param() { - $autosave_ids = array( self::$autosave_post_id ); - for ( $i = 0; $i < 3; $i++ ) { - wp_set_current_user( self::$extra_users[ $i ] ); - $autosave_ids[] = wp_create_post_autosave( + private function create_autosaves_for_extra_users() { + $autosave_ids = array(); + + foreach ( self::$extra_users as $i => $user_id ) { + wp_set_current_user( $user_id ); + $autosave_ids[ $user_id ] = wp_create_post_autosave( array( 'post_content' => "Autosave content {$i}", 'post_ID' => self::$post_id, @@ -984,29 +985,118 @@ public function test_per_page_param() { ); } - // Test default per_page (should return all autosaves) + return $autosave_ids; + } + + /** + * Tests that the collection is unscoped without an author. + * + * @ticket 62057 + */ + public function test_get_items_returns_every_author_by_default() { + $autosave_ids = $this->create_autosaves_for_extra_users(); + + wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( count( $autosave_ids ) + 1, $response->get_data(), 'The extra users\' autosaves and the one created in set_up should all be returned.' ); + } + + /** + * Tests that an author scoped request returns only that author's autosave. + * + * WordPress stores one autosave per author, so this is the single record + * a client reading one user's autosave is after. + * + * @ticket 62057 + */ + public function test_get_items_scoped_to_one_author() { + $autosave_ids = $this->create_autosaves_for_extra_users(); + $author_id = self::$extra_users[1]; + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'author', $author_id ); + $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); - $this->assertCount( count( $autosave_ids ), $data ); + $this->assertCount( 1, $data ); + $this->assertSame( $autosave_ids[ $author_id ], $data[0]['id'] ); + $this->assertSame( $author_id, $data[0]['author'] ); + } - // Test custom per_page parameter - $per_page = 2; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - $request->set_param( 'per_page', $per_page ); + /** + * Tests that the author parameter accepts more than one id. + * + * @ticket 62057 + */ + public function test_get_items_scoped_to_several_authors() { + $autosave_ids = $this->create_autosaves_for_extra_users(); + $author_ids = array( self::$extra_users[0], self::$extra_users[2] ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'author', $author_ids ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); - $this->assertCount( $per_page, $data ); + $this->assertCount( 2, $data ); + $this->assertSameSets( + array( $autosave_ids[ $author_ids[0] ], $autosave_ids[ $author_ids[1] ] ), + wp_list_pluck( $data, 'id' ) + ); + } - // Test getting one record - $per_page = 1; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - $request->set_param( 'per_page', $per_page ); + /** + * Tests that an author without an autosave gets an empty collection rather + * than somebody else's record. + * + * @ticket 62057 + */ + public function test_get_items_for_an_author_with_no_autosave() { + $this->create_autosaves_for_extra_users(); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'author', self::$contributor_id ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( array(), $response->get_data() ); + } + + /** + * Tests that revisions newer than the autosave do not displace it. + * + * Narrowing the underlying revision query by count rather than by author + * returns whatever is most recent, which on a post with revision history + * is usually not an autosave at all. + * + * @ticket 62057 + */ + public function test_get_items_scoped_to_one_author_past_newer_revisions() { + $autosave_ids = $this->create_autosaves_for_extra_users(); + $author_id = self::$extra_users[0]; + + wp_set_current_user( self::$editor_id ); + wp_update_post( + array( + 'ID' => self::$post_id, + 'post_content' => 'A revision made after every autosave.', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'author', $author_id ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( 200, $response->get_status() ); - $this->assertCount( $per_page, $data ); + $this->assertCount( 1, $data ); + $this->assertSame( $autosave_ids[ $author_id ], $data[0]['id'] ); } } From 6108638377e06715b472131fb355c4c56643bcf5 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 24 Sep 2026 22:01:01 -0700 Subject: [PATCH 12/19] Tests: Regenerate the REST API client fixtures for the autosaves author param. The new author collection param on the autosaves endpoints changes the REST schema, so the committed QUnit fixture was stale and every single-site PHPUnit job failed the version-controlled files check. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012XGNSFgvkHJVapMFkJo3VU --- tests/qunit/fixtures/wp-api-generated.js | 63 ++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 5d209f1c79820..7ab401e1ecb48 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -1378,6 +1378,15 @@ mockedApiResponse.Schema = { ], "default": "view", "required": false + }, + "author": { + "description": "Limit result set to autosaves assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false } } }, @@ -2528,6 +2537,15 @@ mockedApiResponse.Schema = { ], "default": "view", "required": false + }, + "author": { + "description": "Limit result set to autosaves assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false } } }, @@ -4438,6 +4456,15 @@ mockedApiResponse.Schema = { ], "default": "view", "required": false + }, + "author": { + "description": "Limit result set to autosaves assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false } } }, @@ -5442,6 +5469,15 @@ mockedApiResponse.Schema = { ], "default": "view", "required": false + }, + "author": { + "description": "Limit result set to autosaves assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false } } }, @@ -5821,6 +5857,15 @@ mockedApiResponse.Schema = { ], "default": "view", "required": false + }, + "author": { + "description": "Limit result set to autosaves assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false } } }, @@ -6505,6 +6550,15 @@ mockedApiResponse.Schema = { ], "default": "view", "required": false + }, + "author": { + "description": "Limit result set to autosaves assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false } } }, @@ -7922,6 +7976,15 @@ mockedApiResponse.Schema = { ], "default": "view", "required": false + }, + "author": { + "description": "Limit result set to autosaves assigned to specific authors.", + "type": "array", + "items": { + "type": "integer" + }, + "default": [], + "required": false } } }, From d163ca59c7139e87e564ed1f1d1956171c82f86f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 17 Oct 2026 12:10:00 -0700 Subject: [PATCH 13/19] Editor: Restore the unscoped autosaves preload path. The block editor still requests /autosaves?context=edit without an author, so an author scoped preload is never consumed: it adds a request on every load and triggers an unconsumed preload warning. Keep trunk's preload until the client change lands in core. See #62057. --- src/wp-admin/edit-form-blocks.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/wp-admin/edit-form-blocks.php b/src/wp-admin/edit-form-blocks.php index 7e43830f15331..693919d3d5bf1 100644 --- a/src/wp-admin/edit-form-blocks.php +++ b/src/wp-admin/edit-form-blocks.php @@ -63,23 +63,12 @@ static function ( $classes ) { '/wp/v2/taxonomies?context=view', add_query_arg( 'context', 'edit', $rest_path ), sprintf( '/wp/v2/types/%s?context=edit', $post_type ), - /* - * The editor reads the current user's autosave rather than the whole - * collection, so this is scoped to match the path it requests. A preload - * entry the client never asks for is served to nobody. - */ - add_query_arg( - array( - 'context' => 'edit', - 'author' => get_current_user_id(), - ), - $rest_path . '/autosaves' - ), '/wp/v2/users/me', array( rest_get_route_for_post_type_items( 'attachment' ), 'OPTIONS' ), array( rest_get_route_for_post_type_items( 'page' ), 'OPTIONS' ), array( rest_get_route_for_post_type_items( 'wp_block' ), 'OPTIONS' ), array( rest_get_route_for_post_type_items( 'wp_template' ), 'OPTIONS' ), + sprintf( '%s/autosaves?context=edit', $rest_path ), '/wp/v2/settings', array( '/wp/v2/settings', 'OPTIONS' ), '/wp/v2/global-styles/themes/' . $active_theme . '?context=view', From fbb7534bcc3454fc613b5f621e9a22c8d9580bcd Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 17 Oct 2026 12:10:23 -0700 Subject: [PATCH 14/19] REST API: Fetch author scoped autosaves in a single query. Running wp_get_post_autosave() once per requested author made the query count grow with the size of the author list and returned duplicates when an ID was repeated. A single author__in query sorted by date and ID, newest first, matches the ordering of the unscoped path, and the result is reduced to one autosave per author. See #62057. --- .../class-wp-rest-autosaves-controller.php | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index b461444cb4d26..75b7fe96c909e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -327,19 +327,34 @@ public function get_items( $request ) { $parent_id = $parent->ID; if ( ! empty( $request['author'] ) ) { - /* - * WordPress stores at most one autosave per author, so scoping by - * author asks for a well defined set of records. Query for those - * directly rather than reading every revision of the post and - * discarding all but the autosaves. - */ - foreach ( $request['author'] as $author_id ) { - $autosave = wp_get_post_autosave( $parent_id, $author_id ); + $autosaves = get_posts( + array( + 'post_type' => 'revision', + 'post_status' => 'inherit', + 'post_parent' => $parent_id, + 'name' => $parent_id . '-autosave-v1', + 'author__in' => wp_parse_id_list( $request['author'] ), + 'orderby' => array( + 'date' => 'DESC', + 'ID' => 'DESC', + ), + 'posts_per_page' => -1, + 'suppress_filters' => false, + ) + ); - if ( $autosave ) { - $data = $this->prepare_item_for_response( $autosave, $request ); - $response[] = $this->prepare_response_for_collection( $data ); + // Each author has at most one autosave; keep only the newest if not. + $seen_authors = array(); + + foreach ( $autosaves as $autosave ) { + if ( isset( $seen_authors[ $autosave->post_author ] ) ) { + continue; } + + $seen_authors[ $autosave->post_author ] = true; + + $data = $this->prepare_item_for_response( $autosave, $request ); + $response[] = $this->prepare_response_for_collection( $data ); } return rest_ensure_response( $response ); From d6ba5828d45b6fbd371f4a9daf4924ac3b391674 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 17 Oct 2026 12:10:23 -0700 Subject: [PATCH 15/19] REST API: Require positive author IDs for autosaves. WP_Query ignores an author of 0 and treats negative IDs as exclusions, so author=0 or author=-1 would return other users' autosaves instead of narrowing the result. See #62057. --- .../rest-api/endpoints/class-wp-rest-autosaves-controller.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index 75b7fe96c909e..9640bbed36057 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -554,7 +554,8 @@ public function get_collection_params() { 'description' => __( 'Limit result set to autosaves assigned to specific authors.' ), 'type' => 'array', 'items' => array( - 'type' => 'integer', + 'type' => 'integer', + 'minimum' => 1, ), 'default' => array(), ), From dc7a9227b1f65952b0833ae26c42384198386b54 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 17 Oct 2026 12:13:48 -0700 Subject: [PATCH 16/19] Tests: Cover invalid, repeated and template author autosave queries. Adds coverage for author IDs below 1, repeated IDs, ordering parity with the unscoped collection and the inherited parameter on the template autosaves route, and corrects a misleading comment on the extra test users. See #62057. --- .../rest-api/rest-autosaves-controller.php | 79 ++++++++++++++++++- .../wpRestTemplateAutosavesController.php | 33 ++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 3a9be63cd4442..c9ad655d4b301 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -111,7 +111,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ); self::$extra_users = array(); for ( $i = 0; $i < 3; $i++ ) { - // Set a random user ID to ensure a new autosave is created. + // Additional editors, each of whom gets their own autosave on the shared post. self::$extra_users[] = $factory->user->create( array( 'role' => 'editor', @@ -1111,4 +1111,81 @@ public function test_get_items_scoped_to_one_author_past_newer_revisions() { $this->assertCount( 1, $data ); $this->assertSame( $autosave_ids[ $author_id ], $data[0]['id'] ); } + + /** + * Tests that author IDs below 1 are rejected rather than widening the query. + * + * @ticket 62057 + * + * @dataProvider data_get_items_rejects_invalid_author + * + * @param mixed $author Value passed as the author parameter. + */ + public function test_get_items_rejects_invalid_author( $author ) { + $this->create_autosaves_for_extra_users(); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'author', $author ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Data provider for test_get_items_rejects_invalid_author(). + * + * @return array[] + */ + public function data_get_items_rejects_invalid_author() { + return array( + 'zero' => array( 0 ), + 'negative' => array( -1 ), + 'zero in a list' => array( array( 1, 0 ) ), + 'negative in a list' => array( array( 1, -1 ) ), + ); + } + + /** + * Tests that a repeated author ID returns their autosave once. + * + * @ticket 62057 + */ + public function test_get_items_with_repeated_author() { + $autosave_ids = $this->create_autosaves_for_extra_users(); + $author_id = self::$extra_users[1]; + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'author', array( $author_id, $author_id ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( 1, $data ); + $this->assertSame( $autosave_ids[ $author_id ], $data[0]['id'] ); + } + + /** + * Tests that author scoped results share the unscoped ordering, newest first. + * + * @ticket 62057 + */ + public function test_get_items_scoped_to_authors_matches_unscoped_order() { + $autosave_ids = $this->create_autosaves_for_extra_users(); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $expected = array_values( + array_intersect( wp_list_pluck( $response->get_data(), 'id' ), $autosave_ids ) + ); + + $request->set_param( 'author', self::$extra_users ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( $expected, wp_list_pluck( $response->get_data(), 'id' ) ); + $this->assertSame( array_reverse( array_values( $autosave_ids ) ), $expected, 'Autosaves should be ordered newest first.' ); + } } diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php index 0738973fd4813..88cd7f20be516 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php @@ -888,4 +888,37 @@ public function test_delete_item() { $this->assertErrorResponse( 'rest_template_not_found', $response, 404 ); } + + /** + * Tests that the inherited author parameter scopes template autosaves. + * + * @ticket 62057 + * + * @covers WP_REST_Autosaves_Controller::get_items + */ + public function test_get_items_scoped_to_author() { + $other_admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + $autosave_ids = array(); + + foreach ( array( self::$admin_id, $other_admin_id ) as $user_id ) { + wp_set_current_user( $user_id ); + $autosave_ids[ $user_id ] = wp_create_post_autosave( + array( + 'post_content' => "Autosave content for {$user_id}.", + 'post_ID' => self::$template_post->ID, + 'post_type' => self::PARENT_POST_TYPE, + ) + ); + } + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/' . self::TEST_THEME . '//' . self::TEMPLATE_NAME . '/autosaves' ); + $request->set_param( 'author', $other_admin_id ); + $response = rest_get_server()->dispatch( $request ); + $autosaves = $response->get_data(); + + $this->assertSame( WP_Http::OK, $response->get_status(), 'Response is expected to have a status code of 200.' ); + $this->assertCount( 1, $autosaves, 'Only the requested author\'s autosave should be returned.' ); + $this->assertSame( $autosave_ids[ $other_admin_id ], $autosaves[0]['wp_id'], 'The autosave should belong to the requested author.' ); + } } From 68c48b0f54330113a23840d235483a46f5e05082 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 17 Oct 2026 12:15:21 -0700 Subject: [PATCH 17/19] Tests: Update the REST API client fixture for the author minimum. See #62057. --- tests/qunit/fixtures/wp-api-generated.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index ec0a1755b713b..d9d53f94c9f90 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -1383,7 +1383,8 @@ mockedApiResponse.Schema = { "description": "Limit result set to autosaves assigned to specific authors.", "type": "array", "items": { - "type": "integer" + "type": "integer", + "minimum": 1 }, "default": [], "required": false @@ -2542,7 +2543,8 @@ mockedApiResponse.Schema = { "description": "Limit result set to autosaves assigned to specific authors.", "type": "array", "items": { - "type": "integer" + "type": "integer", + "minimum": 1 }, "default": [], "required": false @@ -4461,7 +4463,8 @@ mockedApiResponse.Schema = { "description": "Limit result set to autosaves assigned to specific authors.", "type": "array", "items": { - "type": "integer" + "type": "integer", + "minimum": 1 }, "default": [], "required": false @@ -5474,7 +5477,8 @@ mockedApiResponse.Schema = { "description": "Limit result set to autosaves assigned to specific authors.", "type": "array", "items": { - "type": "integer" + "type": "integer", + "minimum": 1 }, "default": [], "required": false @@ -5862,7 +5866,8 @@ mockedApiResponse.Schema = { "description": "Limit result set to autosaves assigned to specific authors.", "type": "array", "items": { - "type": "integer" + "type": "integer", + "minimum": 1 }, "default": [], "required": false @@ -6555,7 +6560,8 @@ mockedApiResponse.Schema = { "description": "Limit result set to autosaves assigned to specific authors.", "type": "array", "items": { - "type": "integer" + "type": "integer", + "minimum": 1 }, "default": [], "required": false @@ -7981,7 +7987,8 @@ mockedApiResponse.Schema = { "description": "Limit result set to autosaves assigned to specific authors.", "type": "array", "items": { - "type": "integer" + "type": "integer", + "minimum": 1 }, "default": [], "required": false From 5f31ccafd5152e346fa40d6c9b26fb2f441f1a0f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 17 Oct 2026 12:26:11 -0700 Subject: [PATCH 18/19] Tests: Give author scoped autosave tests explicit dates. The ordering and newer-revision tests relied on records created in sequence, so they mostly exercised the ID tiebreaker. Set dates that disagree with ID order, and cover an author with two stored autosaves so the newest-record deduplication runs against real duplicates. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012aBHgCnrQQ5WWMiPZjtZK1 --- .../rest-api/rest-autosaves-controller.php | 99 ++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index c9ad655d4b301..0c4ddc8b30a0a 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -1000,6 +1000,27 @@ private function create_autosaves_for_extra_users() { return $autosave_ids; } + /** + * Sets a post's local and GMT dates directly, so tests can order records + * independently of their IDs. + * + * @param int $post_id Post ID. + * @param string $date Date in MySQL format. + */ + private function set_post_date( $post_id, $date ) { + global $wpdb; + + $wpdb->update( + $wpdb->posts, + array( + 'post_date' => $date, + 'post_date_gmt' => $date, + ), + array( 'ID' => $post_id ) + ); + clean_post_cache( $post_id ); + } + /** * Tests that the collection is unscoped without an author. * @@ -1102,6 +1123,9 @@ public function test_get_items_scoped_to_one_author_past_newer_revisions() { ) ); + $revisions = wp_get_post_revisions( self::$post_id, array( 'check_enabled' => false ) ); + $this->set_post_date( reset( $revisions )->ID, '2030-01-01 00:00:00' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); $request->set_param( 'author', $author_id ); $response = rest_get_server()->dispatch( $request ); @@ -1172,7 +1196,12 @@ public function test_get_items_with_repeated_author() { * @ticket 62057 */ public function test_get_items_scoped_to_authors_matches_unscoped_order() { - $autosave_ids = $this->create_autosaves_for_extra_users(); + $autosave_ids = array_values( $this->create_autosaves_for_extra_users() ); + + // The oldest ID gets the newest date, and the other two tie on date. + $this->set_post_date( $autosave_ids[0], '2030-01-02 00:00:00' ); + $this->set_post_date( $autosave_ids[1], '2030-01-01 00:00:00' ); + $this->set_post_date( $autosave_ids[2], '2030-01-01 00:00:00' ); wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); @@ -1186,6 +1215,72 @@ public function test_get_items_scoped_to_authors_matches_unscoped_order() { $this->assertSame( 200, $response->get_status() ); $this->assertSame( $expected, wp_list_pluck( $response->get_data(), 'id' ) ); - $this->assertSame( array_reverse( array_values( $autosave_ids ) ), $expected, 'Autosaves should be ordered newest first.' ); + $this->assertSame( + array( $autosave_ids[0], $autosave_ids[2], $autosave_ids[1] ), + $expected, + 'Autosaves should be ordered by date, newest first, then by ID.' + ); + } + + /** + * Tests that only the newest of several stored autosaves for one author is returned. + * + * @ticket 62057 + * + * @dataProvider data_get_items_with_several_autosaves_for_one_author + * + * @param string $first_date Date of the autosave created first. + * @param string $second_date Date of the autosave created second. + * @param int $expected Index of the autosave expected to be returned. + */ + public function test_get_items_with_several_autosaves_for_one_author( $first_date, $second_date, $expected ) { + $author_id = self::$extra_users[0]; + + wp_set_current_user( $author_id ); + $autosave_ids = array( + wp_create_post_autosave( + array( + 'post_content' => 'First autosave.', + 'post_ID' => self::$post_id, + 'post_type' => 'post', + ) + ), + // wp_create_post_autosave() updates the existing record, so insert the duplicate directly. + self::factory()->post->create( + array( + 'post_type' => 'revision', + 'post_status' => 'inherit', + 'post_parent' => self::$post_id, + 'post_name' => self::$post_id . '-autosave-v1', + 'post_author' => $author_id, + 'post_content' => 'Second autosave.', + ) + ), + ); + + $this->set_post_date( $autosave_ids[0], $first_date ); + $this->set_post_date( $autosave_ids[1], $second_date ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->set_param( 'author', $author_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertCount( 1, $data ); + $this->assertSame( $autosave_ids[ $expected ], $data[0]['id'] ); + } + + /** + * Data provider for test_get_items_with_several_autosaves_for_one_author(). + * + * @return array[] + */ + public function data_get_items_with_several_autosaves_for_one_author() { + return array( + 'older ID has the newer date' => array( '2030-01-02 00:00:00', '2030-01-01 00:00:00', 0 ), + 'same date, newer ID wins' => array( '2030-01-01 00:00:00', '2030-01-01 00:00:00', 1 ), + ); } } From 84953d3803fa0b73f83f0517ebb35251f95a66e8 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 17 Oct 2026 16:21:28 -0700 Subject: [PATCH 19/19] REST API: Simplify author scoped autosave lookup. The schema already sanitizes `author` to a list of positive integers, so running it through wp_parse_id_list() again was redundant. The seen-author check now uses a plain array, since an author scoped query only ever holds a handful of autosaves and a hash lookup buys nothing at that size. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EzTaxRNEc1fpMMaLJWcNyd --- .../endpoints/class-wp-rest-autosaves-controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php index 9640bbed36057..81d072f03e5d1 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php @@ -333,7 +333,7 @@ public function get_items( $request ) { 'post_status' => 'inherit', 'post_parent' => $parent_id, 'name' => $parent_id . '-autosave-v1', - 'author__in' => wp_parse_id_list( $request['author'] ), + 'author__in' => $request['author'], 'orderby' => array( 'date' => 'DESC', 'ID' => 'DESC', @@ -347,11 +347,11 @@ public function get_items( $request ) { $seen_authors = array(); foreach ( $autosaves as $autosave ) { - if ( isset( $seen_authors[ $autosave->post_author ] ) ) { + if ( in_array( $autosave->post_author, $seen_authors, true ) ) { continue; } - $seen_authors[ $autosave->post_author ] = true; + $seen_authors[] = $autosave->post_author; $data = $this->prepare_item_for_response( $autosave, $request ); $response[] = $this->prepare_response_for_collection( $data );