diff --git a/src/wp-includes/revision.php b/src/wp-includes/revision.php index 9d3ca6f424..5433d0a5ce 100644 --- a/src/wp-includes/revision.php +++ b/src/wp-includes/revision.php @@ -200,6 +200,35 @@ function wp_save_post_revision( $post_id ) { $revisions = wp_get_post_revisions( $post_id, array( 'order' => 'ASC' ) ); + /** + * Filters the revisions to be considered for deletion. + * + * @since 6.2.0 + * + * @param WP_Post[]|int[] $revisions Array of revision objects or IDs, + * or an empty array if none. + * @param int $post_id The ID of the post to save as a revision. + */ + $filtered_revisions = apply_filters( + 'wp_save_post_revision_revisions_before_deletion', + $revisions, + $post_id + ); + + if ( is_array( $filtered_revisions ) ) { + $revisions = $filtered_revisions; + } else { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: The filter name. */ + __( 'The "%s" filter should return an array.' ), + 'wp_save_post_revision_revisions_before_deletion' + ), + '6.2.0' + ); + } + $delete = count( $revisions ) - $revisions_to_keep; if ( $delete < 1 ) { diff --git a/tests/phpunit/tests/post/revisions.php b/tests/phpunit/tests/post/revisions.php index c8effbccc2..62e3bbef04 100644 --- a/tests/phpunit/tests/post/revisions.php +++ b/tests/phpunit/tests/post/revisions.php @@ -866,4 +866,115 @@ class Tests_Post_Revisions extends WP_UnitTestCase { add_post_type_support( 'post', 'revisions' ); } + + /** + * Tests that wp_save_post_revision() respects the 'wp_save_post_revision_revisions_before_deletion' filter + * when deleting revisions. + * + * This test should protect the original revision, send the rest to be checked against wp_revisions_to_keep(), + * and result in two revisions: The latest revision, and the original. + * + * @ticket 57320 + * + * @covers ::wp_save_post_revision + */ + public function test_wp_save_post_revision_should_respect_revisions_before_deletion_filter() { + $post_id = self::factory()->post->create( array( 'post_title' => 'Test 57320' ) ); + + add_filter( + 'wp_revisions_to_keep', + static function() { + return 1; + } + ); + + add_filter( + 'wp_save_post_revision_revisions_before_deletion', + static function ( $revisions ) { + // Ignore the first revision and return the rest for deletion. + return array_slice( $revisions, 1 ); + } + ); + + for ( $update = 1; $update < 4; ++$update ) { + wp_update_post( + array( + 'ID' => $post_id, + 'post_title' => 'Test 57320 Update ' . $update, + ) + ); + } + + $actual = wp_get_post_revisions( $post_id ); + + $this->assertCount( + 2, + $actual, + 'There should be two revisions.' + ); + + $first = reset( $actual ); + $second = next( $actual ); + + $this->assertSame( + 'Test 57320 Update 3', + $first->post_title, + 'The title of the first revision was incorrect.' + ); + + $this->assertSame( + 'Test 57320 Update 1', + $second->post_title, + 'The title of the second revision was incorrect.' + ); + } + + /** + * Tests that wp_save_post_revision() ignores an invalid return value + * from the 'wp_save_post_revision_revisions_before_deletion' filter + * and throws _doing_it_wrong(). + * + * @ticket 57320 + * + * @covers ::wp_save_post_revision + * + * @expectedIncorrectUsage wp_save_post_revision + */ + public function test_wp_save_post_revision_should_ignore_invalid_revisions_before_deletion_filter() { + $post_id = self::factory()->post->create( array( 'post_title' => 'Test 57320' ) ); + + add_filter( + 'wp_revisions_to_keep', + static function() { + return 1; + } + ); + + add_filter( 'wp_save_post_revision_revisions_before_deletion', '__return_null' ); + + for ( $update = 1; $update < 4; ++$update ) { + wp_update_post( + array( + 'ID' => $post_id, + 'post_title' => 'Test 57320 Update ' . $update, + ) + ); + } + + $actual = wp_get_post_revisions( $post_id ); + + $this->assertCount( + 1, + $actual, + 'There should only be one revision.' + ); + + $first = reset( $actual ); + + $this->assertSame( + 'Test 57320 Update 3', + $first->post_title, + 'The title of the first revision was incorrect.' + ); + } }