Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,28 @@ protected function get_post( $id ) {
return $post;
}

/**
* Adds a `Last-Modified` response header based on the post modified time (GMT). (#47676)
*
* Enables clients to send `If-Unmodified-Since` on subsequent write requests.
*
* @since 7.1.0
*
* @param WP_REST_Response $response Response object.
* @param WP_Post $post Post object.
*/
protected function add_last_modified_header( $response, $post ) {
$modified = get_post_datetime( $post, 'modified', 'gmt' );
if ( ! $modified ) {
return;
}

$response->header(
'Last-Modified',
gmdate( 'D, d M Y H:i:s', $modified->getTimestamp() ) . ' GMT'
);
}

/**
* Checks if a given request has access to read a post.
*
Expand Down Expand Up @@ -673,6 +695,8 @@ public function get_item( $request ) {
$response->link_header( 'alternate', get_permalink( $post->ID ), array( 'type' => 'text/html' ) );
}

$this->add_last_modified_header( $response, $post );

return $response;
}

Expand Down Expand Up @@ -930,6 +954,21 @@ public function update_item_permissions_check( $request ) {
);
}

$if_unmodified_since = $request->get_header( 'If-Unmodified-Since' );
if ( $if_unmodified_since ) {
$client_time = strtotime( $if_unmodified_since );
if ( false !== $client_time ) {
$modified = get_post_datetime( $post, 'modified', 'gmt' );
if ( $modified && $modified->getTimestamp() > $client_time ) {
return new WP_Error(
'rest_precondition_failed',
__( 'Sorry, the post has been modified on the server since you started editing it. Conflict resolution is required.' ),
array( 'status' => 412 )
);
}
}
}

return true;
}

Expand Down Expand Up @@ -1040,18 +1079,21 @@ public function update_item( $request ) {

// Filter is fired in WP_REST_Attachments_Controller subclass.
if ( 'attachment' === $this->post_type ) {
$response = $this->prepare_item_for_response( $post, $request );
return rest_ensure_response( $response );
$response = rest_ensure_response( $this->prepare_item_for_response( $post, $request ) );
$this->add_last_modified_header( $response, $post );
return $response;
}

/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );

wp_after_insert_post( $post, true, $post_before );

$response = $this->prepare_item_for_response( $post, $request );
$response = rest_ensure_response( $this->prepare_item_for_response( $post, $request ) );

return rest_ensure_response( $response );
$this->add_last_modified_header( $response, $post );

return $response;
}

/**
Expand Down
55 changes: 55 additions & 0 deletions tests/phpunit/tests/rest-api/rest-posts-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -4549,6 +4549,61 @@ public function test_update_item_with_same_template_that_no_longer_exists() {
$this->assertSame( 'post-my-invalid-template.php', $data['template'] );
}

/**
* Tests If-Unmodified-Since conditional updates and Last-Modified headers.
*
* @covers WP_REST_Posts_Controller::update_item_permissions_check
* @covers WP_REST_Posts_Controller::get_item
* @covers WP_REST_Posts_Controller::add_last_modified_header
* @ticket 47676
*/
public function test_update_item_with_if_unmodified_since_precondition() {
wp_set_current_user( self::$editor_id );

$get_request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
$get_request->set_param( 'context', 'edit' );
$get_response = rest_get_server()->dispatch( $get_request );
$this->assertSame( 200, $get_response->get_status() );
$headers = $get_response->get_headers();
$this->assertArrayHasKey( 'Last-Modified', $headers );
$last_modified = $headers['Last-Modified'];

$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
$request->add_header( 'content-type', 'application/json' );
$title1 = 'Same as last modified';
$request->set_body( wp_json_encode( $this->set_post_data( array( 'title' => $title1 ) ) ) );
$request->set_header( 'If-Unmodified-Since', $last_modified );
$response = rest_get_server()->dispatch( $request );
$this->assertSame( 200, $response->get_status(), 'Update should succeed when If-Unmodified-Since matches Last-Modified.' );
$new_data = $response->get_data();
$this->assertSame( $title1, $new_data['title']['raw'] );
$this->assertSame( $title1, get_post( self::$post_id )->post_title );

$get_response = rest_get_server()->dispatch( $get_request );
$headers = $get_response->get_headers();
$this->assertArrayHasKey( 'Last-Modified', $headers );
$last_modified_after_update = $headers['Last-Modified'];

$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
$request->add_header( 'content-type', 'application/json' );
$title2 = '1 second after last modified';
$request->set_body( wp_json_encode( $this->set_post_data( array( 'title' => $title2 ) ) ) );
$request->set_header( 'If-Unmodified-Since', gmdate( 'D, d M Y H:i:s', strtotime( $last_modified_after_update ) + 1 ) . ' GMT' );
$response = rest_get_server()->dispatch( $request );
$this->assertSame( 200, $response->get_status(), 'Update should succeed when If-Unmodified-Since is after the server modified time.' );
$new_data = $response->get_data();
$this->assertSame( $title2, $new_data['title']['raw'] );

$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
$request->add_header( 'content-type', 'application/json' );
$title3 = 'Should not save';
$request->set_body( wp_json_encode( $this->set_post_data( array( 'title' => $title3 ) ) ) );
$request->set_header( 'If-Unmodified-Since', $last_modified );
$response = rest_get_server()->dispatch( $request );
$this->assertSame( 412, $response->get_status(), 'Update should fail when If-Unmodified-Since predates current revision.' );
$this->assertSame( $title2, get_post( self::$post_id )->post_title, 'Expected title not to update due to failed precondition.' );
}

public function verify_post_roundtrip( $input = array(), $expected_output = array() ) {
// Create the post.
$request = new WP_REST_Request( 'POST', '/wp/v2/posts' );
Expand Down
Loading