Skip to content

Commit 12e0ff5

Browse files
committed
REST API: Support If-Unmodified-Since for post updates and send Last-Modified
1 parent e12ddb3 commit 12e0ff5

2 files changed

Lines changed: 102 additions & 4 deletions

File tree

src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,29 @@ protected function get_post( $id ) {
569569
return $post;
570570
}
571571

572+
/**
573+
* Adds a `Last-Modified` response header based on the post modified time (GMT).
574+
*
575+
* Enables clients to send `If-Unmodified-Since` on subsequent write requests.
576+
*
577+
* @since 7.1.0
578+
*
579+
* @param WP_REST_Response $response Response object.
580+
* @param WP_Post $post Post object.
581+
*/
582+
protected function add_last_modified_header( $response, $post ) {
583+
$modified = get_post_datetime( $post, 'modified', 'gmt' );
584+
if ( ! $modified ) {
585+
return;
586+
}
587+
588+
$response->set_headers(
589+
array(
590+
'Last-Modified' => gmdate( 'D, d M Y H:i:s', $modified->getTimestamp() ) . ' GMT',
591+
)
592+
);
593+
}
594+
572595
/**
573596
* Checks if a given request has access to read a post.
574597
*
@@ -673,6 +696,8 @@ public function get_item( $request ) {
673696
$response->link_header( 'alternate', get_permalink( $post->ID ), array( 'type' => 'text/html' ) );
674697
}
675698

699+
$this->add_last_modified_header( $response, $post );
700+
676701
return $response;
677702
}
678703

@@ -930,6 +955,21 @@ public function update_item_permissions_check( $request ) {
930955
);
931956
}
932957

958+
$if_unmodified_since = $request->get_header( 'If-Unmodified-Since' );
959+
if ( $if_unmodified_since ) {
960+
$client_time = strtotime( $if_unmodified_since );
961+
if ( false !== $client_time ) {
962+
$modified = get_post_datetime( $post, 'modified', 'gmt' );
963+
if ( $modified && $modified->getTimestamp() > $client_time ) {
964+
return new WP_Error(
965+
'rest_precondition_failed',
966+
__( 'Sorry, the post has been modified on the server since you started editing it. Conflict resolution is required.' ),
967+
array( 'status' => 412 )
968+
);
969+
}
970+
}
971+
}
972+
933973
return true;
934974
}
935975

@@ -1040,18 +1080,21 @@ public function update_item( $request ) {
10401080

10411081
// Filter is fired in WP_REST_Attachments_Controller subclass.
10421082
if ( 'attachment' === $this->post_type ) {
1043-
$response = $this->prepare_item_for_response( $post, $request );
1044-
return rest_ensure_response( $response );
1083+
$response = rest_ensure_response( $this->prepare_item_for_response( $post, $request ) );
1084+
$this->add_last_modified_header( $response, $post );
1085+
return $response;
10451086
}
10461087

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

10501091
wp_after_insert_post( $post, true, $post_before );
10511092

1052-
$response = $this->prepare_item_for_response( $post, $request );
1093+
$response = rest_ensure_response( $this->prepare_item_for_response( $post, $request ) );
10531094

1054-
return rest_ensure_response( $response );
1095+
$this->add_last_modified_header( $response, $post );
1096+
1097+
return $response;
10551098
}
10561099

10571100
/**

tests/phpunit/tests/rest-api/rest-posts-controller.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4549,6 +4549,61 @@ public function test_update_item_with_same_template_that_no_longer_exists() {
45494549
$this->assertSame( 'post-my-invalid-template.php', $data['template'] );
45504550
}
45514551

4552+
/**
4553+
* Tests If-Unmodified-Since conditional updates and Last-Modified headers.
4554+
*
4555+
* @covers WP_REST_Posts_Controller::update_item_permissions_check
4556+
* @covers WP_REST_Posts_Controller::get_item
4557+
* @covers WP_REST_Posts_Controller::add_last_modified_header
4558+
* @ticket 47676
4559+
*/
4560+
public function test_update_item_with_if_unmodified_since_precondition() {
4561+
wp_set_current_user( self::$editor_id );
4562+
4563+
$get_request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
4564+
$get_request->set_param( 'context', 'edit' );
4565+
$get_response = rest_get_server()->dispatch( $get_request );
4566+
$this->assertSame( 200, $get_response->get_status() );
4567+
$headers = $get_response->get_headers();
4568+
$this->assertArrayHasKey( 'Last-Modified', $headers );
4569+
$last_modified = $headers['Last-Modified'];
4570+
4571+
$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
4572+
$request->add_header( 'content-type', 'application/json' );
4573+
$title1 = 'Same as last modified';
4574+
$request->set_body( wp_json_encode( $this->set_post_data( array( 'title' => $title1 ) ) ) );
4575+
$request->set_header( 'If-Unmodified-Since', $last_modified );
4576+
$response = rest_get_server()->dispatch( $request );
4577+
$this->assertSame( 200, $response->get_status(), 'Update should succeed when If-Unmodified-Since matches Last-Modified.' );
4578+
$new_data = $response->get_data();
4579+
$this->assertSame( $title1, $new_data['title']['raw'] );
4580+
$this->assertSame( $title1, get_post( self::$post_id )->post_title );
4581+
4582+
$get_response = rest_get_server()->dispatch( $get_request );
4583+
$headers = $get_response->get_headers();
4584+
$this->assertArrayHasKey( 'Last-Modified', $headers );
4585+
$last_modified_after_update = $headers['Last-Modified'];
4586+
4587+
$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
4588+
$request->add_header( 'content-type', 'application/json' );
4589+
$title2 = '1 second after last modified';
4590+
$request->set_body( wp_json_encode( $this->set_post_data( array( 'title' => $title2 ) ) ) );
4591+
$request->set_header( 'If-Unmodified-Since', gmdate( 'D, d M Y H:i:s', strtotime( $last_modified_after_update ) + 1 ) . ' GMT' );
4592+
$response = rest_get_server()->dispatch( $request );
4593+
$this->assertSame( 200, $response->get_status(), 'Update should succeed when If-Unmodified-Since is after the server modified time.' );
4594+
$new_data = $response->get_data();
4595+
$this->assertSame( $title2, $new_data['title']['raw'] );
4596+
4597+
$request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
4598+
$request->add_header( 'content-type', 'application/json' );
4599+
$title3 = 'Should not save';
4600+
$request->set_body( wp_json_encode( $this->set_post_data( array( 'title' => $title3 ) ) ) );
4601+
$request->set_header( 'If-Unmodified-Since', $last_modified );
4602+
$response = rest_get_server()->dispatch( $request );
4603+
$this->assertSame( 412, $response->get_status(), 'Update should fail when If-Unmodified-Since predates current revision.' );
4604+
$this->assertSame( $title2, get_post( self::$post_id )->post_title, 'Expected title not to update due to failed precondition.' );
4605+
}
4606+
45524607
public function verify_post_roundtrip( $input = array(), $expected_output = array() ) {
45534608
// Create the post.
45544609
$request = new WP_REST_Request( 'POST', '/wp/v2/posts' );

0 commit comments

Comments
 (0)