Skip to content
Closed
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 @@ -229,41 +229,45 @@ public function create_item( $request ) {
require_once ABSPATH . 'wp-admin/includes/post.php';
}

$post_lock = wp_check_post_lock( $post->ID );
$is_draft = 'draft' === $post->post_status || 'auto-draft' === $post->post_status;
$post_lock_is_active = wp_check_post_lock( $post->ID );
$is_auto_draft = 'auto-draft' === $post->post_status;
$is_draft = 'draft' === $post->post_status || $is_auto_draft;
$is_collaboration_enabled = wp_is_collaboration_enabled();

/*
* In the context of real-time collaboration, all peers are effectively
* authors and we don't want to vary behavior based on whether they are the
* original author. Always target an autosave revision.
*
* This avoids the following issue when real-time collaboration is enabled:
*
* - Autosaves from the original author (if they have the post lock) will
* target the saved post.
* When a post is still in draft form, updates from the author can directly update the post.
* Other autosaves must be stored as per-user autosave revisions.
*
* - Autosaves from other users are applied to a post revision.
* When RTC is active, however, regular draft autosaves must not update the parent post directly.
* Since all peers are sharing a persisted editing state (a shared CRDT), it’s important that
* they all store updates in a revision. If edits were applied to the post, then upon the next
* editor reload, it would appear as though the post had been updated externally, and those same
* changes would be re-applied to the CRDT, duplicating the edits.
*
* - If any user reloads a post, they load changes from the author's autosave.
*
* - The saved post has now diverged from the persisted CRDT document. The
* content (and/or title or excerpt) are now "ahead" of the persisted CRDT
* document.
*
* - When the persisted CRDT document is loaded, a diff is computed against
* the saved post. This diff is then applied to the in-memory CRDT
* document, which can lead to duplicate inserts or deletions.
* The one caveat for RTC is that the first peer to store an edit must promote an auto-draft
* into a real draft post. If this doesn’t happen then the peers may continue to make edits
* but the draft will be lost, as auto-drafts are not listed in post views.
*/
$is_collaboration_enabled = wp_is_collaboration_enabled();
$can_update_author_draft_post = (
$is_draft &&
(int) $post->post_author === $user_id &&
! $is_collaboration_enabled
);

$can_promote_auto_draft_post = (
$is_auto_draft &&
$is_collaboration_enabled &&
current_user_can( 'edit_post', $post->ID )
);

$should_update_parent_draft_post = (
$can_promote_auto_draft_post ||
( ! $post_lock_is_active && $can_update_author_draft_post )
);

if ( $is_draft && (int) $post->post_author === $user_id && ! $post_lock && ! $is_collaboration_enabled ) {
/*
* Draft posts for the same author: autosaving updates the post and does not create a revision.
* Convert the post object to an array and add slashes, wp_update_post() expects escaped array.
*/
if ( $should_update_parent_draft_post ) {
$autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true );
} else {
// Non-draft posts: create or update the post autosave. Pass the meta data.
$autosave_id = $this->create_post_autosave( (array) $prepared_post, (array) $request->get_param( 'meta' ) );
}

Expand Down
168 changes: 168 additions & 0 deletions tests/phpunit/tests/collaboration/restAutosavesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php
/**
* Tests for the collaboration autosaves REST controller override.
*
* @package WordPress
* @subpackage Collaboration
*
* @group collaboration
* @group restapi
*/
class Tests_Collaboration_RestAutosavesController extends WP_UnitTestCase {

protected static int $author_id;
protected static int $editor_id;

public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$author_id = $factory->user->create( array( 'role' => 'author' ) );
self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) );
}

public static function wpTearDownAfterClass() {
self::delete_user( self::$author_id );
self::delete_user( self::$editor_id );
delete_option( 'wp_collaboration_enabled' );
}

public function set_up() {
parent::set_up();
wp_set_current_user( self::$author_id );
}

/**
* Creates an empty auto-draft post.
*
* @return int Post ID.
*/
private function create_auto_draft(): int {
return self::factory()->post->create(
array(
'post_author' => self::$author_id,
'post_content' => '',
'post_status' => 'auto-draft',
'post_title' => 'Auto Draft',
'post_type' => 'post',
)
);
}

