From a9c55fab50a0e3b2185c7232112c67c350787958 Mon Sep 17 00:00:00 2001 From: fabiomsnunes Date: Wed, 14 Oct 2026 15:40:50 +0100 Subject: [PATCH] fix: use the post passed to before_delete_post when cleaning up a deleted copy --- src/post-republisher.php | 14 +++-- tests/Unit/Post_Republisher_Test.php | 80 +++++++++++++++++++++++++++- tests/WP/Post_Republisher_Test.php | 20 +++++++ 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/src/post-republisher.php b/src/post-republisher.php index 5ca7ea7fa..6aa7d3833 100644 --- a/src/post-republisher.php +++ b/src/post-republisher.php @@ -66,7 +66,7 @@ public function register_hooks() { // Clean up orphaned R&R copies when opening a post for editing. \add_action( 'load-post.php', [ $this, 'clean_up_orphaned_copy' ], 11 ); // Clean up the original when the copy is manually deleted from the trash. - \add_action( 'before_delete_post', [ $this, 'clean_up_when_copy_manually_deleted' ] ); + \add_action( 'before_delete_post', [ $this, 'clean_up_when_copy_manually_deleted' ], 10, 2 ); // Ensure scheduled Rewrite and Republish posts are properly handled. \add_action( 'future_to_publish', [ $this, 'republish_scheduled_post' ] ); } @@ -517,14 +517,18 @@ protected function determine_post_status( $post, $original_post ) { /** * Deletes the original post meta that flags it as having a copy when the copy is manually deleted. * - * @param int $post_id Post ID of a post that is going to be deleted. + * @param int $post_id Post ID of a post that is going to be deleted. + * @param WP_Post|null $post Post object of the post that is going to be deleted. * * @return void */ - public function clean_up_when_copy_manually_deleted( $post_id ) { - $post = \get_post( $post_id ); + public function clean_up_when_copy_manually_deleted( $post_id, $post = null ) { + if ( ! $post instanceof WP_Post ) { + $post = \get_post( $post_id ); + } - if ( ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { + // The post can already be gone when a concurrent request deleted it first. + if ( ! $post instanceof WP_Post || ! $this->permissions_helper->is_rewrite_and_republish_copy( $post ) ) { return; } diff --git a/tests/Unit/Post_Republisher_Test.php b/tests/Unit/Post_Republisher_Test.php index e89e36920..b0b937048 100644 --- a/tests/Unit/Post_Republisher_Test.php +++ b/tests/Unit/Post_Republisher_Test.php @@ -123,7 +123,7 @@ public function test_register_hooks() { ->with( [ $this->instance, 'clean_up_orphaned_copy' ], 11 ); Monkey\Actions\expectAdded( 'before_delete_post' ) - ->with( [ $this->instance, 'clean_up_when_copy_manually_deleted' ] ); + ->with( [ $this->instance, 'clean_up_when_copy_manually_deleted' ], 10, 2 ); $this->instance->register_hooks(); } @@ -795,4 +795,82 @@ public function test_clean_up_orphaned_copy_returns_early_when_get_params_missin $this->instance->clean_up_orphaned_copy(); } + + /** + * Tests that clean_up_when_copy_manually_deleted removes the flag from the original using the passed post. + * + * @covers \Yoast\WP\Duplicate_Post\Post_Republisher::clean_up_when_copy_manually_deleted + * + * @return void + */ + public function test_clean_up_when_copy_manually_deleted_uses_passed_post() { + $copy = Mockery::mock( WP_Post::class ); + $copy->ID = 123; + + Monkey\Functions\expect( 'get_post' ) + ->never(); + + $this->permissions_helper + ->expects( 'is_rewrite_and_republish_copy' ) + ->once() + ->with( $copy ) + ->andReturnTrue(); + + Monkey\Functions\expect( 'get_post_meta' ) + ->once() + ->with( 123, '_dp_original', true ) + ->andReturn( '1' ); + + Monkey\Functions\expect( 'delete_post_meta' ) + ->once() + ->with( 1, '_dp_has_rewrite_republish_copy' ); + + $this->instance->clean_up_when_copy_manually_deleted( 123, $copy ); + } + + /** + * Tests that clean_up_when_copy_manually_deleted returns early when the post is not a Rewrite & Republish copy. + * + * @covers \Yoast\WP\Duplicate_Post\Post_Republisher::clean_up_when_copy_manually_deleted + * + * @return void + */ + public function test_clean_up_when_copy_manually_deleted_returns_early_when_not_a_copy() { + $post = Mockery::mock( WP_Post::class ); + $post->ID = 123; + + $this->permissions_helper + ->expects( 'is_rewrite_and_republish_copy' ) + ->once() + ->with( $post ) + ->andReturnFalse(); + + Monkey\Functions\expect( 'delete_post_meta' ) + ->never(); + + $this->instance->clean_up_when_copy_manually_deleted( 123, $post ); + } + + /** + * Tests that clean_up_when_copy_manually_deleted returns early when the post no longer exists. + * + * @covers \Yoast\WP\Duplicate_Post\Post_Republisher::clean_up_when_copy_manually_deleted + * + * @return void + */ + public function test_clean_up_when_copy_manually_deleted_returns_early_when_post_is_gone() { + Monkey\Functions\expect( 'get_post' ) + ->once() + ->with( 123 ) + ->andReturnNull(); + + $this->permissions_helper + ->expects( 'is_rewrite_and_republish_copy' ) + ->never(); + + Monkey\Functions\expect( 'delete_post_meta' ) + ->never(); + + $this->instance->clean_up_when_copy_manually_deleted( 123 ); + } } diff --git a/tests/WP/Post_Republisher_Test.php b/tests/WP/Post_Republisher_Test.php index 67a3a8ae1..f3eaeefbe 100644 --- a/tests/WP/Post_Republisher_Test.php +++ b/tests/WP/Post_Republisher_Test.php @@ -1309,6 +1309,26 @@ public function test_clean_up_when_copy_manually_deleted_removes_original_meta() $this->assertSame( '', \get_post_meta( $original->ID, '_dp_has_rewrite_republish_copy', true ) ); } + /** + * Tests that clean_up_when_copy_manually_deleted does not fail when a concurrent request already deleted the copy. + * + * @covers ::clean_up_when_copy_manually_deleted + * + * @return void + */ + public function test_clean_up_when_copy_manually_deleted_when_copy_was_already_deleted() { + $original = $this->create_original_post(); + $copy = $this->create_rewrite_and_republish_copy( $original ); + + // A concurrent request deletes the copy after this request's wp_delete_post() has read it. + \wp_delete_post( $copy->ID, true ); + + $this->instance->clean_up_when_copy_manually_deleted( $copy->ID, $copy ); + $this->instance->clean_up_when_copy_manually_deleted( $copy->ID ); + + $this->assertSame( '', \get_post_meta( $original->ID, '_dp_has_rewrite_republish_copy', true ) ); + } + /** * Tests that delete_copy fires the duplicate_post_after_rewriting action. *