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

collaboration.php in Gutenberg 23.3.0, at lib/compat/wordpress-7.1/collaboration.php

434 lines 14.7 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' => array(
93 'schema' => array(
94 'type' => 'string',
95 'context' => array( 'edit' ),
96 ),
97 ),
98 'single' => true,
99 'type' => 'string',
100 )
101 );
102 }
103 add_action( 'init', 'gutenberg_rest_api_crdt_post_meta' );
104 }
105
106 if ( ! function_exists( 'wp_collaboration_inject_setting' ) ) {
107 /**
108 * Registers the real-time collaboration setting.
109 */
110 function gutenberg_register_real_time_collaboration_setting() {
111 $option_name = 'wp_collaboration_enabled';
112
113 register_setting(
114 'writing',
115 $option_name,
116 array(
117 'type' => 'boolean',
118 'description' => __( 'Enable Real-Time Collaboration', 'gutenberg' ),
119 'sanitize_callback' => 'rest_sanitize_boolean',
120 'default' => true,
121 'show_in_rest' => true,
122 )
123 );
124
125 add_settings_field(
126 $option_name,
127 __( 'Collaboration', 'gutenberg' ),
128 function () use ( $option_name ) {
129 $option_value = get_option( $option_name );
130
131 if ( wp_is_collaboration_allowed() ) :
132 ?>
133 <label for="wp_collaboration_enabled">
134 <input name="wp_collaboration_enabled" type="checkbox" id="wp_collaboration_enabled" value="1" <?php checked( '1', $option_value ); ?>/>
135 <?php _e( "Enable early access to real-time collaboration. Real-time collaboration may affect your website's performance.", 'gutenberg' ); ?>
136 </label>
137 <?php else : ?>
138 <div class="notice notice-warning inline">
139 <?php
140 printf(
141 /* translators: %s: Prefix "Note:". */
142 '<p>' . __( '%s Real-time collaboration has been disabled.', 'gutenberg' ) . '</p>',
143 '<strong>' . __( 'Note:', 'gutenberg' ) . '</strong>'
144 );
145 ?>
146 </div>
147 <?php
148 endif;
149 },
150 'writing'
151 );
152 }
153 add_action( 'admin_init', 'gutenberg_register_real_time_collaboration_setting' );
154 }
155
156 if ( ! function_exists( 'wp_is_collaboration_enabled' ) ) {
157 /**
158 * Determines whether real-time collaboration is enabled.
159 *
160 * If the WP_ALLOW_COLLABORATION constant is false,
161 * collaboration is always disabled regardless of the database option.
162 * Otherwise, falls back to the 'wp_collaboration_enabled' option.
163 *
164 * @since 7.0.0
165 *
166 * @return bool Whether real-time collaboration is enabled.
167 */
168 function wp_is_collaboration_enabled() {
169 return ( wp_is_collaboration_allowed() && (bool) get_option( 'wp_collaboration_enabled' ) );
170 }
171 }
172
173 if ( ! function_exists( 'wp_is_collaboration_allowed' ) ) {
174 /**
175 * Determines whether real-time collaboration is allowed.
176 *
177 * If the WP_ALLOW_COLLABORATION constant is false,
178 * collaboration is not allowed and cannot be enabled.
179 * The constant defaults to true, unless the WP_ALLOW_COLLABORATION
180 * environment variable is set to string "false".
181 *
182 * @since 7.0.0
183 *
184 * @return bool Whether real-time collaboration is allowed.
185 */
186 function wp_is_collaboration_allowed() {
187 if ( ! defined( 'WP_ALLOW_COLLABORATION' ) ) {
188 $env_value = getenv( 'WP_ALLOW_COLLABORATION' );
189 if ( false === $env_value ) {
190 // Environment variable is not defined, default to allowing collaboration.
191 define( 'WP_ALLOW_COLLABORATION', true );
192 } else {
193 /*
194 * Environment variable is defined, let's confirm it is actually set to
195 * "true" as it may still have a string value "false" – the preceeding
196 * `if` branch only tests for the boolean `false`.
197 */
198 define( 'WP_ALLOW_COLLABORATION', 'true' === $env_value );
199 }
200 }
201
202 return WP_ALLOW_COLLABORATION;
203 }
204 }
205
206 /**
207 * Injects the real-time collaboration setting into a global variable.
208 *
209 * @global string $pagenow The filename of the current screen.
210 */
211 function gutenberg_inject_real_time_collaboration_setting() {
212 global $pagenow;
213
214 if ( ! wp_is_collaboration_enabled() ) {
215 return;
216 }
217
218 // Disable real-time collaboration on the site editor.
219 $enabled = true;
220 if (
221 'site-editor.php' === $pagenow ||
222 ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'site-editor-v2' === $_GET['page'] )
223 ) {
224 $enabled = false;
225 }
226
227 wp_add_inline_script(
228 'wp-core-data',
229 'window._wpCollaborationEnabled = ' . wp_json_encode( $enabled ) . ';',
230 'after'
231 );
232 }
233 add_action( 'admin_init', 'gutenberg_inject_real_time_collaboration_setting' );
234
235 /**
236 * Core adds an option with the default value, so we need to set the option to
237 * our intended default when the Gutenberg plugin is activated, provided
238 * collaboration is allowed.
239 */
240 function gutenberg_set_collaboration_option_on_activation() {
241 if ( wp_is_collaboration_allowed() ) {
242 update_option( 'wp_collaboration_enabled', '1' );
243 }
244 }
245 add_action( 'activate_gutenberg/gutenberg.php', 'gutenberg_set_collaboration_option_on_activation' );
246
247 /**
248 * Modifies the post list UI and heartbeat responses for real-time collaboration.
249 *
250 * When RTC is enabled, hides the lock icon and user avatar, replaces the
251 * user-specific lock text with "Currently being edited", changes the "Edit"
252 * row action to "Join", and re-enables controls that core normally hides
253 * for locked posts (since collaborative editing is possible).
254 *
255 * @global string $pagenow The filename of the current screen.
256 */
257 function gutenberg_post_list_collaboration_ui() {
258 global $pagenow;
259
260 if ( ! wp_is_collaboration_enabled() ) {
261 return;
262 }
263
264 // Heartbeat filter applies globally (not just edit.php) since the
265 // heartbeat API can fire from any admin page.
266 add_filter( 'heartbeat_received', 'gutenberg_filter_locked_posts_heartbeat_for_rtc', 20 );
267
268 // CSS, JS, and row action overrides only apply on the posts list page.
269 if ( 'edit.php' !== $pagenow ) {
270 return;
271 }
272
273 add_action( 'admin_head', 'gutenberg_post_list_collaboration_styles' );
274 add_filter( 'gettext', 'gutenberg_filter_locked_post_text_for_rtc', 10, 3 );
275 add_filter( 'post_row_actions', 'gutenberg_post_list_collaboration_row_actions', 10, 2 );
276 add_filter( 'page_row_actions', 'gutenberg_post_list_collaboration_row_actions', 10, 2 );
277 }
278 add_action( 'admin_init', 'gutenberg_post_list_collaboration_ui' );
279
280 /**
281 * Filters the heartbeat response to remove user-specific lock information
282 * when real-time collaboration is enabled.
283 *
284 * WordPress core's wp_check_locked_posts() runs at priority 10 and populates
285 * the 'wp-check-locked-posts' key with user name, avatar, and text. This
286 * filter runs at priority 20 to replace that data with a generic message,
287 * preventing user-specific lock info from reaching the client.
288 *
289 * @param array $response The heartbeat response.
290 * @return array Modified heartbeat response.
291 */
292 function gutenberg_filter_locked_posts_heartbeat_for_rtc( $response ) {
293 if ( ! empty( $response['wp-check-locked-posts'] ) ) {
294 foreach ( $response['wp-check-locked-posts'] as $key => $lock_data ) {
295 $response['wp-check-locked-posts'][ $key ]['text'] = __( 'Currently being edited', 'gutenberg' );
296 unset( $response['wp-check-locked-posts'][ $key ]['avatar_src'] );
297 unset( $response['wp-check-locked-posts'][ $key ]['avatar_src_2x'] );
298 }
299 }
300
301 return $response;
302 }
303
304 /**
305 * Outputs CSS to hide the post lock icon and user avatar in the post list
306 * when real-time collaboration is enabled.
307 *
308 * Also re-enables checkboxes and row actions that WordPress core hides for
309 * locked posts, since collaborative editing means the post is not exclusively
310 * locked. Toggles "Edit" / "Join" action link text via the
311 * `.wp-collaborative-editing` class that the heartbeat already manages.
312 */
313 function gutenberg_post_list_collaboration_styles() {
314 ?>
315 <style type="text/css">
316 /*
317 * Hide the lock indicator icon in the checkbox column.
318 * WordPress core shows it via .wp-locked .locked-indicator { display: block },
319 * so we match that specificity to override it.
320 */
321 .wp-locked .locked-indicator {
322 display: none;
323 }
324 /* Hide the user avatar in the locked info area. */
325 .wp-locked .locked-info .locked-avatar {
326 display: none;
327 }
328 /*
329 * Re-enable controls that core hides for locked posts,
330 * since RTC allows collaborative editing.
331 * Must use `tr.wp-locked` to match core's specificity in
332 * list-tables.css and actually override its `display: none`.
333 */
334 tr.wp-locked .check-column label,
335 tr.wp-locked .check-column input[type="checkbox"] {
336 display: revert;
337 }
338 tr.wp-locked .row-actions .inline {
339 display: revert;
340 }
341 /*
342 * Toggle "Edit" / "Join" action link text based on lock state.
343 * The heartbeat adds/removes .wp-locked on locked rows. This
344 * CSS only runs when RTC is enabled, so .wp-locked here always
345 * means collaborative editing, not exclusive locking.
346 */
347 .join-action-text {
348 display: none;
349 }
350 .wp-locked .edit-action-text {
351 display: none;
352 }
353 .wp-locked .join-action-text {
354 display: inline;
355 }
356 </style>
357 <?php
358 }
359
360 /**
361 * Filters the translation of the lock text to replace user-specific
362 * "%s is currently editing" with a generic "Currently being edited"
363 * message on initial page render.
364 *
365 * WordPress core outputs this text server-side in WP_Posts_List_Table.
366 * Using a gettext filter replaces it before it reaches the browser,
367 * avoiding a flash of the original text.
368 *
369 * @param string $translation Translated text.
370 * @param string $text Original text to translate.
371 * @param string $domain Text domain.
372 * @return string Modified translation.
373 */
374 function gutenberg_filter_locked_post_text_for_rtc( $translation, $text, $domain ) {
375 if ( 'default' === $domain && '%s is currently editing' === $text ) {
376 return __( 'Currently being edited', 'gutenberg' );
377 }
378
379 return $translation;
380 }
381
382 /**
383 * Filters post row actions to render both "Edit" and "Join" link text
384 * when real-time collaboration is enabled.
385 *
386 * Both labels are always present in the markup; CSS toggles visibility
387 * based on the `.wp-collaborative-editing` class the heartbeat manages.
388 * This ensures the link text updates when the lock state changes without
389 * requiring a page reload.
390 *
391 * @param string[] $actions An array of row action links.
392 * @param WP_Post $post The post object.
393 * @return string[] Modified row action links.
394 */
395 function gutenberg_post_list_collaboration_row_actions( $actions, $post ) {
396 if ( ! isset( $actions['edit'] ) ) {
397 return $actions;
398 }
399
400 $title = _draft_or_post_title( $post->ID );
401
402 /*
403 * Each state is rendered as `<span class="…-action-text"><a>…</a></span>`.
404 * The toggle classes sit on the outer <span> rather than the <a> so they
405 * fall outside core's responsive selector `.row-actions span a` at
406 * <=782px, which otherwise outranks our class selectors and (a) leaves
407 * both labels visible on unlocked rows and (b) forces `display: inline`
408 * on the visible Join link to misalign with sibling row actions. The
409 * visible label is still a direct text child of <a>, so core's mobile
410 * font-size rule
411 * .row-actions span { font-size: 0; }
412 * .row-actions span a { font-size: 13px; }
413 * still reaches it — that's the fix for the original "Edit invisible
414 * at 0px on mobile" regression. CSS in
415 * gutenberg_post_list_collaboration_styles() flips visibility on the
416 * outer spans based on the row's `wp-locked` class, which core's
417 * inline-edit-post.js maintains in response to heartbeat ticks.
418 */
419 $actions['edit'] = sprintf(
420 '<span class="edit-action-text"><a href="%1$s" aria-label="%2$s">%3$s</a></span>'
421 . '<span class="join-action-text"><a href="%1$s" aria-label="%4$s">%5$s</a></span>',
422 esc_url( get_edit_post_link( $post->ID ) ),
423 /* translators: %s: Post title. */
424 esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;' ), $title ) ),
425 __( 'Edit' ),
426 /* translators: %s: Post title. */
427 esc_attr( sprintf( __( 'Join editing &#8220;%s&#8221;', 'gutenberg' ), $title ) ),
428 /* translators: Action link text for a singular post in the post list. Can be any type of post. */
429 _x( 'Join', 'post list', 'gutenberg' )
430 );
431
432 return $actions;
433 }
434