Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions admin-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
[
Expand Down
6 changes: 6 additions & 0 deletions src/admin/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions src/admin/views/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ class="nav-tab"
</fieldset>
</td>
</tr>
<tr>
<th scope="row"><?php \esc_html_e( 'Scheduled Rewrite & Republish', 'duplicate-post' ); ?></th>
<td>
<?php
// phpcs:ignore WordPress.Security.EscapeOutput -- Already escapes correctly.
echo $this->generate_input( 'duplicate_post_preserve_date_on_scheduled_republish' );
?>
<p><?php \esc_html_e( 'Keep the original publication date when a scheduled Rewrite & Republish update is applied. The modified date is always updated.', 'duplicate-post' ); ?></p>
</td>
</tr>
</table>
</section>
<section
Expand Down
30 changes: 22 additions & 8 deletions src/post-republisher.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,11 @@ public function republish_scheduled_post( $copy ) {
return;
}

// Only scheduled republishing can change the publish date, so the option applies here.
$preserve_original_date = ( (int) \get_option( 'duplicate_post_preserve_date_on_scheduled_republish' ) === 1 );

\kses_remove_filters();
$this->republish( $copy, $original_post );
$this->republish( $copy, $original_post, $preserve_original_date );
\kses_init_filters();
$this->delete_copy( $copy->ID, $original_post->ID );
}
Expand Down Expand Up @@ -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.
Expand All @@ -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' );
Expand Down Expand Up @@ -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;

Expand All @@ -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.
Expand Down
52 changes: 51 additions & 1 deletion tests/Unit/Post_Republisher_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
1 change: 1 addition & 0 deletions tests/WP/Admin_Functions_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
112 changes: 112 additions & 0 deletions tests/WP/Post_Republisher_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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.
*
Expand Down