Skip to content

XML-RPC: Convert client-supplied dates before calling IXR_Date methods. - #13517

Open
josephscott wants to merge 18 commits into
WordPress:trunkfrom
josephscott:xmlrpc/client-date-normalization
Open

josephscott wants to merge 18 commits into
WordPress:trunkfrom
josephscott:xmlrpc/client-date-normalization

Conversation

@josephscott

Copy link
Copy Markdown
Contributor

https://core.trac.wordpress.org/ticket/66107

AI assistance: Yes
Tool(s): Claude Code
Model(s): Fable 5.1
Used for: Research and experimentation. Guided directions on writing the code changes. It wrote the tests.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

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 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props josephscott, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@westonruter westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What may make sense is to start adopting JSON Schema for the XML-RPC methods. It could re-use REST API functions to validate and sanitize the incoming data. This would eliminate a lot of ad hoc type checking. Maybe this is out of scope here.

Comment thread src/wp-includes/class-wp-xmlrpc-server.php
Comment thread src/wp-includes/class-wp-xmlrpc-server.php Outdated
Check to make sure we got a struct in the places they are expected
@josephscott

Copy link
Copy Markdown
Contributor Author

I haven't looked at what would be involved in re-using the REST API functions to validate and sanitize the incoming data. Not it scope for this ticket, but it could be an interesting idea to explore.

Comment thread src/wp-includes/class-wp-xmlrpc-server.php
Comment thread src/wp-includes/class-wp-xmlrpc-server.php Outdated
Comment thread src/wp-includes/class-wp-xmlrpc-server.php Outdated
Comment thread src/wp-includes/class-wp-xmlrpc-server.php Outdated
Comment thread src/wp-includes/class-wp-xmlrpc-server.php
Comment thread src/wp-includes/class-wp-xmlrpc-server.php
Comment thread src/wp-includes/class-wp-xmlrpc-server.php
Comment thread src/wp-includes/class-wp-xmlrpc-server.php
Comment thread src/wp-includes/class-wp-xmlrpc-server.php Outdated
Comment thread src/wp-includes/class-wp-xmlrpc-server.php

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Invalid date strings can still propagate false from mysql2date() into IXR_Date::parseIso() and trigger PHP fatals, so the new conversion path needs additional validation/guarding.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

This PR hardens XML-RPC date handling by converting client-supplied date values before calling IXR_Date methods, and by validating that “content struct” arguments are associative arrays to avoid fatals on unexpected input.

Changes:

  • Add _convert_client_date() and use it in post/comment insert/edit paths to normalize IXR_Date vs string dates (and reject non-date types).
  • Add _is_content_struct_array() and use it across XML-RPC endpoints to reject non-associative/non-array content structs with a 400 error.
  • Expand PHPUnit coverage for invalid structs and non-string cached date values, plus add IXR_Date return-type annotations.
File Description
tests/​phpunit/​tests/​xmlrpc/​wp/​newPost.php Adds regression tests for invalid post_date types and non-array content structs in wp.newPost.
tests/​phpunit/​tests/​xmlrpc/​wp/​getPost.php Adds test ensuring non-string cached dates return empty IXR dates instead of fatals.
tests/​phpunit/​tests/​xmlrpc/​wp/​editPost.php Adds coverage for string date acceptance, struct validation, and cached non-string modified dates.
tests/​phpunit/​tests/​xmlrpc/​wp/​editComment.php Adds coverage for string date_created_gmt acceptance and struct validation.
tests/​phpunit/​tests/​xmlrpc/​mw/​newPost.php Adds coverage for string dateCreated acceptance and struct validation in mw.newPost.
tests/​phpunit/​tests/​xmlrpc/​mw/​editPost.php Adds coverage for string dateCreated acceptance and struct validation in mw.editPost.
src/​wp-includes/​IXR/​class-IXR-date.php Adds return types/docblocks for IXR_Date::getIso() and ::getXml().
src/​wp-includes/​class-wp-xmlrpc-server.php Introduces _is_content_struct_array() and _convert_client_date(), and updates date conversion behavior in multiple XML-RPC methods.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/wp-includes/class-wp-xmlrpc-server.php Outdated
@josephscott

Copy link
Copy Markdown
Contributor Author

I think fixing mysql2date() should be a separate PR.

josephscott and others added 4 commits September 22, 2026 16:47
A subclass overriding either method without a return type would fatal
on class load with an incompatible declaration error. The phpdoc return
types are retained for static analysis.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@westonruter westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Comment by Claude Opus 5.5

