Make WordPress Core

Changeset 62311


Ignore:
Timestamp:
05/06/2026 09:55:18 PM (5 months ago)
Author:
dmsnell
Message:

RTC: Promote auto-draft posts on first autosave for collaborative sessions to avoid post loss.

When an RTC session in the editor saves edits on a draft post, they always save into an autosave revision. This prevents a sync but where storing to the post itself would lead to duplicate edits. This was set in wordpress/gutenberg commit 9df142b839320316b406ee1a02e23704d42f8719. However, there is an exception to this rule: the first session to save edits needs to promote the autosave revision into a real draft post, if a post doesn’t exist. The reason is that WordPress hides autosaves.

That is, multiple people could be editing a draft post and then once they close it, never be able to find it again. While the data is still in the database, this is a loss of the post from a practical standpoint.

This change introduces an exception where the first time a collaborative session is saving draft edits, the autodraft is promoted to a real draft post, making it visible in the post list.

AI Disclaimer: This bug was detected in a fuzzing system built by AI models and the fix was first proposed by an AI model.

Developed in: ​https://github.com/WordPress/gutenberg/pull/77865
Discussed in: https://core.trac.wordpress.org/ticket/65138
See also: ​https://github.com/WordPress/gutenberg/pull/77716

Follow-up to [61680].
Props danluu, dmsnell.
See #65138.

Location:
trunk
Files:
1 added
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php

    r62075 r62311  
    230230                }
    231231
    232                 $post_lock = wp_check_post_lock( $post->ID );
    233                 $is_draft  = 'draft' === $post->post_status || 'auto-draft' === $post->post_status;
     232                $post_lock_is_active      = wp_check_post_lock( $post->ID );
     233                $is_auto_draft            = 'auto-draft' === $post->post_status;
     234                $is_draft                 = 'draft' === $post->post_status || $is_auto_draft;
     235                $is_collaboration_enabled = wp_is_collaboration_enabled();
    234236
    235237                /*
    236                  * In the context of real-time collaboration, all peers are effectively
    237                  * authors and we don't want to vary behavior based on whether they are the
    238                  * original author. Always target an autosave revision.
     238                 * When a post is still in draft form, updates from the author can directly update the post.
     239                 * Other autosaves must be stored as per-user autosave revisions.
    239240                 *
    240                  * This avoids the following issue when real-time collaboration is enabled:
     241                 * When RTC is active, however, regular draft autosaves must not update the parent post directly.
     242                 * Since all peers are sharing a persisted editing state (a shared CRDT), it’s important that
     243                 * they all store updates in a revision. If edits were applied to the post, then upon the next
     244                 * editor reload, it would appear as though the post had been updated externally, and those same
     245                 * changes would be re-applied to the CRDT, duplicating the edits.
    241246                 *
    242                  * - Autosaves from the original author (if they have the post lock) will
    243                  *   target the saved post.
    244                  *
    245                  * - Autosaves from other users are applied to a post revision.
    246                  *
    247                  * - If any user reloads a post, they load changes from the author's autosave.
    248                  *
    249                  * - The saved post has now diverged from the persisted CRDT document. The
    250                  *   content (and/or title or excerpt) are now "ahead" of the persisted CRDT
    251                  *   document.
    252                  *
    253                  * - When the persisted CRDT document is loaded, a diff is computed against
    254                  *   the saved post. This diff is then applied to the in-memory CRDT
    255                  *   document, which can lead to duplicate inserts or deletions.
     247                 * The one caveat for RTC is that the first peer to store an edit must promote an auto-draft
     248                 * into a real draft post. If this doesn’t happen then the peers may continue to make edits
     249                 * but the draft will be lost, as auto-drafts are not listed in post views.
    256250                 */
    257                 $is_collaboration_enabled = wp_is_collaboration_enabled();
    258 
    259                 if ( $is_draft && (int) $post->post_author === $user_id && ! $post_lock && ! $is_collaboration_enabled ) {
    260                         /*
    261                          * Draft posts for the same author: autosaving updates the post and does not create a revision.
    262                          * Convert the post object to an array and add slashes, wp_update_post() expects escaped array.
    263                          */
     251                $can_update_author_draft_post = (
     252                        $is_draft &&
     253                        (int) $post->post_author === $user_id &&
     254                        ! $is_collaboration_enabled
     255                );
     256
     257                $can_promote_auto_draft_post = (
     258                        $is_auto_draft &&
     259                        $is_collaboration_enabled &&
     260                        current_user_can( 'edit_post', $post->ID )
     261                );
     262
     263                $should_update_parent_draft_post = (
     264                        $can_promote_auto_draft_post ||
     265                        ( ! $post_lock_is_active && $can_update_author_draft_post )
     266                );
     267
     268                if ( $should_update_parent_draft_post ) {
    264269                        $autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true );
    265270                } else {
    266                         // Non-draft posts: create or update the post autosave. Pass the meta data.
    267271                        $autosave_id = $this->create_post_autosave( (array) $prepared_post, (array) $request->get_param( 'meta' ) );
    268272                }
Note: See TracChangeset for help on using the changeset viewer.