PluginProbe
Gutenberg / 24.1.0
Gutenberg v24.1.0
24.1.0 24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 All 404 releases
← All changes | lib/experimental/collaboration/class-gutenberg-rest-autosaves-controller.php +110 -52 23.7.2 → 24.1.0 View file →
@@ -14,8 +14,36 @@
14 14 */
15 15 class Gutenberg_REST_Autosaves_Controller extends WP_REST_Autosaves_Controller {
16 16
17 17 /**
18 + * Meta key holding the CRDT snapshot describing an autosave's content.
19 + *
20 + * @var string
21 + */
22 + public const CRDT_SNAPSHOT_META_KEY = '_crdt_autosave_snapshot';
23 +
24 + /**
25 + * Request parameter holding the CRDT snapshot describing an autosave's
26 + * content.
27 + *
28 + * This string must match CRDT_AUTOSAVE_SNAPSHOT_KEY in @wordpress/core-data.
29 + *
30 + * @var string
31 + */
32 + public const CRDT_SNAPSHOT_PARAM = 'crdt_snapshot';
33 +
34 + /**
35 + * Maximum accepted length of a stored CRDT snapshot, in bytes.
36 + *
37 + * A snapshot is a state vector plus a delete set, so it grows with the
38 + * much slower than content. Unlikely to exceed 1 MB, but use as a safety
39 + * backstop.
40 + *
41 + * @var int
42 + */
43 + private const MAX_CRDT_SNAPSHOT_LENGTH = MB_IN_BYTES;
44 +
45 + /**
18 46 * Parent post controller.
19 47 *
20 48 * @since 5.0.0
21 49 * @var WP_REST_Controller
@@ -52,11 +80,12 @@
52 80 * @param WP_REST_Request $request Full details about the request.
53 81 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
54 82 */
55 83 public function create_item( $request ) {
56 -
57 - if ( ! defined( 'WP_RUN_CORE_TESTS' ) && ! defined( 'DOING_AUTOSAVE' ) ) {
58 - define( 'DOING_AUTOSAVE', true );
84 + // Gutenberg selects this controller only when RTC is enabled. Preserve
85 + // Core behavior if it is registered explicitly or instantiated directly.
86 + if ( ! wp_is_collaboration_enabled() ) {
87 + return parent::create_item( $request );
59 88 }
60 89
61 90 $post = $this->get_parent( $request['id'] );
62 91
@@ -63,64 +92,57 @@
63 92 if ( is_wp_error( $post ) ) {
64 93 return $post;
65 94 }
66 95
67 - $prepared_post = $this->gutenberg_parent_controller->prepare_item_for_database( $request );
68 - $prepared_post->ID = $post->ID;
69 - $user_id = get_current_user_id();
96 + // Autosave creation may fire this callback for revisioned post meta.
97 + if ( ! function_exists( 'wp_autosave_post_revisioned_meta_fields' ) ) {
98 + require_once ABSPATH . 'wp-admin/includes/post.php';
99 + }
70 100
71 - // We need to check post lock to ensure the original author didn't leave their browser tab open.
72 - if ( ! function_exists( 'wp_check_post_lock' ) ) {
73 - require_once ABSPATH . 'wp-admin/includes/post.php';
101 + // Post-type collaboration support is determined after the autosaves
102 + // controller is selected, so disabled post types must delegate to Core.
103 + if ( wp_is_post_type_collaboration_disabled( $post->post_type ) ) {
104 + return parent::create_item( $request );
74 105 }
75 106
76 - $post_lock_is_active = wp_check_post_lock( $post->ID );
77 - $is_auto_draft = 'auto-draft' === $post->post_status;
78 - $is_draft = 'draft' === $post->post_status || $is_auto_draft;
79 - $is_collaboration_enabled = wp_is_collaboration_enabled();
107 + if ( ! defined( 'WP_RUN_CORE_TESTS' ) && ! defined( 'DOING_AUTOSAVE' ) ) {
108 + define( 'DOING_AUTOSAVE', true );
109 + }
80 110
111 + $prepared_post = $this->gutenberg_parent_controller->prepare_item_for_database( $request );
112 + $prepared_post->ID = $post->ID;
113 + $post_data = (array) $prepared_post;
114 + $meta = (array) $request->get_param( 'meta' );
115 +
81 116 /*
82 - * When a post is still in draft form, updates from the author can directly update the post.
83 - * Other autosaves must be stored as per-user autosave revisions.
117 + * Regular draft autosaves must not update the parent post directly under
118 + * RTC. All peers share a persisted editing state in the CRDT, so their
119 + * autosaved changes must be stored in revisions. Applying those edits to
120 + * the parent post would make them appear to be external changes when the
121 + * editor next reloads, causing the same changes to be reapplied to the
122 + * CRDT and duplicated.
84 123 *
85 - * When RTC is active, however, regular draft autosaves must not update the parent post directly.
86 - * Since all peers are sharing a persisted editing state (a shared CRDT), it’s important that
87 - * they all store updates in a revision. If edits were applied to the post, then upon the next
88 - * editor reload, it would appear as though the post had been updated externally, and those same
89 - * changes would be re-applied to the CRDT, duplicating the edits.
90 - *
91 - * The one caveat for RTC is that the first peer to store an edit must promote an auto-draft
92 - * into a real draft post. If this doesn’t happen then the peers may continue to make edits
93 - * but the draft will be lost, as auto-drafts are not listed in post views.
124 + * The first peer to store an edit must still promote an auto-draft into
125 + * a real draft. Otherwise, peers could continue editing while the post
126 + * remains an unlisted auto-draft and may be lost.
94 127 */
95 - $can_update_author_draft_post = (
96 - $is_draft &&
97 - (int) $post->post_author === $user_id &&
98 - ! $is_collaboration_enabled
99 - );
100 - $can_promote_auto_draft_post = (
101 - $is_auto_draft &&
102 - $is_collaboration_enabled &&
128 + $should_promote_auto_draft = (
129 + 'auto-draft' === $post->post_status &&
103 130 current_user_can( 'edit_post', $post->ID )
104 131 );
105 132
106 - $should_update_parent_draft_post = (
107 - $can_promote_auto_draft_post ||
108 - ( ! $post_lock_is_active && $can_update_author_draft_post )
109 - );
110 -
111 - if ( $should_update_parent_draft_post ) {
112 - $autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true );
113 - } elseif ( $this->is_redundant_autosave( $post, (array) $prepared_post, (array) $request->get_param( 'meta' ) ) ) {
133 + if ( $should_promote_auto_draft ) {
134 + $autosave_id = wp_update_post( wp_slash( $post_data ), true );
135 + } elseif ( $this->is_redundant_autosave( $post, $post_data, $meta ) ) {
114 136 /*
115 - * Nothing changed and there is no existing autosave to update, so
116 - * storing a revision would only create one that is identical to the
117 - * post. Avoid a no-op revision because WordPress decides whether to warn
118 - * about "a more recent autosave" by comparing timestamps.
137 + * Nothing changed from the latest shared state, so storing a
138 + * revision would only create an identical one. Avoid a no-op
139 + * revision because WordPress decides whether to warn about "a more
140 + * recent autosave" by comparing timestamps.
119 141 */
120 142 $autosave_id = $post->ID;
121 143 } else {
122 - $autosave_id = $this->create_post_autosave( (array) $prepared_post, (array) $request->get_param( 'meta' ) );
144 + $autosave_id = $this->create_post_autosave( $post_data, $meta );
123 145 }
124 146
125 147 if ( is_wp_error( $autosave_id ) ) {
126 148 return $autosave_id;
@@ -125,8 +147,13 @@
125 147 if ( is_wp_error( $autosave_id ) ) {
126 148 return $autosave_id;
127 149 }
128 150
151 + // Only save a CRDT snapshot with an autosave (not with parent post)
152 + if ( $autosave_id !== $post->ID ) {
153 + $this->store_crdt_snapshot( $autosave_id, $request );
154 + }
155 +
129 156 $autosave = get_post( $autosave_id );
130 157 $request->set_param( 'context', 'edit' );
131 158
132 159 $response = $this->prepare_item_for_response( $autosave, $request );
@@ -135,8 +162,46 @@
135 162 return $response;
136 163 }
137 164
138 165 /**
166 + * Stores the CRDT snapshot describing the content an autosave captured.
167 + *
168 + * The snapshot allows an editor session verify that a shared document
169 + * already contains everything the autosave holds, so the "more recent
170 + * autosave" notice can be suppressed as redundant.
171 + *
172 + * Anything invalid is dropped rather than erroring, and any previously
173 + * stored snapshot is cleared, because a missing snapshot only means the
174 + * editor falls back to showing the "newer autosave" notice.
175 + *
176 + * @param int $autosave_id Autosave revision ID.
177 + * @param WP_REST_Request $request Full details about the request.
178 + * @return void
179 + */
180 + private function store_crdt_snapshot( $autosave_id, $request ) {
181 + $snapshot = $request->get_param( self::CRDT_SNAPSHOT_PARAM );
182 +
183 + $is_valid_snapshot = (
184 + is_string( $snapshot ) &&
185 + '' !== $snapshot &&
186 + strlen( $snapshot ) <= self::MAX_CRDT_SNAPSHOT_LENGTH
187 + );
188 +
189 + if ( ! $is_valid_snapshot ) {
190 + /*
191 + * The autosave revision is reused across autosaves, so a snapshot
192 + * stored by an earlier request would otherwise remain attached to
193 + * this request's newer content and could wrongly vouch for it.
194 + * Clear it so the editor falls back to showing the notice.
195 + */
196 + delete_metadata( 'post', $autosave_id, self::CRDT_SNAPSHOT_META_KEY );
197 + return;
198 + }
199 +
200 + update_metadata( 'post', $autosave_id, self::CRDT_SNAPSHOT_META_KEY, wp_slash( $snapshot ) );
201 + }
202 +
203 + /**
139 204 * Determines whether an incoming autosave would be a redundant no-op.
140 205 *
141 206 * Core's WP_REST_Autosaves_Controller::create_post_autosave() avoids a
142 207 * redundant write by comparing the incoming autosave against the parent post.
@@ -199,15 +264,8 @@
199 264 * @param WP_Post $post The saved parent post.
200 265 * @return WP_Post The post or revision to compare against.
201 266 */
202 267 private function get_autosave_comparison_baseline( $post ) {
203 - // Outside RTC the parent post is kept up to date (author draft autosaves
204 - // update it directly), so core's parent comparison is already correct.
205 - // Keep the default behavior for non-RTC.
206 - if ( ! wp_is_collaboration_enabled() ) {
207 - return $post;
208 - }
209 -
210 268 // wp_get_post_revisions() returns revisions newest-first by default, and
211 269 // includes per-user autosaves (they are revisions), so the first entry is
212 270 // the most recent shared content.
213 271 $revisions = wp_get_post_revisions( $post->ID, array( 'posts_per_page' => 1 ) );