Review findings on the current head (f3f15b8):

  1. wp.newPost now rejects a non-string post_date_gmt even when post_date is supplied (inline below).
  2. An unparseable date string yields an empty IXR_Date rather than an error (inline below). This is being punted to a follow-up ticket, as noted in the Copilot thread, but it is flagged here since the consequence on edit methods is more significant than a malformed response.
  3. Native : string return types on IXR_Date::getIso() and getXml() would fatal on class load for any subclass that overrides either method without a return type. This has been addressed in f3f15b8, which removes the native return types while keeping the @return / @phpstan-return tags for static analysis.

Comment thread src/wp-includes/class-wp-xmlrpc-server.php Outdated
Comment thread src/wp-includes/class-wp-xmlrpc-server.php Outdated

@westonruter westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Comment by Claude Opus 5.5

Follow-up review on b352645. The two findings from the previous review are resolved by 0cad4e3 (a non-date post_date_gmt is again ignored when post_date is supplied) and b352645 (unparseable date strings are rejected).

Three further findings, all low severity. None is a regression relative to trunk: in each case the input either already behaved this way or previously caused a fatal error.

  1. A malformed dateTime.iso8601 value bypasses the new validation (inline below).
  2. An explicit timezone offset in a string date is dropped (inline below). This likely belongs in the follow-up ticket along with the other date-parsing policy questions.
  3. A zero post_date_gmt string on wp.editPost results in an invalid_date error (inline below), whereas wp.newPost treats it as empty.

* @phpstan-return ( $date is IXR_Date|string ? IXR_Date : IXR_Error )
*/
protected function _convert_client_date( $date ) {
if ( $date instanceof IXR_Date ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Comment by Claude Opus 5.5

The new format check only applies to strings. An IXR_Date is returned as-is, but IXR parses any <dateTime.iso8601> contents into one, so <dateTime.iso8601>garbage</dateTime.iso8601> yields a junk getIso(). In _insert_post(), iso8601_to_datetime() then returns false while edit_date is true, so an edited post's date is silently reset to the current time — the same outcome the string check now prevents.

This is pre-existing on trunk, but since the check now exists, it could apply to both input types for consistency, for example:

if ( is_string( $date ) ) {
	$date = $this->_convert_date( $date );
}

if ( ! ( $date instanceof IXR_Date ) ) {
	return new IXR_Error( 400, __( 'Dates must be a dateTime.iso8601 value or a string.' ) );
}

// A value that could not be parsed as a date produces an IXR_Date with empty or malformed components.
if ( ! preg_match( '/^\d{8}T\d{2}:\d{2}:\d{2}/', $date->getIso() ) ) {
	return new IXR_Error( 400, __( 'Invalid date.' ) );
}

return $date;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to reject any client date that cannot be parsed

}

if ( is_string( $date ) ) {
$date = $this->_convert_date( $date );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Comment by Claude Opus 5.5

_convert_date() formats with mysql2date( 'Ymd\TH:i:s', … ), which keeps the wall-clock time but discards any offset in the input. For example, 2020-01-01T05:00:00+02:00 becomes 20200101T05:00:00, whereas the correct UTC time is 03:00. For post_date_gmt / date_created_gmt the Z is then appended, storing 05:00 GMT; a local post_date / dateCreated of …05:00:00Z is likewise stored as 05:00 in the site timezone.

This was already the behavior for wp.newPost on trunk, but wp.editPost, metaWeblog.newPost, metaWeblog.editPost, and wp.editComment now accept strings (previously a fatal error), so they now store a shifted date rather than failing. _convert_date() was designed for WordPress's own offset-less MySQL-format dates, so deciding how offsets should be interpreted for local vs. GMT fields seems suited to the follow-up ticket.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to honor timezone offsets in client date strings

@@ -1577,10 +1639,20 @@ protected function _insert_post( $user, $content_struct ) {

// Do some timestamp voodoo.
if ( ! empty( $post_data['post_date_gmt'] ) ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Comment by Claude Opus 5.5

wp.newPost unsets a post_date_gmt of '0000-00-00 00:00:00' as "no GMT date", but wp.editPost passes the struct straight to _insert_post(). There the zero string is non-empty, _convert_client_date() returns IXR_Date( '00000000T00:00:00Z' ) (which passes the format check), and iso8601_to_datetime() produces -0001-11-30 00:00:00 with edit_date = true, so wp_update_post() fails with invalid_date.

On trunk this input was a fatal error, so an error is an improvement, but a client sending the zero GMT date to keep a draft's date unset may reasonably expect the same "empty" handling as wp.newPost. Treating '0000-00-00 00:00:00' as empty here (before the ! empty() checks) would make the two consistent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update to treat an empty date as no date in _insert_post()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants