diff --git a/admin-functions.php b/admin-functions.php
index 26acf31ae..1c680f3a3 100644
--- a/admin-functions.php
+++ b/admin-functions.php
@@ -136,6 +136,7 @@ function duplicate_post_plugin_upgrade() {
add_option( 'duplicate_post_show_original_column', '0' );
add_option( 'duplicate_post_show_original_in_post_states', '0' );
add_option( 'duplicate_post_show_original_meta_box', '0' );
+ add_option( 'duplicate_post_preserve_date_on_scheduled_republish', '0' );
add_option(
'duplicate_post_show_link',
[
diff --git a/src/admin/options.php b/src/admin/options.php
index 995ac18c8..6943f008b 100644
--- a/src/admin/options.php
+++ b/src/admin/options.php
@@ -179,6 +179,12 @@ public function get_options() {
'value' => 1,
'description' => \__( 'except pingbacks and trackbacks', 'duplicate-post' ),
],
+ 'duplicate_post_preserve_date_on_scheduled_republish' => [
+ 'tab' => 'what-to-copy',
+ 'type' => 'checkbox',
+ 'label' => \__( 'Preserve the original publication date', 'duplicate-post' ),
+ 'value' => 1,
+ ],
'duplicate_post_copymenuorder' => [
'tab' => 'what-to-copy',
'fieldset' => 'elements-to-copy',
diff --git a/src/admin/views/options.php b/src/admin/views/options.php
index a1652ac27..49d5744ef 100644
--- a/src/admin/views/options.php
+++ b/src/admin/views/options.php
@@ -133,6 +133,16 @@ class="nav-tab"
+
+ |
+
+ generate_input( 'duplicate_post_preserve_date_on_scheduled_republish' );
+ ?>
+
+ |
+
republish( $copy, $original_post );
+ $this->republish( $copy, $original_post, $preserve_original_date );
\kses_init_filters();
$this->delete_copy( $copy->ID, $original_post->ID );
}
@@ -304,12 +307,13 @@ public function is_rest_request() {
/**
* Republishes the post by overwriting the original post.
*
- * @param WP_Post $post The Rewrite & Republish copy.
- * @param WP_Post $original_post The original post.
+ * @param WP_Post $post The Rewrite & Republish copy.
+ * @param WP_Post $original_post The original post.
+ * @param bool $preserve_original_date Whether to preserve the original post date.
*
* @return void
*/
- public function republish( WP_Post $post, WP_Post $original_post ) {
+ public function republish( WP_Post $post, WP_Post $original_post, $preserve_original_date = false ) {
/**
* Fires before the Rewrite & Republish copy is republished to the original post.
@@ -333,7 +337,7 @@ public function republish( WP_Post $post, WP_Post $original_post ) {
$this->republish_post_meta( $post );
// Republish the post.
- $this->republish_post_elements( $post, $original_post );
+ $this->republish_post_elements( $post, $original_post, $preserve_original_date );
// Mark the copy as already published.
\update_post_meta( $post->ID, '_dp_has_been_republished', '1' );
@@ -387,12 +391,13 @@ public function delete_copy( $copy_id, $post_id = null, $permanently_delete = tr
/**
* Republishes the post elements overwriting the original post.
*
- * @param WP_Post $post The post object.
- * @param WP_Post $original_post The original post.
+ * @param WP_Post $post The post object.
+ * @param WP_Post $original_post The original post.
+ * @param bool $preserve_original_date Whether to preserve the original post date.
*
* @return void
*/
- protected function republish_post_elements( $post, $original_post ) {
+ protected function republish_post_elements( $post, $original_post, $preserve_original_date = false ) {
// Cast to array and not alter the copy's original object.
$post_to_be_rewritten = clone $post;
@@ -401,6 +406,15 @@ protected function republish_post_elements( $post, $original_post ) {
$post_to_be_rewritten->post_name = $original_post->post_name;
$post_to_be_rewritten->post_status = $this->determine_post_status( $post, $original_post );
+ if ( $preserve_original_date ) {
+ unset(
+ $post_to_be_rewritten->post_date,
+ $post_to_be_rewritten->post_date_gmt,
+ $post_to_be_rewritten->post_modified,
+ $post_to_be_rewritten->post_modified_gmt,
+ );
+ }
+
/**
* Yoast SEO and other plugins prevent from accidentally updating another post's
* data (e.g. the Yoast SEO metadata by checking the $_POST data ID with the post object ID.
diff --git a/tests/Unit/Post_Republisher_Test.php b/tests/Unit/Post_Republisher_Test.php
index e89e36920..e9fb372cc 100644
--- a/tests/Unit/Post_Republisher_Test.php
+++ b/tests/Unit/Post_Republisher_Test.php
@@ -546,10 +546,60 @@ public function test_republish_scheduled_post() {
->once()
->andReturn( $original );
+ Monkey\Functions\expect( 'get_option' )
+ ->with( 'duplicate_post_preserve_date_on_scheduled_republish' )
+ ->once()
+ ->andReturn( '0' );
+
Monkey\Functions\expect( 'kses_remove_filters' );
Monkey\Functions\expect( 'kses_init_filters' );
- $this->instance->expects( 'republish' )->with( $copy, $original )->once();
+ $this->instance->expects( 'republish' )->with( $copy, $original, false )->once();
+ $this->instance->expects( 'delete_copy' )->with( $copy->ID, $original->ID )->once();
+
+ $this->instance->republish_scheduled_post( $copy );
+ }
+
+ /**
+ * Tests the republish_scheduled_post function when the original date should be preserved.
+ *
+ * @covers \Yoast\WP\Duplicate_Post\Post_Republisher::republish_scheduled_post
+ * @runInSeparateProcess
+ * @preserveGlobalState disabled
+ *
+ * @return void
+ */
+ public function test_republish_scheduled_post_preserves_original_date_when_enabled() {
+ $original = Mockery::mock( WP_Post::class );
+ $original->ID = 1;
+ $original->post_status = 'publish';
+
+ $copy = Mockery::mock( WP_Post::class );
+ $copy->ID = 123;
+ $copy->post_status = 'future';
+
+ $this->permissions_helper
+ ->expects( 'is_rewrite_and_republish_copy' )
+ ->with( $copy )
+ ->once()
+ ->andReturnTrue();
+
+ $utils = Mockery::mock( 'alias:\Yoast\WP\Duplicate_Post\Utils' );
+ $utils
+ ->expects( 'get_original' )
+ ->with( $copy->ID )
+ ->once()
+ ->andReturn( $original );
+
+ Monkey\Functions\expect( 'get_option' )
+ ->with( 'duplicate_post_preserve_date_on_scheduled_republish' )
+ ->once()
+ ->andReturn( '1' );
+
+ Monkey\Functions\expect( 'kses_remove_filters' );
+ Monkey\Functions\expect( 'kses_init_filters' );
+
+ $this->instance->expects( 'republish' )->with( $copy, $original, true )->once();
$this->instance->expects( 'delete_copy' )->with( $copy->ID, $original->ID )->once();
$this->instance->republish_scheduled_post( $copy );
diff --git a/tests/WP/Admin_Functions_Test.php b/tests/WP/Admin_Functions_Test.php
index 23ae40cea..f21c05626 100644
--- a/tests/WP/Admin_Functions_Test.php
+++ b/tests/WP/Admin_Functions_Test.php
@@ -30,6 +30,7 @@ final class Admin_Functions_Test extends TestCase {
'duplicate_post_copycontent',
'duplicate_post_copyexcerpt',
'duplicate_post_copydate',
+ 'duplicate_post_preserve_date_on_scheduled_republish',
'duplicate_post_copystatus',
'duplicate_post_copyslug',
'duplicate_post_copyauthor',
diff --git a/tests/WP/Post_Republisher_Test.php b/tests/WP/Post_Republisher_Test.php
index 67a3a8ae1..b59e4fb8c 100644
--- a/tests/WP/Post_Republisher_Test.php
+++ b/tests/WP/Post_Republisher_Test.php
@@ -47,6 +47,7 @@ public function set_up() {
// Enable post and page for duplication.
\update_option( 'duplicate_post_types_enabled', [ 'post', 'page' ] );
+ \update_option( 'duplicate_post_preserve_date_on_scheduled_republish', '0' );
$this->post_duplicator = new Post_Duplicator();
$this->permissions_helper = new Permissions_Helper();
@@ -458,6 +459,117 @@ public function test_republish_scheduled_post_republishes_copy() {
$this->assertSame( '', \get_post_meta( $original_id, '_dp_has_rewrite_republish_copy', true ) );
}
+ /**
+ * Tests republish_scheduled_post preserves the original post date when the option is enabled.
+ *
+ * @covers ::republish_scheduled_post
+ * @covers ::republish_post_elements
+ *
+ * @return void
+ */
+ public function test_republish_scheduled_post_preserves_original_date_when_option_enabled() {
+ \update_option( 'duplicate_post_preserve_date_on_scheduled_republish', '1' );
+
+ $original = $this->create_original_post(
+ [
+ 'post_title' => 'Original Title',
+ 'post_content' => 'Original content.',
+ 'post_date' => '2020-01-01 00:00:00',
+ 'post_date_gmt' => '2020-01-01 00:00:00',
+ ],
+ );
+
+ $copy = $this->create_rewrite_and_republish_copy( $original );
+
+ // Pin the original's modified date in the past so the refresh is observable.
+ // wp_update_post is not used here because it recalculates post_modified itself.
+ global $wpdb;
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
+ $wpdb->update(
+ $wpdb->posts,
+ [
+ 'post_modified' => '2020-01-02 00:00:00',
+ 'post_modified_gmt' => '2020-01-02 00:00:00',
+ ],
+ [ 'ID' => $original->ID ],
+ );
+ \clean_post_cache( $original->ID );
+
+ $this->update_post_without_republish(
+ [
+ 'ID' => $copy->ID,
+ 'post_title' => 'Scheduled Updated Title',
+ 'post_content' => 'Scheduled updated content.',
+ ],
+ );
+
+ $copy = $this->schedule_copy_for_future( $copy );
+
+ $this->instance->republish_scheduled_post( $copy );
+
+ $updated_original = \get_post( $original->ID );
+
+ // The content is republished as usual.
+ $this->assertSame( 'Scheduled Updated Title', $updated_original->post_title );
+ $this->assertSame( 'Scheduled updated content.', $updated_original->post_content );
+
+ // The publish date is untouched, so the post does not jump to the top of the feed.
+ $this->assertSame( '2020-01-01 00:00:00', $updated_original->post_date );
+ $this->assertSame( '2020-01-01 00:00:00', $updated_original->post_date_gmt );
+
+ // The modified date is refreshed, mimicking a default WordPress post update.
+ $this->assertNotSame( '2020-01-02 00:00:00', $updated_original->post_modified );
+ $this->assertNotSame( '2020-01-02 00:00:00', $updated_original->post_modified_gmt );
+ $this->assertSame( 'publish', $updated_original->post_status );
+
+ // Verify the copy was deleted.
+ $this->assertNull( \get_post( $copy->ID ) );
+ }
+
+ /**
+ * Tests republish_scheduled_post overwrites the original post date by default.
+ *
+ * Without the option, the pre-existing behavior is kept: the original takes the
+ * scheduled date of the copy.
+ *
+ * @covers ::republish_scheduled_post
+ * @covers ::republish_post_elements
+ *
+ * @return void
+ */
+ public function test_republish_scheduled_post_overwrites_original_date_by_default() {
+ $original = $this->create_original_post(
+ [
+ 'post_title' => 'Original Title',
+ 'post_content' => 'Original content.',
+ 'post_date' => '2020-01-01 00:00:00',
+ 'post_date_gmt' => '2020-01-01 00:00:00',
+ ],
+ );
+
+ $copy = $this->create_rewrite_and_republish_copy( $original );
+
+ $this->update_post_without_republish(
+ [
+ 'ID' => $copy->ID,
+ 'post_title' => 'Scheduled Updated Title',
+ 'post_content' => 'Scheduled updated content.',
+ ],
+ );
+
+ $copy = $this->schedule_copy_for_future( $copy );
+
+ $this->instance->republish_scheduled_post( $copy );
+
+ $updated_original = \get_post( $original->ID );
+
+ $this->assertSame( 'Scheduled Updated Title', $updated_original->post_title );
+
+ // Without the option the original adopts the copy's scheduled date.
+ $this->assertSame( $copy->post_date, $updated_original->post_date );
+ $this->assertNotSame( '2020-01-01 00:00:00', $updated_original->post_date );
+ }
+
/**
* Tests republish_scheduled_post trashes copy when original is deleted.
*