PluginProbe
Gutenberg / 23.1.1
Gutenberg v23.1.1
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 12.6.0 7.4.0 All 402 releases
gutenberg / lib / compat / wordpress-7.0 / collaboration.php

collaboration.php in Gutenberg 23.1.1, at lib/compat/wordpress-7.0/collaboration.php

426 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Bootstraps collaborative editing.
4 *
5 * @package gutenberg
6 */
7
8 if ( ! class_exists( 'WP_Sync_Post_Meta_Storage' ) ) {
9 require_once __DIR__ . '/interface-wp-sync-storage.php';
10 require_once __DIR__ . '/class-wp-sync-post-meta-storage.php';
11 require_once __DIR__ . '/class-wp-http-polling-sync-server.php';
12 }
13
14 if ( ! function_exists( 'gutenberg_register_sync_storage_post_type' ) ) {
15 /**
16 * Registers the custom post type for sync storage.
17 */
18 function gutenberg_register_sync_storage_post_type() {
19 register_post_type(
20 'wp_sync_storage',
21 array(
22 'labels' => array(
23 'name' => __( 'Sync Updates', 'gutenberg' ),
24 'singular_name' => __( 'Sync Update', 'gutenberg' ),
25 ),
26 'public' => false,
27 'hierarchical' => false,
28 'capabilities' => array(
29 'read' => 'do_not_allow',
30 'read_private_posts' => 'do_not_allow',
31 'create_posts' => 'do_not_allow',
32 'publish_posts' => 'do_not_allow',
33 'edit_posts' => 'do_not_allow',
34 'edit_others_posts' => 'do_not_allow',
35 'edit_published_posts' => 'do_not_allow',
36 'delete_posts' => 'do_not_allow',
37 'delete_others_posts' => 'do_not_allow',
38 'delete_published_posts' => 'do_not_allow',
39 ),
40 'map_meta_cap' => false,
41 'publicly_queryable' => false,
42 'query_var' => false,
43 'rewrite' => false,
44 'show_in_menu' => false,
45 'show_in_rest' => false,
46 'show_ui' => false,
47 'supports' => array( 'custom-fields' ),
48 )
49 );
50 }
51 add_action( 'init', 'gutenberg_register_sync_storage_post_type' );
52 }
53
54 if ( ! function_exists( 'gutenberg_register_collaboration_rest_routes' ) ) {
55 /**
56 * Registers REST API routes for collaborative editing.
57 */
58 function gutenberg_register_collaboration_rest_routes(): void {
59 $sync_storage = new WP_Sync_Post_Meta_Storage();
60 $sync_server = new WP_HTTP_Polling_Sync_Server( $sync_storage );
61 $sync_server->register_routes();
62 }
63 add_action( 'rest_api_init', 'gutenberg_register_collaboration_rest_routes' );
64 }
65
66 if ( ! function_exists( 'wp_collaboration_register_meta' ) ) {
67 /**
68 * Registers post meta for persisting CRDT documents.
69 */
70 function gutenberg_rest_api_crdt_post_meta() {
71 // This string must match WORDPRESS_META_KEY_FOR_CRDT_DOC_PERSISTENCE in @wordpress/sync.
72 $persisted_crdt_post_meta_key = '_crdt_document';
73
74 register_meta(
75 'post',
76 $persisted_crdt_post_meta_key,
77 array(
78 'auth_callback' => static function ( bool $_allowed, string $_meta_key, int $object_id, int $user_id ): bool {
79 return user_can( $user_id, 'edit_post', $object_id );
80 },
81 /*
82 * Revisions must be disabled because we always want to preserve
83 * the latest persisted CRDT document, even when a revision is restored.
84 * This ensures that we can continue to apply updates to a shared document
85 * and peers can simply merge the restored revision like any other incoming
86 * update.
87 *
88 * If we want to persist CRDT documents alongside revisions in the
89 * future, we should do so in a separate meta key.
90 */
91 'revisions_enabled' => false,
92 'show_in_rest' => true,
93 'single' => true,
94 'type' => 'string',
95 )
96 );
97 }
98 add_action( 'init', 'gutenberg_rest_api_crdt_post_meta' );
99 }
100
101 if ( ! function_exists( 'wp_collaboration_inject_setting' ) ) {
102 /**
103 * Registers the real-time collaboration setting.
104 */
105 function gutenberg_register_real_time_collaboration_setting() {
106 $option_name = 'wp_collaboration_enabled';
107
108 register_setting(
109 'writing',
110 $option_name,
111 array(
112 'type' => 'boolean',
113 'description' => __( 'Enable Real-Time Collaboration', 'gutenberg' ),
114 'sanitize_callback' => 'rest_sanitize_boolean',
115 'default' => true,
116 'show_in_rest' => true,
117 )
118 );
119
120 add_settings_field(
121 $option_name,
122 __( 'Collaboration', 'gutenberg' ),
123 function () use ( $option_name ) {
124 $option_value = get_option( $option_name );
125
126 if ( wp_is_collaboration_allowed() ) :
127 ?>
128 <label for="wp_collaboration_enabled">
129 <input name="wp_collaboration_enabled" type="checkbox" id="wp_collaboration_enabled" value="1" <?php checked( '1', $option_value ); ?>/>
130 <?php _e( "Enable early access to real-time collaboration. Real-time collaboration may affect your website's performance.", 'gutenberg' ); ?>
131 </label>
132 <?php else : ?>
133 <div class="notice notice-warning inline">
134 <?php
135 printf(
136 /* translators: %s: Prefix "Note:". */
137 '<p>' . __( '%s Real-time collaboration has been disabled.', 'gutenberg' ) . '</p>',
138 '<strong>' . __( 'Note:', 'gutenberg' ) . '</strong>'
139 );
140 ?>
141 </div>
142 <?php
143 endif;
144 },
145 'writing'
146 );
147 }
148 add_action( 'admin_init', 'gutenberg_register_real_time_collaboration_setting' );
149 }
150
151 if ( ! function_exists( 'wp_is_collaboration_enabled' ) ) {
152 /**
153 * Determines whether real-time collaboration is enabled.
154 *
155 * If the WP_ALLOW_COLLABORATION constant is false,
156 * collaboration is always disabled regardless of the database option.
157 * Otherwise, falls back to the 'wp_collaboration_enabled' option.
158 *
159 * @since 7.0.0
160 *
161 * @return bool Whether real-time collaboration is enabled.
162 */
163 function wp_is_collaboration_enabled() {
164 return ( wp_is_collaboration_allowed() && (bool) get_option( 'wp_collaboration_enabled' ) );
165 }
166 }
167
168 if ( ! function_exists( 'wp_is_collaboration_allowed' ) ) {
169 /**
170 * Determines whether real-time collaboration is allowed.
171 *
172 * If the WP_ALLOW_COLLABORATION constant is false,
173 * collaboration is not allowed and cannot be enabled.
174 * The constant defaults to true, unless the WP_ALLOW_COLLABORATION
175 * environment variable is set to string "false".
176 *
177 * @since 7.0.0
178 *
179 * @return bool Whether real-time collaboration is allowed.
180 */
181 function wp_is_collaboration_allowed() {
182 if ( ! defined( 'WP_ALLOW_COLLABORATION' ) ) {
183 $env_value = getenv( 'WP_ALLOW_COLLABORATION' );
184 if ( false === $env_value ) {
185 // Environment variable is not defined, default to allowing collaboration.
186 define( 'WP_ALLOW_COLLABORATION', true );
187 } else {
188 /*
189 * Environment variable is defined, let's confirm it is actually set to
190 * "true" as it may still have a string value "false" – the preceeding
191 * `if` branch only tests for the boolean `false`.
192 */
193 define( 'WP_ALLOW_COLLABORATION', 'true' === $env_value );
194 }
195 }
196
197 return WP_ALLOW_COLLABORATION;
198 }
199 }
200
201 /**
202 * Injects the real-time collaboration setting into a global variable.
203 *
204 * @global string $pagenow The filename of the current screen.
205 */
206 function gutenberg_inject_real_time_collaboration_setting() {
207 global $pagenow;
208
209 if ( ! wp_is_collaboration_enabled() ) {
210 return;
211 }
212
213 // Disable real-time collaboration on the site editor.
214 $enabled = true;
215 if (
216 'site-editor.php' === $pagenow ||
217 ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'site-editor-v2' === $_GET['page'] )
218 ) {
219 $enabled = false;
220 }
221
222 wp_add_inline_script(
223 'wp-core-data',
224 'window._wpCollaborationEnabled = ' . wp_json_encode( $enabled ) . ';',
225 'after'
226 );
227 }
228 add_action( 'admin_init', 'gutenberg_inject_real_time_collaboration_setting' );
229
230 /**
231 * Core adds an option with the default value, so we need to set the option to
232 * our intended default when the Gutenberg plugin is activated, provided
233 * collaboration is allowed.
234 */
235 function gutenberg_set_collaboration_option_on_activation() {
236 if ( wp_is_collaboration_allowed() ) {
237 update_option( 'wp_collaboration_enabled', '1' );
238 }
239 }
240 add_action( 'activate_gutenberg/gutenberg.php', 'gutenberg_set_collaboration_option_on_activation' );
241
242 /**
243 * Modifies the post list UI and heartbeat responses for real-time collaboration.
244 *
245 * When RTC is enabled, hides the lock icon and user avatar, replaces the
246 * user-specific lock text with "Currently being edited", changes the "Edit"
247 * row action to "Join", and re-enables controls that core normally hides
248 * for locked posts (since collaborative editing is possible).
249 *
250 * @global string $pagenow The filename of the current screen.
251 */
252 function gutenberg_post_list_collaboration_ui() {
253 global $pagenow;
254
255 if ( ! wp_is_collaboration_enabled() ) {
256 return;
257 }
258
259 // Heartbeat filter applies globally (not just edit.php) since the
260 // heartbeat API can fire from any admin page.
261 add_filter( 'heartbeat_received', 'gutenberg_filter_locked_posts_heartbeat_for_rtc', 20 );
262
263 // CSS, JS, and row action overrides only apply on the posts list page.
264 if ( 'edit.php' !== $pagenow ) {
265 return;
266 }
267
268 add_action( 'admin_head', 'gutenberg_post_list_collaboration_styles' );
269 add_filter( 'gettext', 'gutenberg_filter_locked_post_text_for_rtc', 10, 3 );
270 add_filter( 'post_row_actions', 'gutenberg_post_list_collaboration_row_actions', 10, 2 );
271 add_filter( 'page_row_actions', 'gutenberg_post_list_collaboration_row_actions', 10, 2 );
272 }
273 add_action( 'admin_init', 'gutenberg_post_list_collaboration_ui' );
274
275 /**
276 * Filters the heartbeat response to remove user-specific lock information
277 * when real-time collaboration is enabled.
278 *
279 * WordPress core's wp_check_locked_posts() runs at priority 10 and populates
280 * the 'wp-check-locked-posts' key with user name, avatar, and text. This
281 * filter runs at priority 20 to replace that data with a generic message,
282 * preventing user-specific lock info from reaching the client.
283 *
284 * @param array $response The heartbeat response.
285 * @return array Modified heartbeat response.
286 */
287 function gutenberg_filter_locked_posts_heartbeat_for_rtc( $response ) {
288 if ( ! empty( $response['wp-check-locked-posts'] ) ) {
289 foreach ( $response['wp-check-locked-posts'] as $key => $lock_data ) {
290 $response['wp-check-locked-posts'][ $key ]['text'] = __( 'Currently being edited', 'gutenberg' );
291 unset( $response['wp-check-locked-posts'][ $key ]['avatar_src'] );
292 unset( $response['wp-check-locked-posts'][ $key ]['avatar_src_2x'] );
293 }
294 }
295
296 return $response;
297 }
298
299 /**
300 * Outputs CSS to hide the post lock icon and user avatar in the post list
301 * when real-time collaboration is enabled.
302 *
303 * Also re-enables checkboxes and row actions that WordPress core hides for
304 * locked posts, since collaborative editing means the post is not exclusively
305 * locked. Toggles "Edit" / "Join" action link text via the
306 * `.wp-collaborative-editing` class that the heartbeat already manages.
307 */
308 function gutenberg_post_list_collaboration_styles() {
309 ?>
310 <style type="text/css">
311 /*
312 * Hide the lock indicator icon in the checkbox column.
313 * WordPress core shows it via .wp-locked .locked-indicator { display: block },
314 * so we match that specificity to override it.
315 */
316 .wp-locked .locked-indicator {
317 display: none;
318 }
319 /* Hide the user avatar in the locked info area. */
320 .wp-locked .locked-info .locked-avatar {
321 display: none;
322 }
323 /*
324 * Re-enable controls that core hides for locked posts,
325 * since RTC allows collaborative editing.
326 * Must use `tr.wp-locked` to match core's specificity in
327 * list-tables.css and actually override its `display: none`.
328 */
329 tr.wp-locked .check-column label,
330 tr.wp-locked .check-column input[type="checkbox"] {
331 display: revert;
332 }
333 tr.wp-locked .row-actions .inline {
334 display: revert;
335 }
336 /*
337 * Toggle "Edit" / "Join" action link text based on lock state.
338 * The heartbeat adds/removes .wp-locked on locked rows. This
339 * CSS only runs when RTC is enabled, so .wp-locked here always
340 * means collaborative editing, not exclusive locking.
341 */
342 .join-action-text {
343 display: none;
344 }
345 .wp-locked .edit-action-text {
346 display: none;
347 }
348 .wp-locked .join-action-text {
349 display: inline;
350 }
351 </style>
352 <?php
353 }
354
355 /**
356 * Filters the translation of the lock text to replace user-specific
357 * "%s is currently editing" with a generic "Currently being edited"
358 * message on initial page render.
359 *
360 * WordPress core outputs this text server-side in WP_Posts_List_Table.
361 * Using a gettext filter replaces it before it reaches the browser,
362 * avoiding a flash of the original text.
363 *
364 * @param string $translation Translated text.
365 * @param string $text Original text to translate.
366 * @param string $domain Text domain.
367 * @return string Modified translation.
368 */
369 function gutenberg_filter_locked_post_text_for_rtc( $translation, $text, $domain ) {
370 if ( 'default' === $domain && '%s is currently editing' === $text ) {
371 return __( 'Currently being edited', 'gutenberg' );
372 }
373
374 return $translation;
375 }
376
377 /**
378 * Filters post row actions to render both "Edit" and "Join" link text
379 * when real-time collaboration is enabled.
380 *
381 * Both labels are always present in the markup; CSS toggles visibility
382 * based on the `.wp-collaborative-editing` class the heartbeat manages.
383 * This ensures the link text updates when the lock state changes without
384 * requiring a page reload.
385 *
386 * @param string[] $actions An array of row action links.
387 * @param WP_Post $post The post object.
388 * @return string[] Modified row action links.
389 */
390 function gutenberg_post_list_collaboration_row_actions( $actions, $post ) {
391 if ( ! isset( $actions['edit'] ) ) {
392 return $actions;
393 }
394
395 $title = _draft_or_post_title( $post->ID );
396
397 /*
398 * Both "Edit" and "Join" labels are rendered. The visible label is
399 * toggled by CSS based on the row's `wp-collaborative-editing` class,
400 * which is added or removed by inline-edit-post.js in response to
401 * heartbeat ticks.
402 */
403 $actions['edit'] = sprintf(
404 '<a href="%1$s">'
405 . '<span class="edit-action-text">'
406 . '<span aria-hidden="true">%2$s</span>'
407 . '<span class="screen-reader-text">%3$s</span>'
408 . '</span>'
409 . '<span class="join-action-text">'
410 . '<span aria-hidden="true">%4$s</span>'
411 . '<span class="screen-reader-text">%5$s</span>'
412 . '</span>'
413 . '</a>',
414 get_edit_post_link( $post->ID ),
415 __( 'Edit' ),
416 /* translators: %s: Post title. */
417 sprintf( __( 'Edit &#8220;%s&#8221;' ), $title ),
418 /* translators: Action link text for a singular post in the post list. Can be any type of post. */
419 _x( 'Join', 'post list', 'gutenberg' ),
420 /* translators: %s: Post title. */
421 sprintf( __( 'Join editing &#8220;%s&#8221;', 'gutenberg' ), $title )
422 );
423
424 return $actions;
425 }
426