/**
* Creates a draft post.
*
* @param string $title Post title.
* @param string $content Post content.
* @return int Post ID.
*/
private function create_draft( string $title, string $content ): int {
return self::factory()->post->create(
array(
'post_author' => self::$author_id,
'post_content' => $content,
'post_status' => 'draft',
'post_title' => $title,
'post_type' => 'post',
)
);
}

/**
* Dispatches an autosave request for a post.
*
* @param int $post_id Post ID.
* @param string $title Autosaved post title.
* @param string $content Autosaved post content.
* @return WP_REST_Response Autosave response.
*/
private function dispatch_autosave( int $post_id, string $title, string $content ): WP_REST_Response {
$request = new WP_REST_Request( 'POST', "/wp/v2/posts/{$post_id}/autosaves" );
$request->set_param( 'title', $title );
$request->set_param( 'content', $content );
$request->set_param( 'status', 'draft' );

return rest_get_server()->dispatch( $request );
}

/**
* @ticket 65138
*/
public function test_auto_draft_autosave_promotes_parent_post_when_collaboration_is_disabled() {
update_option( 'wp_collaboration_enabled', 0 );

$post_id = $this->create_auto_draft();
$title = 'No RTC autosaved title';
$content = '<!-- wp:paragraph --><p>No RTC autosaved content</p><!-- /wp:paragraph -->';

$response = $this->dispatch_autosave( $post_id, $title, $content );

$this->assertSame( 200, $response->get_status() );
$post = get_post( $post_id );
$this->assertSame( 'draft', $post->post_status );
$this->assertSame( $title, $post->post_title );
$this->assertSame( $content, $post->post_content );
}

/**
* @ticket 65138
*/
public function test_auto_draft_autosave_promotes_parent_post_when_collaboration_is_enabled() {
update_option( 'wp_collaboration_enabled', 1 );

$post_id = $this->create_auto_draft();
$title = 'RTC autosaved title';
$content = '<!-- wp:paragraph --><p>RTC autosaved content</p><!-- /wp:paragraph -->';

$response = $this->dispatch_autosave( $post_id, $title, $content );

$this->assertSame( 200, $response->get_status() );
$post = get_post( $post_id );
$this->assertSame( 'draft', $post->post_status );
$this->assertSame( $title, $post->post_title );
$this->assertSame( $content, $post->post_content );
}

/**
* @ticket 65138
*/
public function test_collaborator_auto_draft_autosave_promotes_parent_post_when_collaboration_is_enabled() {
update_option( 'wp_collaboration_enabled', 1 );

$post_id = $this->create_auto_draft();
$title = 'RTC collaborator autosaved title';
$content = '<!-- wp:paragraph --><p>RTC collaborator autosaved content</p><!-- /wp:paragraph -->';

wp_set_current_user( self::$editor_id );
$response = $this->dispatch_autosave( $post_id, $title, $content );

$this->assertSame( 200, $response->get_status() );
$post = get_post( $post_id );
$this->assertSame( 'draft', $post->post_status );
$this->assertSame( $title, $post->post_title );
$this->assertSame( $content, $post->post_content );
}

/**
* @ticket 65138
*/
public function test_draft_autosave_creates_revision_when_collaboration_is_enabled() {
update_option( 'wp_collaboration_enabled', 1 );

$original_title = 'Original RTC draft title';
$original_content = '<!-- wp:paragraph --><p>Original RTC draft content</p><!-- /wp:paragraph -->';
$post_id = $this->create_draft( $original_title, $original_content );
$title = 'RTC draft autosaved title';
$content = '<!-- wp:paragraph --><p>RTC draft autosaved content</p><!-- /wp:paragraph -->';

$response = $this->dispatch_autosave( $post_id, $title, $content );

$this->assertSame( 200, $response->get_status() );
$post = get_post( $post_id );
$this->assertSame( 'draft', $post->post_status );
$this->assertSame( $original_title, $post->post_title );
$this->assertSame( $original_content, $post->post_content );

$autosave = wp_get_post_autosave( $post_id, self::$author_id );
$this->assertInstanceOf( WP_Post::class, $autosave );
$this->assertSame( $title, $autosave->post_title );
$this->assertSame( $content, $autosave->post_content );
}
}
Loading