From 222cb85d1b71fd99ff089191ede01e94479ca873 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 23:33:17 +0000 Subject: [PATCH] fix: update post date to current time when publishing from custom status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Posts published from custom statuses like "Pitch" or "Assigned" were retaining their original creation date rather than updating to the actual publication time. This caused SEO issues when dates appear in URLs and created unnecessary redirects. WordPress core updates post_date when publishing from 'draft' or 'pending', but doesn't know about Edit Flow's custom statuses. This fix adds that same behavior for custom statuses by hooking into wp_insert_post_data and updating the date when transitioning to 'publish' from any unpublished status with an empty post_date_gmt. Scheduled posts and explicitly set dates are still respected. Fixes #750 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 1 + modules/custom-status/custom-status.php | 58 +++ .../CustomStatusPublishDateTest.php | 334 ++++++++++++++++++ 3 files changed, 393 insertions(+) create mode 100644 tests/Integration/CustomStatusPublishDateTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 195ac61d..6bf25f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed +* fix: update post date to current time when publishing from custom status by @GaryJones in [#856](https://github.com/Automattic/Edit-Flow/pull/856) * fix: resolve calendar drag-and-drop not persisting post date changes by @GaryJones in [#854](https://github.com/Automattic/Edit-Flow/pull/854) * fix: guard against null return from get_edit_post_link() and get_permalink() by @GaryJones in [#853](https://github.com/Automattic/Edit-Flow/pull/853) * fix: prevent get_custom_statuses() from corrupting WordPress's term cache by @GaryJones in [#852](https://github.com/Automattic/Edit-Flow/pull/852) diff --git a/modules/custom-status/custom-status.php b/modules/custom-status/custom-status.php index 8e18e538..d8bfa5c4 100644 --- a/modules/custom-status/custom-status.php +++ b/modules/custom-status/custom-status.php @@ -107,6 +107,7 @@ public function init() { add_action( 'admin_init', [ $this, 'check_timestamp_on_publish' ] ); add_filter( 'wp_insert_post_data', [ $this, 'fix_custom_status_timestamp' ], 10, 2 ); add_filter( 'wp_insert_post_data', [ $this, 'maybe_keep_post_name_empty' ], 10, 2 ); + add_filter( 'wp_insert_post_data', [ $this, 'update_post_date_on_publish_from_custom_status' ], 10, 2 ); add_filter( 'pre_wp_unique_post_slug', [ $this, 'fix_unique_post_slug' ], 10, 6 ); add_filter( 'preview_post_link', [ $this, 'fix_preview_link_part_one' ] ); add_filter( 'post_link', [ $this, 'fix_preview_link_part_two' ], 10, 3 ); @@ -1396,6 +1397,63 @@ public function fix_custom_status_timestamp( $data, $postarr ) { return $data; } + /** + * Update post_date to current time when publishing from a custom status. + * + * When a post with a custom status (like "Pitch" or "Assigned") is published, + * the post_date should reflect the actual publication time, not the original + * creation time. This matches WordPress core behavior for 'draft' and 'pending'. + * + * @since 0.10.0 + * + * @see https://github.com/Automattic/Edit-Flow/issues/750 + * + * @param array $data An array of slashed, sanitized post data. + * @param array $postarr An array of sanitized post data. + * @return array Modified post data with updated post_date if applicable. + */ + public function update_post_date_on_publish_from_custom_status( $data, $postarr ) { + // Only process when transitioning to 'publish' status. + if ( 'publish' !== $data['post_status'] ) { + return $data; + } + + // Must be an existing post (have an ID) for this to be a status transition. + if ( empty( $postarr['ID'] ) ) { + return $data; + } + + // Get the current post from the database to check its current status. + $current_post = get_post( $postarr['ID'] ); + if ( ! $current_post ) { + return $data; + } + + // If already published, scheduled, or private, don't change the date. + $published_statuses = [ 'publish', 'future', 'private' ]; + if ( in_array( $current_post->post_status, $published_statuses, true ) ) { + return $data; + } + + // If the post had an explicitly set GMT date (scheduled), don't change it. + if ( ! empty( $current_post->post_date_gmt ) + && '0000-00-00 00:00:00' !== $current_post->post_date_gmt ) { + return $data; + } + + // If the user is explicitly setting a different date in this update, respect it. + // Compare the incoming post_date with the current post_date. + if ( ! empty( $postarr['post_date'] ) && $postarr['post_date'] !== $current_post->post_date ) { + return $data; + } + + // Update post_date to current time. + $data['post_date'] = current_time( 'mysql' ); + $data['post_date_gmt'] = current_time( 'mysql', true ); + + return $data; + } + /** * A new hack! hack! hack! until core better supports custom statuses` * diff --git a/tests/Integration/CustomStatusPublishDateTest.php b/tests/Integration/CustomStatusPublishDateTest.php new file mode 100644 index 00000000..2cfb3f1b --- /dev/null +++ b/tests/Integration/CustomStatusPublishDateTest.php @@ -0,0 +1,334 @@ +user->create( array( 'role' => 'administrator' ) ); + + self::$ef_custom_status = new EF_Custom_Status(); + self::$ef_custom_status->install(); + self::$ef_custom_status->init(); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_user_id ); + self::$ef_custom_status = null; + } + + protected function setUp(): void { + parent::setUp(); + + global $pagenow; + $pagenow = 'post.php'; + } + + protected function tearDown(): void { + global $pagenow; + $pagenow = 'index.php'; + + parent::tearDown(); + } + + /** + * Test that publishing a post from a custom status updates post_date to current time. + * + * This test demonstrates the issue reported in #750: + * Posts published from custom statuses retain their original creation date + * instead of being updated to the actual publication date. + */ + public function test_publishing_from_custom_status_updates_post_date() { + // Create a post with 'pitch' status at a specific time in the past. + $past_date = '2020-01-15 10:30:00'; + + $post_id = self::factory()->post->create( + array( + 'post_status' => 'pitch', + 'post_author' => self::$admin_user_id, + 'post_title' => 'Test Post', + 'post_date' => $past_date, + ) + ); + + $post_before = get_post( $post_id ); + + // Verify the post was created with the past date. + $this->assertEquals( $past_date, $post_before->post_date, 'Post should be created with the specified past date.' ); + $this->assertEquals( '0000-00-00 00:00:00', $post_before->post_date_gmt, 'Custom status posts should have empty GMT date.' ); + + // Now publish the post (simulating what happens when an editor clicks "Publish"). + $current_time_before_publish = current_time( 'mysql' ); + + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'publish', + ) + ); + + $current_time_after_publish = current_time( 'mysql' ); + + $post_after = get_post( $post_id ); + + // The post_date should be updated to reflect the actual publish time, + // not retain the old creation date. + $this->assertNotEquals( + $past_date, + $post_after->post_date, + 'Post date should be updated when publishing from custom status.' + ); + + // The new post_date should be approximately the current time. + $this->assertGreaterThanOrEqual( + $current_time_before_publish, + $post_after->post_date, + 'Post date should be at or after the time we started publishing.' + ); + + $this->assertLessThanOrEqual( + $current_time_after_publish, + $post_after->post_date, + 'Post date should be at or before the time we finished publishing.' + ); + + // post_date_gmt should also be set correctly. + $this->assertNotEquals( + '0000-00-00 00:00:00', + $post_after->post_date_gmt, + 'GMT date should be set when publishing.' + ); + } + + /** + * Test that publishing a post from 'draft' status updates post_date. + * + * Draft is a core status that Edit Flow overrides; it should behave the same way. + */ + public function test_publishing_from_draft_status_updates_post_date() { + $past_date = '2020-06-20 14:00:00'; + + $post_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => self::$admin_user_id, + 'post_title' => 'Draft Test Post', + 'post_date' => $past_date, + ) + ); + + $post_before = get_post( $post_id ); + $this->assertEquals( $past_date, $post_before->post_date ); + + $current_time_before_publish = current_time( 'mysql' ); + + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'publish', + ) + ); + + $post_after = get_post( $post_id ); + + $this->assertNotEquals( + $past_date, + $post_after->post_date, + 'Post date should be updated when publishing from draft status.' + ); + + $this->assertGreaterThanOrEqual( + $current_time_before_publish, + $post_after->post_date, + 'Post date should reflect the publish time.' + ); + } + + /** + * Test that publishing a post from 'pending' status updates post_date. + */ + public function test_publishing_from_pending_status_updates_post_date() { + $past_date = '2019-03-10 08:15:00'; + + $post_id = self::factory()->post->create( + array( + 'post_status' => 'pending', + 'post_author' => self::$admin_user_id, + 'post_title' => 'Pending Test Post', + 'post_date' => $past_date, + ) + ); + + $post_before = get_post( $post_id ); + $this->assertEquals( $past_date, $post_before->post_date ); + + $current_time_before_publish = current_time( 'mysql' ); + + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'publish', + ) + ); + + $post_after = get_post( $post_id ); + + $this->assertNotEquals( + $past_date, + $post_after->post_date, + 'Post date should be updated when publishing from pending status.' + ); + + $this->assertGreaterThanOrEqual( + $current_time_before_publish, + $post_after->post_date, + 'Post date should reflect the publish time.' + ); + } + + /** + * Test that a scheduled post retains its scheduled date when it becomes published. + * + * This is the expected behavior for scheduled posts - they should keep their + * scheduled date, not update to current time. + */ + public function test_scheduled_post_retains_date_when_published() { + $future_date = gmdate( 'Y-m-d H:i:s', strtotime( '+1 day' ) ); + $future_date_gmt = get_gmt_from_date( $future_date ); + + $post_id = self::factory()->post->create( + array( + 'post_status' => 'future', + 'post_author' => self::$admin_user_id, + 'post_title' => 'Scheduled Test Post', + 'post_date' => $future_date, + 'post_date_gmt' => $future_date_gmt, + ) + ); + + $post_before = get_post( $post_id ); + $this->assertEquals( $future_date, $post_before->post_date ); + $this->assertEquals( $future_date_gmt, $post_before->post_date_gmt ); + + // Simulate the scheduled publish (what wp-cron does). + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'publish', + ) + ); + + $post_after = get_post( $post_id ); + + // Scheduled posts should retain their scheduled date. + $this->assertEquals( + $future_date, + $post_after->post_date, + 'Scheduled posts should retain their scheduled date when published.' + ); + } + + /** + * Test that publishing via REST API also updates the post date. + */ + public function test_publishing_from_custom_status_via_rest_api_updates_post_date() { + wp_set_current_user( self::$admin_user_id ); + + $past_date = '2021-02-28 16:45:00'; + + $post_id = self::factory()->post->create( + array( + 'post_status' => 'pitch', + 'post_author' => self::$admin_user_id, + 'post_title' => 'REST API Test Post', + 'post_date' => $past_date, + ) + ); + + $post_before = get_post( $post_id ); + $this->assertEquals( $past_date, $post_before->post_date ); + + $current_time_before_publish = current_time( 'mysql' ); + + // Publish via REST API. + $request = new \WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->set_body_params( + array( + 'status' => 'publish', + ) + ); + rest_get_server()->dispatch( $request ); + + $post_after = get_post( $post_id ); + + $this->assertNotEquals( + $past_date, + $post_after->post_date, + 'Post date should be updated when publishing via REST API from custom status.' + ); + + $this->assertGreaterThanOrEqual( + $current_time_before_publish, + $post_after->post_date, + 'Post date should reflect the publish time.' + ); + } + + /** + * Test that if a user explicitly sets a post date before publishing, it is respected. + * + * If the user deliberately backdates or forward-dates a post, that should be honored. + */ + public function test_explicit_post_date_is_respected_when_publishing() { + $creation_date = '2020-01-15 10:30:00'; + $explicit_date = '2023-06-15 12:00:00'; + + $post_id = self::factory()->post->create( + array( + 'post_status' => 'pitch', + 'post_author' => self::$admin_user_id, + 'post_title' => 'Explicit Date Test Post', + 'post_date' => $creation_date, + ) + ); + + // Publish with an explicitly set date. + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'publish', + 'post_date' => $explicit_date, + ) + ); + + $post_after = get_post( $post_id ); + + $this->assertEquals( + $explicit_date, + $post_after->post_date, + 'Explicitly set post date should be respected when publishing.' + ); + } +}