diff --git a/src/php/Admin/Menus/Manage/Manage_Menu_Assets.php b/src/php/Admin/Menus/Manage/Manage_Menu_Assets.php index be1ebac7f..3d4f2eec2 100644 --- a/src/php/Admin/Menus/Manage/Manage_Menu_Assets.php +++ b/src/php/Admin/Menus/Manage/Manage_Menu_Assets.php @@ -111,6 +111,9 @@ public function enqueue( array $script_dependencies, array $style_dependencies ) function ( Snippet $snippet ) { $fields = $snippet->get_fields(); $fields['code'] = ''; + // Match the REST response: a UTC value with no offset is read + // as local time by the browser. + $fields['modified'] = $snippet->modified_iso; return $fields; }, get_snippets() diff --git a/src/php/Model/Snippet.php b/src/php/Model/Snippet.php index 95a166577..f74cd9ceb 100644 --- a/src/php/Model/Snippet.php +++ b/src/php/Model/Snippet.php @@ -463,6 +463,24 @@ protected function get_modified_timestamp(): int { return $datetime ? $datetime->getTimestamp() : 0; } + /** + * Retrieve the modification date as an ISO 8601 string, including the UTC + * offset. + * + * `$modified` is stored as 'Y-m-d H:i:s' in UTC, which carries no offset, so + * anything parsing it — a browser, in particular — is free to read it as + * local time and land the snippet hours away from when it was really saved. + * This is the form to hand to clients. + * + * @return string|null ISO 8601 date, or null if no modification date is set. + * @noinspection PhpUnused + */ + protected function get_modified_iso(): ?string { + $timestamp = $this->get_modified_timestamp(); + + return $timestamp ? gmdate( 'c', $timestamp ) : null; + } + /** * Retrieve the modification time in the local timezone. * diff --git a/src/php/REST_API/Snippets/Snippets_REST_Controller.php b/src/php/REST_API/Snippets/Snippets_REST_Controller.php index 1f925ce45..c1b8ff455 100644 --- a/src/php/REST_API/Snippets/Snippets_REST_Controller.php +++ b/src/php/REST_API/Snippets/Snippets_REST_Controller.php @@ -826,6 +826,10 @@ public function prepare_item_for_response( $item, $request ) { $response[ $property ] = $item->$property; } + // The schema declares this as a date-time, so send one: the stored value + // is UTC without an offset, which clients read as local time. + $response['modified'] = $item->modified_iso; + return rest_ensure_response( $response ); } diff --git a/tests/unit/Model/Snippet_Test.php b/tests/unit/Model/Snippet_Test.php index b611f675b..326b35182 100644 --- a/tests/unit/Model/Snippet_Test.php +++ b/tests/unit/Model/Snippet_Test.php @@ -30,4 +30,40 @@ public function test_description_round_trips_without_sanitization(): void { $this->assertSame( $description, $snippet->desc ); } + + /** + * The modification date is exposed to clients with an explicit UTC offset. + * + * Stored as 'Y-m-d H:i:s' in UTC, the raw value carries no offset, so a + * browser reads it as local time and shows a snippet saved moments ago as + * hours into the future on any site behind UTC. + * + * @return void + */ + public function test_modified_is_exposed_as_iso_8601_utc(): void { + $snippet = new Snippet( + [ + 'name' => 'Timezone', + 'modified' => '2026-08-27 07:35:20', + ] + ); + + $this->assertSame( '2026-08-27T07:35:20+00:00', $snippet->modified_iso ); + $this->assertNotSame( + $snippet->modified, + $snippet->modified_iso, + 'the stored value has no offset, so it must not be handed to clients as-is' + ); + } + + /** + * A snippet with no modification date exposes null rather than an epoch date. + * + * @return void + */ + public function test_modified_iso_is_null_when_unset(): void { + $snippet = new Snippet( [ 'name' => 'Never modified' ] ); + + $this->assertNull( $snippet->modified_iso ); + } }