Skip to content

Commit

Permalink
REST API: Prevent deletion of post revisions.
Browse files Browse the repository at this point in the history
Allowing the client to delete revisions breaks the "audit trail" functionality. This is not allowed in WordPress and shouldn't be allowed through the API.
While not recommended, a plugin may opt-in to the previous behavior by setting a custom 'delete_post' capability for the revisions post type.

Props dlh, danielbachhuber, TimothyBlynJacobs, azaozz, kadamwhite.
Fixes #43709.



git-svn-id: https://develop.svn.wordpress.org/trunk@45812 602fd350-edb4-49c9-b593-d223f7449a82
  • Loading branch information
kadamwhite committed Aug 15, 2019
1 parent eb468c4 commit 8f10077
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 7 deletions.
7 changes: 2 additions & 5 deletions src/wp-includes/capabilities.php
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -72,11 +72,8 @@ function map_meta_cap( $cap, $user_id, ...$args ) {
} }


if ( 'revision' == $post->post_type ) { if ( 'revision' == $post->post_type ) {
$post = get_post( $post->post_parent ); $caps[] = 'do_not_allow';
if ( ! $post ) { break;
$caps[] = 'do_not_allow';
break;
}
} }


if ( ( get_option( 'page_for_posts' ) == $post->ID ) || ( get_option( 'page_on_front' ) == $post->ID ) ) { if ( ( get_option( 'page_for_posts' ) == $post->ID ) || ( get_option( 'page_on_front' ) == $post->ID ) ) {
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ public function get_item( $request ) {
return $parent; return $parent;
} }


$parent_post_type = get_post_type_object( $parent->post_type );
if ( ! current_user_can( $parent_post_type->cap->delete_post, $parent->ID ) ) {
return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete revisions of this post.' ), array( 'status' => rest_authorization_required_code() ) );
}

$revision = $this->get_revision( $request['id'] ); $revision = $this->get_revision( $request['id'] );
if ( is_wp_error( $revision ) ) { if ( is_wp_error( $revision ) ) {
return $revision; return $revision;
Expand Down Expand Up @@ -383,7 +388,12 @@ public function delete_item_permissions_check( $request ) {
} }


$post_type = get_post_type_object( 'revision' ); $post_type = get_post_type_object( 'revision' );
return current_user_can( $post_type->cap->delete_post, $revision->ID );
if ( ! current_user_can( $post_type->cap->delete_post, $revision->ID ) ) {
return new WP_Error( 'rest_cannot_delete', __( 'Sorry, you are not allowed to delete this revision.' ), array( 'status' => rest_authorization_required_code() ) );
}

return true;
} }


/** /**
Expand Down
42 changes: 41 additions & 1 deletion tests/phpunit/tests/rest-api/rest-revisions-controller.php
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ public function setUp() {
$this->revision_id3 = $this->revision_3->ID; $this->revision_id3 = $this->revision_3->ID;
} }


public function tearDown() {
parent::tearDown();

remove_filter( 'map_meta_cap', array( $this, '_filter_map_meta_cap_remove_no_allow_revisions' ) );
}

public function _filter_map_meta_cap_remove_no_allow_revisions( $caps, $cap, $user_id, $args ) {
if ( 'delete_post' !== $cap || empty( $args ) ) {
return $caps;
}
$post = get_post( $args[0] );
if ( ! $post || 'revision' !== $post->post_type ) {
return $caps;
}
$key = array_search( 'do_not_allow', $caps, true );
if ( false !== $key ) {
unset( $caps[ $key ] );
}
return $caps;
}

public function test_register_routes() { public function test_register_routes() {
$routes = rest_get_server()->get_routes(); $routes = rest_get_server()->get_routes();
$this->assertArrayHasKey( '/wp/v2/posts/(?P<parent>[\d]+)/revisions', $routes ); $this->assertArrayHasKey( '/wp/v2/posts/(?P<parent>[\d]+)/revisions', $routes );
Expand Down Expand Up @@ -216,13 +237,32 @@ public function test_delete_item() {
$request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 );
$request->set_param( 'force', true ); $request->set_param( 'force', true );
$response = rest_get_server()->dispatch( $request ); $response = rest_get_server()->dispatch( $request );
$this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
$this->assertNotNull( get_post( $this->revision_id1 ) );
}

public function test_delete_item_remove_do_not_allow() {
wp_set_current_user( self::$editor_id );
add_filter( 'map_meta_cap', array( $this, '_filter_map_meta_cap_remove_no_allow_revisions' ), 10, 4 );
$request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 );
$request->set_param( 'force', true );
$response = rest_get_server()->dispatch( $request );
$this->assertEquals( 200, $response->get_status() ); $this->assertEquals( 200, $response->get_status() );
$this->assertNull( get_post( $this->revision_id1 ) ); $this->assertNull( get_post( $this->revision_id1 ) );
} }


public function test_delete_item_no_trash() { public function test_delete_item_cannot_delete_parent() {
wp_set_current_user( self::$editor_id ); wp_set_current_user( self::$editor_id );
$request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 );
$request->set_param( 'force', true );
$response = rest_get_server()->dispatch( $request );
$this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
$this->assertNotNull( get_post( $this->revision_id1 ) );
}


public function test_delete_item_no_trash() {
wp_set_current_user( self::$editor_id );
add_filter( 'map_meta_cap', array( $this, '_filter_map_meta_cap_remove_no_allow_revisions' ), 10, 4 );
$request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 );
$response = rest_get_server()->dispatch( $request ); $response = rest_get_server()->dispatch( $request );
$this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 ); $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501 );
Expand Down

0 comments on commit 8f10077

Please sign in to comment.