From 6cb1c07a5c6641a5a691bab725740e1fa2843cb7 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Mon, 14 Sep 2026 10:57:36 -0600 Subject: [PATCH 1/4] XML-RPC: Convert client-supplied dates before calling IXR_Date methods. Six methods called getIso() or getTimestamp() on client-supplied date values, assuming they were IXR_Date objects. A client sending a date as a string, array, or other type caused a fatal error. A new wp_xmlrpc_server::_convert_client_date() helper passes an IXR_Date through, converts a string the same way wp.newPost already did, and returns a 400 IXR_Error for anything else. It is used in _insert_post(), wp.editPost, wp.editComment, metaWeblog.newPost, and metaWeblog.editPost. See #66107, #42995. Co-Authored-By: Claude Fable 5.1 --- src/wp-includes/class-wp-xmlrpc-server.php | 84 ++++++++++++++++--- tests/phpunit/tests/xmlrpc/mw/editPost.php | 14 ++++ tests/phpunit/tests/xmlrpc/mw/newPost.php | 16 ++++ tests/phpunit/tests/xmlrpc/wp/editComment.php | 17 ++++ tests/phpunit/tests/xmlrpc/wp/editPost.php | 58 +++++++++++++ tests/phpunit/tests/xmlrpc/wp/newPost.php | 15 ++++ 6 files changed, 194 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index dcb2636d7a726..198ac341ccd73 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -934,6 +934,30 @@ protected function _convert_date_gmt( $date_gmt, $date ) { return $this->_convert_date( $date_gmt ); } + /** + * Converts a client-supplied date value to an IXR_Date object. + * + * XML-RPC clients may send a date either as a dateTime.iso8601 value, which + * arrives as an IXR_Date object, or as a plain string. Any other type cannot + * be a date and results in an error. + * + * @since 7.2.0 + * + * @param mixed $date Client-supplied date value. + * @return IXR_Date|IXR_Error IXR_Date object on success, IXR_Error if the value is not a date. + */ + protected function _convert_client_date( $date ) { + if ( $date instanceof IXR_Date ) { + return $date; + } + + if ( is_string( $date ) ) { + return $this->_convert_date( $date ); + } + + return new IXR_Error( 400, __( 'Dates must be a dateTime.iso8601 value or a string.' ) ); + } + /** * Prepares post data for return in an XML-RPC object. * @@ -1365,7 +1389,7 @@ public function wp_newPost( $args ) { } // Convert the date field back to IXR form. - if ( isset( $content_struct['post_date'] ) && ! ( $content_struct['post_date'] instanceof IXR_Date ) ) { + if ( isset( $content_struct['post_date'] ) && is_string( $content_struct['post_date'] ) ) { $content_struct['post_date'] = $this->_convert_date( $content_struct['post_date'] ); } @@ -1373,7 +1397,7 @@ public function wp_newPost( $args ) { * Ignore the existing GMT date if it is empty or a non-GMT date was supplied in $content_struct, * since _insert_post() will ignore the non-GMT date if the GMT date is set. */ - if ( isset( $content_struct['post_date_gmt'] ) && ! ( $content_struct['post_date_gmt'] instanceof IXR_Date ) ) { + if ( isset( $content_struct['post_date_gmt'] ) && is_string( $content_struct['post_date_gmt'] ) ) { if ( '0000-00-00 00:00:00' === $content_struct['post_date_gmt'] || isset( $content_struct['post_date'] ) ) { unset( $content_struct['post_date_gmt'] ); } else { @@ -1550,10 +1574,20 @@ protected function _insert_post( $user, $content_struct ) { // Do some timestamp voodoo. if ( ! empty( $post_data['post_date_gmt'] ) ) { + $post_date_gmt = $this->_convert_client_date( $post_data['post_date_gmt'] ); + if ( $post_date_gmt instanceof IXR_Error ) { + return $post_date_gmt; + } + // We know this is supposed to be GMT, so we're going to slap that Z on there by force. - $date_created = rtrim( $post_data['post_date_gmt']->getIso(), 'Z' ) . 'Z'; + $date_created = rtrim( $post_date_gmt->getIso(), 'Z' ) . 'Z'; } elseif ( ! empty( $post_data['post_date'] ) ) { - $date_created = $post_data['post_date']->getIso(); + $post_date = $this->_convert_client_date( $post_data['post_date'] ); + if ( $post_date instanceof IXR_Error ) { + return $post_date; + } + + $date_created = $post_date->getIso(); } // Default to not flagging the post date to be edited unless it's intentional. @@ -1792,8 +1826,13 @@ public function wp_editPost( $args ) { } if ( isset( $content_struct['if_not_modified_since'] ) ) { + $if_not_modified_since = $this->_convert_client_date( $content_struct['if_not_modified_since'] ); + if ( $if_not_modified_since instanceof IXR_Error ) { + return $if_not_modified_since; + } + // If the post has been modified since the date provided, return an error. - if ( mysql2date( 'U', $post['post_modified_gmt'] ) > $content_struct['if_not_modified_since']->getTimestamp() ) { + if ( mysql2date( 'U', $post['post_modified_gmt'] ) > $if_not_modified_since->getTimestamp() ) { return new IXR_Error( 409, __( 'There is a revision of this post that is more recent.' ) ); } } @@ -3913,8 +3952,13 @@ public function wp_editComment( $args ) { // Do some timestamp voodoo. if ( ! empty( $content_struct['date_created_gmt'] ) ) { + $date_created_gmt = $this->_convert_client_date( $content_struct['date_created_gmt'] ); + if ( $date_created_gmt instanceof IXR_Error ) { + return $date_created_gmt; + } + // We know this is supposed to be GMT, so we're going to slap that Z on there by force. - $date_created = rtrim( $content_struct['date_created_gmt']->getIso(), 'Z' ) . 'Z'; + $date_created = rtrim( $date_created_gmt->getIso(), 'Z' ) . 'Z'; $comment['comment_date'] = get_date_from_gmt( $date_created ); $comment['comment_date_gmt'] = iso8601_to_datetime( $date_created, 'gmt' ); @@ -5671,10 +5715,20 @@ public function mw_newPost( $args ) { // Do some timestamp voodoo. if ( ! empty( $content_struct['date_created_gmt'] ) ) { + $date_created_gmt = $this->_convert_client_date( $content_struct['date_created_gmt'] ); + if ( $date_created_gmt instanceof IXR_Error ) { + return $date_created_gmt; + } + // We know this is supposed to be GMT, so we're going to slap that Z on there by force. - $date_created = rtrim( $content_struct['date_created_gmt']->getIso(), 'Z' ) . 'Z'; + $date_created = rtrim( $date_created_gmt->getIso(), 'Z' ) . 'Z'; } elseif ( ! empty( $content_struct['dateCreated'] ) ) { - $date_created = $content_struct['dateCreated']->getIso(); + $date_created_object = $this->_convert_client_date( $content_struct['dateCreated'] ); + if ( $date_created_object instanceof IXR_Error ) { + return $date_created_object; + } + + $date_created = $date_created_object->getIso(); } $post_date = ''; @@ -6077,10 +6131,20 @@ public function mw_editPost( $args ) { // Do some timestamp voodoo. if ( ! empty( $content_struct['date_created_gmt'] ) ) { + $date_created_gmt = $this->_convert_client_date( $content_struct['date_created_gmt'] ); + if ( $date_created_gmt instanceof IXR_Error ) { + return $date_created_gmt; + } + // We know this is supposed to be GMT, so we're going to slap that Z on there by force. - $date_created = rtrim( $content_struct['date_created_gmt']->getIso(), 'Z' ) . 'Z'; + $date_created = rtrim( $date_created_gmt->getIso(), 'Z' ) . 'Z'; } elseif ( ! empty( $content_struct['dateCreated'] ) ) { - $date_created = $content_struct['dateCreated']->getIso(); + $date_created_object = $this->_convert_client_date( $content_struct['dateCreated'] ); + if ( $date_created_object instanceof IXR_Error ) { + return $date_created_object; + } + + $date_created = $date_created_object->getIso(); } // Default to not flagging the post date to be edited unless it's intentional. diff --git a/tests/phpunit/tests/xmlrpc/mw/editPost.php b/tests/phpunit/tests/xmlrpc/mw/editPost.php index 23515233325ff..c0efc1ddc0ab7 100644 --- a/tests/phpunit/tests/xmlrpc/mw/editPost.php +++ b/tests/phpunit/tests/xmlrpc/mw/editPost.php @@ -335,4 +335,18 @@ public function test_draft_not_prematurely_published() { $future_date_string = date_format( date_create( "@{$future_time}" ), 'Y-m-d H:i:s' ); $this->assertSame( $future_date_string, $after->post_date ); } + + /** + * @ticket 66107 + */ + public function test_string_date_created_is_accepted(): void { + $editor_id = $this->make_user_by_role( 'editor' ); + $post_id = self::factory()->post->create( array( 'post_author' => $editor_id ) ); + + $date_string = '1984-01-11 05:00:00'; + $result = $this->myxmlrpcserver->mw_editPost( array( $post_id, 'editor', 'editor', array( 'dateCreated' => $date_string ) ) ); + $this->assertNotIXRError( $result ); + $this->assertTrue( $result ); + $this->assertSame( $date_string, get_post( $post_id )->post_date ); + } } diff --git a/tests/phpunit/tests/xmlrpc/mw/newPost.php b/tests/phpunit/tests/xmlrpc/mw/newPost.php index e7c4af0cee01f..2385f40957374 100644 --- a/tests/phpunit/tests/xmlrpc/mw/newPost.php +++ b/tests/phpunit/tests/xmlrpc/mw/newPost.php @@ -201,4 +201,20 @@ public function test_draft_post_date() { $this->assertSame( 'draft', $out->post_status ); $this->assertSame( '0000-00-00 00:00:00', $out->post_date_gmt ); } + + /** + * @ticket 66107 + */ + public function test_string_date_created_is_accepted(): void { + $this->make_user_by_role( 'author' ); + + $date_string = '1984-01-11 05:00:00'; + $post = array( + 'title' => 'Test', + 'dateCreated' => $date_string, + ); + $result = $this->myxmlrpcserver->mw_newPost( array( 1, 'author', 'author', $post ) ); + $this->assertNotIXRError( $result ); + $this->assertSame( $date_string, get_post( $result )->post_date ); + } } diff --git a/tests/phpunit/tests/xmlrpc/wp/editComment.php b/tests/phpunit/tests/xmlrpc/wp/editComment.php index 0f998df96707a..719af7b7a0823 100644 --- a/tests/phpunit/tests/xmlrpc/wp/editComment.php +++ b/tests/phpunit/tests/xmlrpc/wp/editComment.php @@ -93,4 +93,21 @@ public function test_trash_comment() { $this->assertSame( 'trash', get_comment( $comment_id )->comment_approved ); } + + /** + * @ticket 66107 + * @ticket 42995 + */ + public function test_string_date_created_gmt_is_accepted(): void { + $this->make_user_by_role( 'administrator' ); + $comment_id = self::factory()->comment->create(); + + $date_string = '1984-01-11 05:00:00'; + $result = $this->myxmlrpcserver->wp_editComment( + array( 1, 'administrator', 'administrator', $comment_id, array( 'date_created_gmt' => $date_string ) ) + ); + $this->assertNotIXRError( $result ); + $this->assertTrue( $result ); + $this->assertSame( $date_string, get_comment( $comment_id )->comment_date_gmt ); + } } diff --git a/tests/phpunit/tests/xmlrpc/wp/editPost.php b/tests/phpunit/tests/xmlrpc/wp/editPost.php index dacd64573a6d5..1c47e5426a067 100644 --- a/tests/phpunit/tests/xmlrpc/wp/editPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/editPost.php @@ -528,4 +528,62 @@ public function test_draft_not_assigned_published_date() { $after = get_post( $post_id ); $this->assertSame( '0000-00-00 00:00:00', $after->post_date_gmt ); } + + /** + * @ticket 66107 + */ + public function test_string_post_date_is_accepted(): void { + $editor_id = $this->make_user_by_role( 'editor' ); + $post_id = self::factory()->post->create( array( 'post_author' => $editor_id ) ); + + $date_string = '1984-01-11 05:00:00'; + $result = $this->myxmlrpcserver->wp_editPost( array( 1, 'editor', 'editor', $post_id, array( 'post_date' => $date_string ) ) ); + $this->assertNotIXRError( $result ); + $this->assertTrue( $result ); + $this->assertSame( $date_string, get_post( $post_id )->post_date ); + } + + /** + * @ticket 66107 + */ + public function test_string_post_date_gmt_is_accepted(): void { + $editor_id = $this->make_user_by_role( 'editor' ); + $post_id = self::factory()->post->create( array( 'post_author' => $editor_id ) ); + + $date_string = '1984-01-11 05:00:00'; + $result = $this->myxmlrpcserver->wp_editPost( array( 1, 'editor', 'editor', $post_id, array( 'post_date_gmt' => $date_string ) ) ); + $this->assertNotIXRError( $result ); + $this->assertTrue( $result ); + $this->assertSame( $date_string, get_post( $post_id )->post_date_gmt ); + } + + /** + * @ticket 66107 + */ + public function test_string_if_not_modified_since_is_accepted(): void { + $editor_id = $this->make_user_by_role( 'editor' ); + $post_id = self::factory()->post->create( array( 'post_author' => $editor_id ) ); + + $struct = array( + 'post_title' => 'Updated', + 'if_not_modified_since' => gmdate( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), + ); + $result = $this->myxmlrpcserver->wp_editPost( array( 1, 'editor', 'editor', $post_id, $struct ) ); + $this->assertNotIXRError( $result ); + $this->assertTrue( $result ); + $this->assertSame( 'Updated', get_post( $post_id )->post_title ); + } + + /** + * @ticket 66107 + */ + public function test_non_date_post_date_returns_error(): void { + $editor_id = $this->make_user_by_role( 'editor' ); + $post_id = self::factory()->post->create( array( 'post_author' => $editor_id ) ); + + $struct = array( 'post_date' => array( '1984-01-11 05:00:00' ) ); + $result = $this->myxmlrpcserver->wp_editPost( array( 1, 'editor', 'editor', $post_id, $struct ) ); + $this->assertIXRError( $result ); + $this->assertSame( 400, $result->code ); + } } diff --git a/tests/phpunit/tests/xmlrpc/wp/newPost.php b/tests/phpunit/tests/xmlrpc/wp/newPost.php index c6386a1f23d5b..e68d07371f51d 100644 --- a/tests/phpunit/tests/xmlrpc/wp/newPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/newPost.php @@ -446,4 +446,19 @@ public function test_valid_IXR_post_date_gmt() { $this->assertStringMatchesFormat( '%d', $result ); $this->assertSame( $date_string, $fetched_post->post_date_gmt ); } + + /** + * @ticket 66107 + */ + public function test_non_date_post_date_returns_error(): void { + $this->make_user_by_role( 'author' ); + + $post = array( + 'post_title' => 'test', + 'post_date' => array( '1984-01-11 05:00:00' ), + ); + $result = $this->myxmlrpcserver->wp_newPost( array( 1, 'author', 'author', $post ) ); + $this->assertIXRError( $result ); + $this->assertSame( 400, $result->code ); + } } From 48b79958b64f1020d09837488d4b8fe87c0546e0 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Thu, 17 Sep 2026 09:38:53 -0600 Subject: [PATCH 2/4] _is_content_struct_array() helper Check to make sure we got a struct in the places they are expected --- src/wp-includes/class-wp-xmlrpc-server.php | 44 +++++++++++++++++++ tests/phpunit/tests/xmlrpc/mw/editPost.php | 12 +++++ tests/phpunit/tests/xmlrpc/mw/newPost.php | 11 +++++ tests/phpunit/tests/xmlrpc/wp/editComment.php | 12 +++++ tests/phpunit/tests/xmlrpc/wp/editPost.php | 12 +++++ tests/phpunit/tests/xmlrpc/wp/newPost.php | 11 +++++ 6 files changed, 102 insertions(+) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 198ac341ccd73..6dfc930f64d99 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -827,6 +827,25 @@ protected function _is_fields_array( $fields ): bool { return true; } + /** + * Checks that the content struct argument received from a client is an array. + * + * @since 7.2.0 + * + * @param mixed $content_struct The content struct argument to check. + * @return bool True if `$content_struct` is an array, false otherwise. + * + * @phpstan-assert-if-true array $content_struct + */ + protected function _is_content_struct_array( $content_struct ): bool { + if ( ! is_array( $content_struct ) ) { + $this->error = new IXR_Error( 400, __( 'The content struct argument must be an array.' ) ); + return false; + } + + return true; + } + /** * Prepares taxonomy data for return in an XML-RPC object. * @@ -1329,6 +1348,7 @@ protected function _prepare_user( $user, $fields ) { * Creates a new post for any registered post type. * * @since 3.4.0 + * @since 7.2.0 Returns an error if the content struct argument is not an array. * * @link https://en.wikipedia.org/wiki/RSS_enclosure for information on RSS enclosures. * @@ -1383,6 +1403,10 @@ public function wp_newPost( $args ) { $password = $args[2]; $content_struct = $args[3]; + if ( ! $this->_is_content_struct_array( $content_struct ) ) { + return $this->error; + } + $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; @@ -1787,6 +1811,7 @@ static function ( $value ) { * should be changed. All other fields will retain their existing values. * * @since 3.4.0 + * @since 7.2.0 Returns an error if the content struct argument is not an array. * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. @@ -1811,6 +1836,10 @@ public function wp_editPost( $args ) { $post_id = (int) $args[3]; $content_struct = $args[4]; + if ( ! $this->_is_content_struct_array( $content_struct ) ) { + return $this->error; + } + $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; @@ -3900,6 +3929,7 @@ public function wp_deleteComment( $args ) { * - 'status'. Common statuses are 'approve', 'hold', 'spam'. See get_comment_statuses() for more details. * * @since 2.7.0 + * @since 7.2.0 Returns an error if the content struct argument is not an array. * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. @@ -3920,6 +3950,10 @@ public function wp_editComment( $args ) { $comment_id = (int) $args[3]; $content_struct = $args[4]; + if ( ! $this->_is_content_struct_array( $content_struct ) ) { + return $this->error; + } + $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; @@ -5482,6 +5516,7 @@ public function blogger_deletePost( $args ) { * - wp_post_thumbnail * * @since 1.5.0 + * @since 7.2.0 Returns an error if the content struct argument is not an array. * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. @@ -5502,6 +5537,10 @@ public function mw_newPost( $args ) { $content_struct = $args[3]; $publish = $args[4] ?? 0; + if ( ! $this->_is_content_struct_array( $content_struct ) ) { + return $this->error; + } + $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; @@ -5887,6 +5926,7 @@ public function attach_uploads( $post_id, $post_content ) { * Edits a post. * * @since 1.5.0 + * @since 7.2.0 Returns an error if the content struct argument is not an array. * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. @@ -5908,6 +5948,10 @@ public function mw_editPost( $args ) { $content_struct = $args[3]; $publish = $args[4] ?? 0; + if ( ! $this->_is_content_struct_array( $content_struct ) ) { + return $this->error; + } + $user = $this->login( $username, $password ); if ( ! $user ) { return $this->error; diff --git a/tests/phpunit/tests/xmlrpc/mw/editPost.php b/tests/phpunit/tests/xmlrpc/mw/editPost.php index c0efc1ddc0ab7..4e9a264f6128b 100644 --- a/tests/phpunit/tests/xmlrpc/mw/editPost.php +++ b/tests/phpunit/tests/xmlrpc/mw/editPost.php @@ -349,4 +349,16 @@ public function test_string_date_created_is_accepted(): void { $this->assertTrue( $result ); $this->assertSame( $date_string, get_post( $post_id )->post_date ); } + + /** + * @ticket 66107 + */ + public function test_non_array_content_struct_returns_error(): void { + $editor_id = $this->make_user_by_role( 'editor' ); + $post_id = self::factory()->post->create( array( 'post_author' => $editor_id ) ); + + $result = $this->myxmlrpcserver->mw_editPost( array( $post_id, 'editor', 'editor', 'not a struct' ) ); + $this->assertIXRError( $result ); + $this->assertSame( 400, $result->code ); + } } diff --git a/tests/phpunit/tests/xmlrpc/mw/newPost.php b/tests/phpunit/tests/xmlrpc/mw/newPost.php index 2385f40957374..924bec51d0e96 100644 --- a/tests/phpunit/tests/xmlrpc/mw/newPost.php +++ b/tests/phpunit/tests/xmlrpc/mw/newPost.php @@ -217,4 +217,15 @@ public function test_string_date_created_is_accepted(): void { $this->assertNotIXRError( $result ); $this->assertSame( $date_string, get_post( $result )->post_date ); } + + /** + * @ticket 66107 + */ + public function test_non_array_content_struct_returns_error(): void { + $this->make_user_by_role( 'author' ); + + $result = $this->myxmlrpcserver->mw_newPost( array( 1, 'author', 'author', 'not a struct' ) ); + $this->assertIXRError( $result ); + $this->assertSame( 400, $result->code ); + } } diff --git a/tests/phpunit/tests/xmlrpc/wp/editComment.php b/tests/phpunit/tests/xmlrpc/wp/editComment.php index 719af7b7a0823..d355e85ef2a30 100644 --- a/tests/phpunit/tests/xmlrpc/wp/editComment.php +++ b/tests/phpunit/tests/xmlrpc/wp/editComment.php @@ -110,4 +110,16 @@ public function test_string_date_created_gmt_is_accepted(): void { $this->assertTrue( $result ); $this->assertSame( $date_string, get_comment( $comment_id )->comment_date_gmt ); } + + /** + * @ticket 66107 + */ + public function test_non_array_content_struct_returns_error(): void { + $this->make_user_by_role( 'administrator' ); + $comment_id = self::factory()->comment->create(); + + $result = $this->myxmlrpcserver->wp_editComment( array( 1, 'administrator', 'administrator', $comment_id, 'not a struct' ) ); + $this->assertIXRError( $result ); + $this->assertSame( 400, $result->code ); + } } diff --git a/tests/phpunit/tests/xmlrpc/wp/editPost.php b/tests/phpunit/tests/xmlrpc/wp/editPost.php index 1c47e5426a067..84784a68e8873 100644 --- a/tests/phpunit/tests/xmlrpc/wp/editPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/editPost.php @@ -586,4 +586,16 @@ public function test_non_date_post_date_returns_error(): void { $this->assertIXRError( $result ); $this->assertSame( 400, $result->code ); } + + /** + * @ticket 66107 + */ + public function test_non_array_content_struct_returns_error(): void { + $editor_id = $this->make_user_by_role( 'editor' ); + $post_id = self::factory()->post->create( array( 'post_author' => $editor_id ) ); + + $result = $this->myxmlrpcserver->wp_editPost( array( 1, 'editor', 'editor', $post_id, 'not a struct' ) ); + $this->assertIXRError( $result ); + $this->assertSame( 400, $result->code ); + } } diff --git a/tests/phpunit/tests/xmlrpc/wp/newPost.php b/tests/phpunit/tests/xmlrpc/wp/newPost.php index e68d07371f51d..28053415915aa 100644 --- a/tests/phpunit/tests/xmlrpc/wp/newPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/newPost.php @@ -461,4 +461,15 @@ public function test_non_date_post_date_returns_error(): void { $this->assertIXRError( $result ); $this->assertSame( 400, $result->code ); } + + /** + * @ticket 66107 + */ + public function test_non_array_content_struct_returns_error(): void { + $this->make_user_by_role( 'author' ); + + $result = $this->myxmlrpcserver->wp_newPost( array( 1, 'author', 'author', 'not a struct' ) ); + $this->assertIXRError( $result ); + $this->assertSame( 400, $result->code ); + } } From 22c334c352361d537abded8c75205f37f76a1442 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Thu, 17 Sep 2026 09:59:53 -0600 Subject: [PATCH 3/4] Make sure $post['post_modified_gmt'] is a string --- src/wp-includes/class-wp-xmlrpc-server.php | 13 +++++++++--- tests/phpunit/tests/xmlrpc/wp/editPost.php | 24 ++++++++++++++++++++++ tests/phpunit/tests/xmlrpc/wp/getPost.php | 22 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 6dfc930f64d99..28b0f4ce17483 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -929,11 +929,13 @@ protected function _prepare_term( $term ) { /** * Converts a WordPress date string to an IXR_Date object. * + * @since 7.2.0 A value that is not a string is treated as an empty date. + * * @param string $date Date string to convert. * @return IXR_Date IXR_Date object. */ protected function _convert_date( $date ) { - if ( '0000-00-00 00:00:00' === $date ) { + if ( ! is_string( $date ) || '0000-00-00 00:00:00' === $date ) { return new IXR_Date( '00000000T00:00:00Z' ); } return new IXR_Date( mysql2date( 'Ymd\TH:i:s', $date, false ) ); @@ -947,7 +949,7 @@ protected function _convert_date( $date ) { * @return IXR_Date IXR_Date object. */ protected function _convert_date_gmt( $date_gmt, $date ) { - if ( '0000-00-00 00:00:00' !== $date && '0000-00-00 00:00:00' === $date_gmt ) { + if ( is_string( $date ) && '0000-00-00 00:00:00' !== $date && '0000-00-00 00:00:00' === $date_gmt ) { return new IXR_Date( get_gmt_from_date( mysql2date( 'Y-m-d H:i:s', $date, false ), 'Ymd\TH:i:s' ) ); } return $this->_convert_date( $date_gmt ); @@ -1860,8 +1862,13 @@ public function wp_editPost( $args ) { return $if_not_modified_since; } + $post_modified_timestamp = false; + if ( is_string( $post['post_modified_gmt'] ) ) { + $post_modified_timestamp = mysql2date( 'U', $post['post_modified_gmt'] ); + } + // If the post has been modified since the date provided, return an error. - if ( mysql2date( 'U', $post['post_modified_gmt'] ) > $if_not_modified_since->getTimestamp() ) { + if ( false !== $post_modified_timestamp && $post_modified_timestamp > $if_not_modified_since->getTimestamp() ) { return new IXR_Error( 409, __( 'There is a revision of this post that is more recent.' ) ); } } diff --git a/tests/phpunit/tests/xmlrpc/wp/editPost.php b/tests/phpunit/tests/xmlrpc/wp/editPost.php index 84784a68e8873..85fbd7a8033e5 100644 --- a/tests/phpunit/tests/xmlrpc/wp/editPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/editPost.php @@ -598,4 +598,28 @@ public function test_non_array_content_struct_returns_error(): void { $this->assertIXRError( $result ); $this->assertSame( 400, $result->code ); } + + /** + * Ensure a stored modified date that is not a string skips the `if_not_modified_since` check + * instead of causing a fatal error. + * + * @ticket 66107 + */ + public function test_non_string_stored_modified_date_skips_if_not_modified_since(): void { + $editor_id = $this->make_user_by_role( 'editor' ); + $post_id = self::factory()->post->create( array( 'post_author' => $editor_id ) ); + + $cached_post = (object) get_object_vars( get_post( $post_id ) ); + $cached_post->post_modified_gmt = array( 'not a date' ); + wp_cache_set( $post_id, $cached_post, 'posts' ); + + $struct = array( + 'post_title' => 'Updated', + 'if_not_modified_since' => new IXR_Date( time() ), + ); + $result = $this->myxmlrpcserver->wp_editPost( array( 1, 'editor', 'editor', $post_id, $struct ) ); + + $this->assertTrue( $result ); + $this->assertSame( 'Updated', get_post( $post_id )->post_title ); + } } diff --git a/tests/phpunit/tests/xmlrpc/wp/getPost.php b/tests/phpunit/tests/xmlrpc/wp/getPost.php index 8f2e28b5fd6ae..050e45b58ab96 100644 --- a/tests/phpunit/tests/xmlrpc/wp/getPost.php +++ b/tests/phpunit/tests/xmlrpc/wp/getPost.php @@ -159,4 +159,26 @@ public function test_non_array_fields_returns_error(): void { $this->assertIXRError( $result ); $this->assertSame( 400, $result->code ); } + + /** + * Ensure a stored date that is not a string is returned as an empty date instead of causing a fatal error. + * + * @ticket 66107 + */ + public function test_non_string_stored_date_returns_empty_date(): void { + /* + * A non-string date with an empty GMT date reaches both _convert_date() + * and the local date fallback in _convert_date_gmt(). + */ + $cached_post = (object) get_object_vars( get_post( $this->post_id ) ); + $cached_post->post_modified = array( 'not a date' ); + $cached_post->post_modified_gmt = '0000-00-00 00:00:00'; + wp_cache_set( $this->post_id, $cached_post, 'posts' ); + + $result = $this->myxmlrpcserver->wp_getPost( array( 1, 'author', 'author', $this->post_id, array( 'post' ) ) ); + + $this->assertNotIXRError( $result ); + $this->assertSame( '00000000T00:00:00Z', $result['post_modified']->getIso() ); + $this->assertSame( '00000000T00:00:00Z', $result['post_modified_gmt']->getIso() ); + } } From 95bf28aa1be7f86838dd7ed0e509e918bd4e7305 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Thu, 17 Sep 2026 12:18:59 -0600 Subject: [PATCH 4/4] Add @phpstan-return --- src/wp-includes/class-wp-xmlrpc-server.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 28b0f4ce17483..d4b93bd89c19b 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -966,6 +966,8 @@ protected function _convert_date_gmt( $date_gmt, $date ) { * * @param mixed $date Client-supplied date value. * @return IXR_Date|IXR_Error IXR_Date object on success, IXR_Error if the value is not a date. + * + * @phpstan-return ( $date is IXR_Date|string ? IXR_Date : IXR_Error ) */ protected function _convert_client_date( $date ) { if ( $date instanceof IXR_Date ) {