From 83d27ba447046bfd2067ac423b4b4e4957cd4f7b Mon Sep 17 00:00:00 2001 From: James Nylen Date: Tue, 21 Feb 2017 18:17:32 +0000 Subject: [PATCH] REST API: Fix multiple issues with setting dates of posts and comments. This commit modifies the `rest_get_date_with_gmt` function to correctly parse local and UTC timestamps with or without timezone information. It also ensures that the REST API can edit the dates of draft posts by setting the `edit_date` flag to `wp_update_post`. Overall this commit ensures that post and comment dates can be set and updated as expected. Fixes #39256. git-svn-id: https://develop.svn.wordpress.org/trunk@40101 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/rest-api.php | 28 +++- .../class-wp-rest-posts-controller.php | 2 + tests/phpunit/tests/rest-api.php | 63 ++++++++ .../rest-api/rest-comments-controller.php | 111 ++++++++++++++ .../tests/rest-api/rest-posts-controller.php | 138 ++++++++++++++++++ 5 files changed, 335 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index d5d76386cd..e6cdc3a959 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -780,26 +780,40 @@ function rest_parse_date( $date, $force_utc = false ) { } /** - * Retrieves a local date with its GMT equivalent, in MySQL datetime format. + * Parses a date into both its local and UTC equivalent, in MySQL datetime format. * * @since 4.4.0 * * @see rest_parse_date() * - * @param string $date RFC3339 timestamp. - * @param bool $force_utc Whether a UTC timestamp should be forced. Default false. + * @param string $date RFC3339 timestamp. + * @param bool $is_utc Whether the provided date should be interpreted as UTC. Default false. * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s), * null on failure. */ -function rest_get_date_with_gmt( $date, $force_utc = false ) { - $date = rest_parse_date( $date, $force_utc ); +function rest_get_date_with_gmt( $date, $is_utc = false ) { + // Whether or not the original date actually has a timezone string + // changes the way we need to do timezone conversion. Store this info + // before parsing the date, and use it later. + $has_timezone = preg_match( '#(Z|[+-]\d{2}(:\d{2})?)$#', $date ); + + $date = rest_parse_date( $date ); if ( empty( $date ) ) { return null; } - $utc = date( 'Y-m-d H:i:s', $date ); - $local = get_date_from_gmt( $utc ); + // At this point $date could either be a local date (if we were passed a + // *local* date without a timezone offset) or a UTC date (otherwise). + // Timezone conversion needs to be handled differently between these two + // cases. + if ( ! $is_utc && ! $has_timezone ) { + $local = date( 'Y-m-d H:i:s', $date ); + $utc = get_gmt_from_date( $local ); + } else { + $utc = date( 'Y-m-d H:i:s', $date ); + $local = get_date_from_gmt( $utc ); + } return array( $local, $utc ); } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 16765a5a5f..3648d9fa07 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -1004,12 +1004,14 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { if ( ! empty( $date_data ) ) { list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data; + $prepared_post->edit_date = true; } } elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) { $date_data = rest_get_date_with_gmt( $request['date_gmt'], true ); if ( ! empty( $date_data ) ) { list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data; + $prepared_post->edit_date = true; } } diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php index 69bf9a341d..f303a0130b 100644 --- a/tests/phpunit/tests/rest-api.php +++ b/tests/phpunit/tests/rest-api.php @@ -383,4 +383,67 @@ class Tests_REST_API extends WP_UnitTestCase { $this->assertEquals( $valid, wp_check_jsonp_callback( $callback ) ); } + public function rest_date_provider() { + return array( + // Valid dates with timezones + array( '2017-01-16T11:30:00-05:00', gmmktime( 11, 30, 0, 1, 16, 2017 ) + 5 * HOUR_IN_SECONDS ), + array( '2017-01-16T11:30:00-05:30', gmmktime( 11, 30, 0, 1, 16, 2017 ) + 5.5 * HOUR_IN_SECONDS ), + array( '2017-01-16T11:30:00-05' , gmmktime( 11, 30, 0, 1, 16, 2017 ) + 5 * HOUR_IN_SECONDS ), + array( '2017-01-16T11:30:00+05' , gmmktime( 11, 30, 0, 1, 16, 2017 ) - 5 * HOUR_IN_SECONDS ), + array( '2017-01-16T11:30:00-00' , gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + array( '2017-01-16T11:30:00+00' , gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + array( '2017-01-16T11:30:00Z' , gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + + // Valid dates without timezones + array( '2017-01-16T11:30:00' , gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + + // Invalid dates (TODO: support parsing partial dates as ranges, see #38641) + array( '2017-01-16T11:30:00-5', false ), + array( '2017-01-16T11:30', false ), + array( '2017-01-16T11', false ), + array( '2017-01-16T', false ), + array( '2017-01-16', false ), + array( '2017-01', false ), + array( '2017', false ), + ); + } + + /** + * @dataProvider rest_date_provider + */ + public function test_rest_parse_date( $string, $value ) { + $this->assertEquals( $value, rest_parse_date( $string ) ); + } + + public function rest_date_force_utc_provider() { + return array( + // Valid dates with timezones + array( '2017-01-16T11:30:00-05:00', gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + array( '2017-01-16T11:30:00-05:30', gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + array( '2017-01-16T11:30:00-05' , gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + array( '2017-01-16T11:30:00+05' , gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + array( '2017-01-16T11:30:00-00' , gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + array( '2017-01-16T11:30:00+00' , gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + array( '2017-01-16T11:30:00Z' , gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + + // Valid dates without timezones + array( '2017-01-16T11:30:00' , gmmktime( 11, 30, 0, 1, 16, 2017 ) ), + + // Invalid dates (TODO: support parsing partial dates as ranges, see #38641) + array( '2017-01-16T11:30:00-5', false ), + array( '2017-01-16T11:30', false ), + array( '2017-01-16T11', false ), + array( '2017-01-16T', false ), + array( '2017-01-16', false ), + array( '2017-01', false ), + array( '2017', false ), + ); + } + + /** + * @dataProvider rest_date_force_utc_provider + */ + public function test_rest_parse_date_force_utc( $string, $value ) { + $this->assertEquals( $value, rest_parse_date( $string, true ) ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-comments-controller.php b/tests/phpunit/tests/rest-api/rest-comments-controller.php index fea7878b43..502a6d2c5b 100644 --- a/tests/phpunit/tests/rest-api/rest-comments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-comments-controller.php @@ -960,6 +960,84 @@ class WP_Test_REST_Comments_Controller extends WP_Test_REST_Controller_Testcase $this->assertEquals( self::$post_id, $data['post'] ); } + public function comment_dates_provider() { + return array( + 'set date without timezone' => array( + 'params' => array( + 'timezone_string' => 'America/New_York', + 'date' => '2016-12-12T14:00:00', + ), + 'results' => array( + 'date' => '2016-12-12T14:00:00', + 'date_gmt' => '2016-12-12T19:00:00', + ), + ), + 'set date_gmt without timezone' => array( + 'params' => array( + 'timezone_string' => 'America/New_York', + 'date_gmt' => '2016-12-12T19:00:00', + ), + 'results' => array( + 'date' => '2016-12-12T14:00:00', + 'date_gmt' => '2016-12-12T19:00:00', + ), + ), + 'set date with timezone' => array( + 'params' => array( + 'timezone_string' => 'America/New_York', + 'date' => '2016-12-12T18:00:00-01:00', + ), + 'results' => array( + 'date' => '2016-12-12T14:00:00', + 'date_gmt' => '2016-12-12T19:00:00', + ), + ), + 'set date_gmt with timezone' => array( + 'params' => array( + 'timezone_string' => 'America/New_York', + 'date_gmt' => '2016-12-12T18:00:00-01:00', + ), + 'results' => array( + 'date' => '2016-12-12T14:00:00', + 'date_gmt' => '2016-12-12T19:00:00', + ), + ), + ); + } + + /** + * @dataProvider comment_dates_provider + */ + public function test_create_comment_date( $params, $results ) { + wp_set_current_user( self::$admin_id ); + update_option( 'timezone_string', $params['timezone_string'] ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->set_param( 'content', 'not empty' ); + $request->set_param( 'post', self::$post_id ); + if ( isset( $params['date'] ) ) { + $request->set_param( 'date', $params['date'] ); + } + if ( isset( $params['date_gmt'] ) ) { + $request->set_param( 'date_gmt', $params['date_gmt'] ); + } + $response = $this->server->dispatch( $request ); + + update_option( 'timezone_string', '' ); + + $this->assertEquals( 201, $response->get_status() ); + $data = $response->get_data(); + $comment = get_comment( $data['id'] ); + + $this->assertEquals( $results['date'], $data['date'] ); + $comment_date = str_replace( 'T', ' ', $results['date'] ); + $this->assertEquals( $comment_date, $comment->comment_date ); + + $this->assertEquals( $results['date_gmt'], $data['date_gmt'] ); + $comment_date_gmt = str_replace( 'T', ' ', $results['date_gmt'] ); + $this->assertEquals( $comment_date_gmt, $comment->comment_date_gmt ); + } + public function test_create_item_using_accepted_content_raw_value() { wp_set_current_user( self::$admin_id ); @@ -1970,6 +2048,39 @@ class WP_Test_REST_Comments_Controller extends WP_Test_REST_Controller_Testcase $this->assertEquals( '2014-11-07T10:14:25', $comment['date'] ); } + /** + * @dataProvider comment_dates_provider + */ + public function test_update_comment_date( $params, $results ) { + wp_set_current_user( self::$editor_id ); + update_option( 'timezone_string', $params['timezone_string'] ); + + $comment_id = $this->factory->comment->create(); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) ); + if ( isset( $params['date'] ) ) { + $request->set_param( 'date', $params['date'] ); + } + if ( isset( $params['date_gmt'] ) ) { + $request->set_param( 'date_gmt', $params['date_gmt'] ); + } + $response = $this->server->dispatch( $request ); + + update_option( 'timezone_string', '' ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $comment = get_comment( $data['id'] ); + + $this->assertEquals( $results['date'], $data['date'] ); + $comment_date = str_replace( 'T', ' ', $results['date'] ); + $this->assertEquals( $comment_date, $comment->comment_date ); + + $this->assertEquals( $results['date_gmt'], $data['date_gmt'] ); + $comment_date_gmt = str_replace( 'T', ' ', $results['date_gmt'] ); + $this->assertEquals( $comment_date_gmt, $comment->comment_date_gmt ); + } + public function test_update_item_no_content() { $post_id = $this->factory->post->create(); diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index ca163561ba..6943329577 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -1152,6 +1152,110 @@ class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Te $this->check_create_post_response( $response ); } + public function post_dates_provider() { + $all_statuses = array( + 'draft', + 'publish', + 'future', + 'pending', + 'private', + ); + + $cases_short = array( + 'set date without timezone' => array( + 'statuses' => $all_statuses, + 'params' => array( + 'timezone_string' => 'America/New_York', + 'date' => '2016-12-12T14:00:00', + ), + 'results' => array( + 'date' => '2016-12-12T14:00:00', + 'date_gmt' => '2016-12-12T19:00:00', + ), + ), + 'set date_gmt without timezone' => array( + 'statuses' => $all_statuses, + 'params' => array( + 'timezone_string' => 'America/New_York', + 'date_gmt' => '2016-12-12T19:00:00', + ), + 'results' => array( + 'date' => '2016-12-12T14:00:00', + 'date_gmt' => '2016-12-12T19:00:00', + ), + ), + 'set date with timezone' => array( + 'statuses' => array( 'draft', 'publish' ), + 'params' => array( + 'timezone_string' => 'America/New_York', + 'date' => '2016-12-12T18:00:00-01:00', + ), + 'results' => array( + 'date' => '2016-12-12T14:00:00', + 'date_gmt' => '2016-12-12T19:00:00', + ), + ), + 'set date_gmt with timezone' => array( + 'statuses' => array( 'draft', 'publish' ), + 'params' => array( + 'timezone_string' => 'America/New_York', + 'date_gmt' => '2016-12-12T18:00:00-01:00', + ), + 'results' => array( + 'date' => '2016-12-12T14:00:00', + 'date_gmt' => '2016-12-12T19:00:00', + ), + ), + ); + + $cases = array(); + foreach ( $cases_short as $description => $case ) { + foreach ( $case['statuses'] as $status ) { + $cases[ $description . ', status=' . $status ] = array( + $status, + $case['params'], + $case['results'], + ); + } + } + + return $cases; + } + + /** + * @dataProvider post_dates_provider + */ + public function test_create_post_date( $status, $params, $results ) { + wp_set_current_user( self::$editor_id ); + update_option( 'timezone_string', $params['timezone_string'] ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/posts' ); + $request->set_param( 'status', $status ); + $request->set_param( 'title', 'not empty' ); + if ( isset( $params['date'] ) ) { + $request->set_param( 'date', $params['date'] ); + } + if ( isset( $params['date_gmt'] ) ) { + $request->set_param( 'date_gmt', $params['date_gmt'] ); + } + $response = $this->server->dispatch( $request ); + + update_option( 'timezone_string', '' ); + + $this->assertEquals( 201, $response->get_status() ); + $data = $response->get_data(); + $post = get_post( $data['id'] ); + + $this->assertEquals( $results['date'], $data['date'] ); + $post_date = str_replace( 'T', ' ', $results['date'] ); + $this->assertEquals( $post_date, $post->post_date ); + + $this->assertEquals( $results['date_gmt'], $data['date_gmt'] ); + // TODO expect null here for drafts (see https://core.trac.wordpress.org/ticket/5698#comment:14) + $post_date_gmt = str_replace( 'T', ' ', $results['date_gmt'] ); + $this->assertEquals( $post_date_gmt, $post->post_date_gmt ); + } + /** * @ticket 38698 */ @@ -1985,6 +2089,40 @@ class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Te $this->assertEquals( date( 'Y-m-d', strtotime( $expected_modified ) ), date( 'Y-m-d', strtotime( $new_post->post_modified ) ) ); } + /** + * @dataProvider post_dates_provider + */ + public function test_update_post_date( $status, $params, $results ) { + wp_set_current_user( self::$editor_id ); + update_option( 'timezone_string', $params['timezone_string'] ); + + $post_id = $this->factory->post->create( array( 'post_status' => $status ) ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + if ( isset( $params['date'] ) ) { + $request->set_param( 'date', $params['date'] ); + } + if ( isset( $params['date_gmt'] ) ) { + $request->set_param( 'date_gmt', $params['date_gmt'] ); + } + $response = $this->server->dispatch( $request ); + + update_option( 'timezone_string', '' ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $post = get_post( $data['id'] ); + + $this->assertEquals( $results['date'], $data['date'] ); + $post_date = str_replace( 'T', ' ', $results['date'] ); + $this->assertEquals( $post_date, $post->post_date ); + + $this->assertEquals( $results['date_gmt'], $data['date_gmt'] ); + // TODO expect null here for drafts (see https://core.trac.wordpress.org/ticket/5698#comment:14) + $post_date_gmt = str_replace( 'T', ' ', $results['date_gmt'] ); + $this->assertEquals( $post_date_gmt, $post->post_date_gmt ); + } + public function test_update_post_with_invalid_date() { wp_set_current_user( self::$editor_id );