Skip to content

Commit b022cfa

Browse files
swissspidyclaude
andcommitted
Reject invalid modification dates and cover attachments
The wp_insert_post_data filter runs after core has validated post_date, and get_gmt_from_date() / get_date_from_gmt() fall back to the epoch for input they cannot parse, so `--post_modified=banana` reported success and stored 0000-00-00 00:00:00 or 1970-01-01 00:00:00. Validate each supplied value the way wp_resolve_post_date() validates post_date and return the same `Invalid date.` error core gives for a bad `--post_date`, before anything is written. Attachments take wp_insert_attachment_data rather than wp_insert_post_data, so `post update <attachment-id> --post_modified=...` was still a silent no-op. Hook the callback on both filters and remove both afterwards. Scenarios cover the invalid values on create and update (asserting the stored dates are unchanged on update), an attachment on create and update, and an explicit `--post_modified` winning over the source post's when duplicating with `--from-post`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QWUCg3yvcZ4uDBf2cwQ7X1
1 parent 07fe403 commit b022cfa

3 files changed

Lines changed: 155 additions & 8 deletions

File tree

‎features/post-create-duplicate.feature‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,14 @@ Feature: Create Duplicate WordPress post from existing posts.
9999
"""
100100
2015-03-03
101101
"""
102+
103+
# An explicit modification date still wins over the source post's.
104+
When I run `wp post create --from-post={SOURCE_ID} --post_title='Dated duplicate' --post_modified='2016-04-04 10:00:00' --porcelain`
105+
Then STDOUT should be a number
106+
And save STDOUT as {DATED_DUPLICATE_ID}
107+
108+
When I run `wp post get {DATED_DUPLICATE_ID} --field=post_modified`
109+
Then STDOUT should be:
110+
"""
111+
2016-04-04 10:00:00
112+
"""

‎features/post.feature‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,3 +982,94 @@ Feature: Manage WordPress posts
982982
"""
983983
2020-06-01 08:00:00
984984
"""
985+
986+
Scenario: An invalid modification date is rejected on create
987+
Given a WP install
988+
989+
When I try `wp post create --post_title='Bad date' --post_status=publish --post_modified='banana'`
990+
Then the return code should be 1
991+
And STDERR should be:
992+
"""
993+
Error: Invalid date.
994+
"""
995+
996+
When I try `wp post create --post_title='Bad GMT date' --post_status=publish --post_modified_gmt='banana'`
997+
Then the return code should be 1
998+
And STDERR should be:
999+
"""
1000+
Error: Invalid date.
1001+
"""
1002+
1003+
# Well-formed but not a real calendar date.
1004+
When I try `wp post create --post_title='Bad calendar date' --post_status=publish --post_modified='2020-02-30 12:00:00'`
1005+
Then the return code should be 1
1006+
And STDERR should be:
1007+
"""
1008+
Error: Invalid date.
1009+
"""
1010+
1011+
When I run `wp post list --s=Bad --format=count`
1012+
Then STDOUT should be:
1013+
"""
1014+
0
1015+
"""
1016+
1017+
Scenario: An invalid modification date is rejected on update
1018+
Given a WP install
1019+
1020+
When I run `wp post create --post_title='A post' --post_status=publish --post_modified='2020-01-01 12:00:00' --porcelain`
1021+
Then STDOUT should be a number
1022+
And save STDOUT as {POST_ID}
1023+
1024+
When I try `wp post update {POST_ID} --post_modified='banana'`
1025+
Then the return code should be 1
1026+
And STDERR should be:
1027+
"""
1028+
Warning: Invalid date.
1029+
"""
1030+
1031+
When I try `wp post update {POST_ID} --post_modified_gmt='banana'`
1032+
Then the return code should be 1
1033+
And STDERR should be:
1034+
"""
1035+
Warning: Invalid date.
1036+
"""
1037+
1038+
When I run `wp post get {POST_ID} --field=post_modified`
1039+
Then STDOUT should be:
1040+
"""
1041+
2020-01-01 12:00:00
1042+
"""
1043+
1044+
When I run `wp post get {POST_ID} --field=post_modified_gmt`
1045+
Then STDOUT should be:
1046+
"""
1047+
2020-01-01 12:00:00
1048+
"""
1049+
1050+
Scenario: Set an attachment's modification date
1051+
Given a WP install
1052+
1053+
# Attachments take the wp_insert_attachment_data filter rather than
1054+
# wp_insert_post_data, so they are a separate path to cover.
1055+
When I run `wp post create --post_type=attachment --post_title='An attachment' --post_mime_type=image/png --post_modified='2020-01-01 12:00:00' --porcelain`
1056+
Then STDOUT should be a number
1057+
And save STDOUT as {ATTACHMENT_ID}
1058+
1059+
When I run `wp post get {ATTACHMENT_ID} --field=post_modified`
1060+
Then STDOUT should be:
1061+
"""
1062+
2020-01-01 12:00:00
1063+
"""
1064+
1065+
When I run `wp post update {ATTACHMENT_ID} --post_modified='2021-02-02 13:00:00'`
1066+
Then STDOUT should be:
1067+
"""
1068+
Success: Updated post {ATTACHMENT_ID}.
1069+
"""
1070+
1071+
When I run `wp post get {ATTACHMENT_ID} --field=post_modified`
1072+
Then STDOUT should be:
1073+
"""
1074+
2021-02-02 13:00:00
1075+
"""

‎src/Post_Command.php‎

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@ public function create( $args, $assoc_args ) {
223223
$args,
224224
$assoc_args,
225225
function ( $params ) {
226+
$modified_callback = self::add_post_modified_filter( $params );
227+
if ( is_wp_error( $modified_callback ) ) {
228+
return $modified_callback;
229+
}
230+
226231
$filter_callback = null;
227232

228233
if ( 0 === get_current_user_id() && ! empty( $params['tax_input'] ) ) {
@@ -251,12 +256,10 @@ function ( $params ) {
251256
}
252257
}
253258

254-
$modified_callback = self::add_post_modified_filter( $params );
255-
256259
$result = wp_insert_post( $params, true );
257260

258261
if ( $modified_callback ) {
259-
remove_filter( 'wp_insert_post_data', $modified_callback );
262+
self::remove_post_modified_filter( $modified_callback );
260263
}
261264

262265
if ( $filter_callback ) {
@@ -276,9 +279,15 @@ function ( $params ) {
276279
* the current time — so the documented parameters have to be applied to the
277280
* post data on its way to the database.
278281
*
282+
* The filter runs after core has validated post_date, and both
283+
* get_gmt_from_date() and get_date_from_gmt() fall back to the epoch for a
284+
* value they cannot parse, so the supplied dates are validated here the same
285+
* way core validates post_date.
286+
*
279287
* @param array<string, mixed> $params Parameters passed to wp_insert_post() or wp_update_post().
280-
* @return callable|null The registered callback, for the caller to remove, or null when
281-
* no modification date was requested.
288+
* @return callable|WP_Error|null The registered callback, for the caller to remove; a WP_Error
289+
* when a supplied date is invalid; or null when no modification
290+
* date was requested.
282291
*/
283292
private static function add_post_modified_filter( $params ) {
284293
$local = ! empty( $params['post_modified'] ) && is_scalar( $params['post_modified'] )
@@ -292,6 +301,12 @@ private static function add_post_modified_filter( $params ) {
292301
return null;
293302
}
294303

304+
foreach ( [ $local, $gmt ] as $date ) {
305+
if ( null !== $date && ! self::is_valid_post_date( $date ) ) {
306+
return new WP_Error( 'invalid_date', __( 'Invalid date.' ) );
307+
}
308+
}
309+
295310
// Keep the pair consistent when only one of the two was given.
296311
if ( null === $gmt ) {
297312
$gmt = get_gmt_from_date( $local );
@@ -308,11 +323,38 @@ private static function add_post_modified_filter( $params ) {
308323
return array_merge( $data, $modified );
309324
};
310325

326+
// Attachments go through wp_insert_attachment_data instead.
311327
add_filter( 'wp_insert_post_data', $callback );
328+
add_filter( 'wp_insert_attachment_data', $callback );
312329

313330
return $callback;
314331
}
315332

333+
/**
334+
* Removes the callback registered by add_post_modified_filter().
335+
*
336+
* @param callable $callback The callback to remove.
337+
* @return void
338+
*/
339+
private static function remove_post_modified_filter( $callback ) {
340+
remove_filter( 'wp_insert_post_data', $callback );
341+
remove_filter( 'wp_insert_attachment_data', $callback );
342+
}
343+
344+
/**
345+
* Checks a date the way core checks post_date in wp_resolve_post_date().
346+
*
347+
* @param string $date Date in MySQL format.
348+
* @return bool Whether the date is valid.
349+
*/
350+
private static function is_valid_post_date( $date ) {
351+
if ( ! preg_match( '/^(\d{4})-(\d{1,2})-(\d{1,2})/', $date, $matches ) ) {
352+
return false;
353+
}
354+
355+
return wp_checkdate( (int) $matches[2], (int) $matches[3], (int) $matches[1], $date );
356+
}
357+
316358
/**
317359
* Updates one or more existing posts.
318360
*
@@ -457,6 +499,11 @@ public function update( $args, $assoc_args ) {
457499
$args,
458500
$assoc_args,
459501
function ( $params ) {
502+
$modified_callback = self::add_post_modified_filter( $params );
503+
if ( is_wp_error( $modified_callback ) ) {
504+
return $modified_callback;
505+
}
506+
460507
$filter_callback = null;
461508

462509
if ( 0 === get_current_user_id() && ! empty( $params['tax_input'] ) ) {
@@ -485,12 +532,10 @@ function ( $params ) {
485532
}
486533
}
487534

488-
$modified_callback = self::add_post_modified_filter( $params );
489-
490535
$result = wp_update_post( $params, true );
491536

492537
if ( $modified_callback ) {
493-
remove_filter( 'wp_insert_post_data', $modified_callback );
538+
self::remove_post_modified_filter( $modified_callback );
494539
}
495540

496541
if ( $filter_callback ) {

0 commit comments

Comments
 (0)