diff --git a/src/wp-admin/includes/privacy-tools.php b/src/wp-admin/includes/privacy-tools.php index e7b949842ffd1..e76af875c7cbc 100644 --- a/src/wp-admin/includes/privacy-tools.php +++ b/src/wp-admin/includes/privacy-tools.php @@ -205,13 +205,14 @@ function _wp_personal_data_cleanup_requests() { 'date_query' => array( array( /* - * The local-time column must be used here, not post_modified_gmt: + * An absolute UTC threshold is compared against the GMT column so that + * the expiry window matches wp_validate_user_request_key() and is not + * affected by the site's timezone changing after a request was made. * WP_Date_Query resolves relative date strings like "N seconds ago" - * in the site's timezone, so comparing against the GMT column would - * shift the expiry window by the site's UTC offset. + * in the site's timezone, so a relative string cannot be used here. */ - 'column' => 'post_modified', - 'before' => $expires . ' seconds ago', + 'column' => 'post_modified_gmt', + 'before' => gmdate( 'Y-m-d H:i:s', time() - $expires ), ), ), ) diff --git a/tests/phpunit/tests/privacy/wpPersonalDataCleanupRequests.php b/tests/phpunit/tests/privacy/wpPersonalDataCleanupRequests.php index edc157b3af3dc..05b17b169f324 100644 --- a/tests/phpunit/tests/privacy/wpPersonalDataCleanupRequests.php +++ b/tests/phpunit/tests/privacy/wpPersonalDataCleanupRequests.php @@ -5,7 +5,7 @@ * Covers: * - wp_schedule_personal_data_cleanup_requests() * - wp_privacy_personal_data_cleanup_requests() - * - _wp_personal_data_cleanup_requests() (including the post_modified timezone fix) + * - _wp_personal_data_cleanup_requests() (including the expiry timezone fixes) * * @package WordPress * @subpackage UnitTests @@ -329,17 +329,13 @@ public function test_expiry_duration_is_filterable(): void { * Timezone fix: a request-pending post that is only 20 hours old must not * be marked failed on a UTC+5 site. * - * Before the fix, the date_query used 'post_modified_gmt' (a UTC column) but - * WP_Date_Query::build_mysql_datetime() resolved the relative 'before' string - * (e.g. "86400 seconds ago") using the site's local timezone and returned a - * *local-time* string. Comparing a UTC column against a local-time threshold + * The expiry threshold must be an absolute UTC time. A relative 'before' + * string such as "86400 seconds ago" is resolved by WP_Date_Query in the + * site's timezone, so comparing it against the UTC post_modified_gmt column * shifted the effective expiry window by the UTC offset — on UTC+5 sites, * requests expired 5 hours too early, flagging posts that were as recent as * 19 hours old as expired. * - * The fix changes the column to 'post_modified' (local time) so both sides - * of the comparison are in the same timezone. - * * @ticket 44498 */ public function test_unexpired_request_not_expired_on_utcplus_site(): void { @@ -374,9 +370,9 @@ public function test_unexpired_request_not_expired_on_utcplus_site(): void { * Symmetric check for UTC- sites: a request just over the expiry threshold * must be correctly marked as failed even on a UTC- site. * - * On UTC- sites, the old buggy code shifted the effective expiry window in - * the opposite direction — requests would linger longer than intended before - * being cleaned up. With the fix, the threshold is always applied consistently. + * A relative 'before' string shifted the effective expiry window in the + * opposite direction on UTC- sites — requests lingered longer than intended + * before being cleaned up. An absolute UTC threshold applies consistently. * * @ticket 44498 */ @@ -400,6 +396,30 @@ public function test_expired_request_is_marked_failed_on_utcminus_site(): void { ); } + /** + * Changing the site timezone must not retroactively expire a fresh request. + * + * @ticket 65731 + */ + public function test_fresh_request_survives_a_large_timezone_change(): void { + // UTC-11 at request time. + update_option( 'timezone_string', 'Pacific/Pago_Pago' ); + + $id = wp_create_user_request( $this->unique_email(), 'export_personal_data' ); + $this->assertIsInt( $id ); + + // UTC+14 afterwards: 25 hours ahead, more than the 24-hour expiry window. + update_option( 'timezone_string', 'Pacific/Kiritimati' ); + + _wp_personal_data_cleanup_requests(); + + $this->assertSame( + 'request-pending', + get_post_status( $id ), + 'A seconds-old request should stay request-pending after the site timezone changes.' + ); + } + // ========================================================================= // wp_privacy_personal_data_cleanup_requests() (cron callback) // =========================================================================