Index: branches/4.7/src/wp-includes/rest-api.php
===================================================================
--- branches/4.7/src/wp-includes/rest-api.php	(revision 40113)
+++ branches/4.7/src/wp-includes/rest-api.php	(revision 40114)
@@ -781,5 +781,5 @@
 
 /**
- * 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
@@ -787,11 +787,16 @@
  * @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 ) ) {
@@ -799,6 +804,15 @@
 	}
 
-	$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 );
Index: branches/4.7/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
===================================================================
--- branches/4.7/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php	(revision 40113)
+++ branches/4.7/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php	(revision 40114)
@@ -1000,4 +1000,5 @@
 			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'] ) ) {
@@ -1006,4 +1007,5 @@
 			if ( ! empty( $date_data ) ) {
 				list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
+				$prepared_post->edit_date = true;
 			}
 		}
Index: branches/4.7/tests/phpunit/tests/rest-api.php
===================================================================
--- branches/4.7/tests/phpunit/tests/rest-api.php	(revision 40113)
+++ branches/4.7/tests/phpunit/tests/rest-api.php	(revision 40114)
@@ -384,3 +384,66 @@
 	}
 
+	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 ) );
+	}
 }
Index: branches/4.7/tests/phpunit/tests/rest-api/rest-comments-controller.php
===================================================================
--- branches/4.7/tests/phpunit/tests/rest-api/rest-comments-controller.php	(revision 40113)
+++ branches/4.7/tests/phpunit/tests/rest-api/rest-comments-controller.php	(revision 40114)
@@ -959,4 +959,82 @@
 	}
 
+	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 );
@@ -1941,4 +2019,37 @@
 		$this->assertEquals( mysql_to_rfc3339( $updated->comment_date ), $comment['date'] );
 		$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 );
 	}
 
Index: branches/4.7/tests/phpunit/tests/rest-api/rest-posts-controller.php
===================================================================
--- branches/4.7/tests/phpunit/tests/rest-api/rest-posts-controller.php	(revision 40113)
+++ branches/4.7/tests/phpunit/tests/rest-api/rest-posts-controller.php	(revision 40114)
@@ -1097,4 +1097,108 @@
 	}
 
+	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
@@ -1928,4 +2032,38 @@
 		$this->assertEquals( date( 'Y-m-d', strtotime( mysql_to_rfc3339( $expected_modified ) ) ), date( 'Y-m-d', strtotime( $data['modified'] ) ) );
 		$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 );
 	}
 
