← All changes
|
lib/experimental/collaboration/collaboration.php
+121
-119
23.7.0
→
trunk
View file →
| @@ -17,8 +17,12 @@ | ||
| 17 | 17 | /** |
| 18 | 18 | * Registers the custom post type for sync storage. |
| 19 | 19 | */ |
| 20 | 20 | function gutenberg_register_sync_storage_post_type() { |
| 21 | + if ( ! wp_is_collaboration_enabled() ) { | |
| 22 | + return; | |
| 23 | + } | |
| 24 | + | |
| 21 | 25 | register_post_type( |
| 22 | 26 | 'wp_sync_storage', |
| 23 | 27 | array( |
| 24 | 28 | 'labels' => array( |
| @@ -57,10 +61,36 @@ | ||
| 57 | 61 | /** |
| 58 | 62 | * Registers REST API routes for collaborative editing. |
| 59 | 63 | */ |
| 60 | 64 | function gutenberg_register_collaboration_rest_routes(): void { |
| 61 | - $sync_storage = new WP_Sync_Post_Meta_Storage(); | |
| 62 | - $sync_server = new WP_HTTP_Polling_Sync_Server( $sync_storage ); | |
| 65 | + if ( ! wp_is_collaboration_enabled() ) { | |
| 66 | + return; | |
| 67 | + } | |
| 68 | + | |
| 69 | + /** | |
| 70 | + * Filters the sync storage implementation for collaborative editing. | |
| 71 | + * | |
| 72 | + * Allows plugins to replace the default post meta storage with alternative | |
| 73 | + * backends. The primary use case is the realtime-collaboration plugin, | |
| 74 | + * which uses Presence API for awareness and a dedicated wp_collaboration | |
| 75 | + * table for CRDT updates, eliminating cache side effects. | |
| 76 | + * | |
| 77 | + * This filter is unstable and may change as RTC explores fundamental changes | |
| 78 | + * to how syncing works. The current interface assumes a pure naïve relay, | |
| 79 | + * which could change. | |
| 80 | + * | |
| 81 | + * @since Gutenberg 21.x | |
| 82 | + * | |
| 83 | + * @param WP_Sync_Storage $sync_storage Storage implementation. Must implement | |
| 84 | + * the WP_Sync_Storage interface. | |
| 85 | + */ | |
| 86 | + $sync_storage = apply_filters( '__unstable_wp_sync_storage', new WP_Sync_Post_Meta_Storage() ); | |
| 87 | + | |
| 88 | + if ( ! $sync_storage instanceof WP_Sync_Storage ) { | |
| 89 | + $sync_storage = new WP_Sync_Post_Meta_Storage(); | |
| 90 | + } | |
| 91 | + | |
| 92 | + $sync_server = new WP_HTTP_Polling_Sync_Server( $sync_storage ); | |
| 63 | 93 | $sync_server->register_routes(); |
| 64 | 94 | |
| 65 | 95 | $sync_save_server = new WP_Sync_Save_Server(); |
| 66 | 96 | $sync_save_server->register_routes(); |
| @@ -72,8 +102,12 @@ | ||
| 72 | 102 | /** |
| 73 | 103 | * Registers post meta for persisting CRDT documents. |
| 74 | 104 | */ |
| 75 | 105 | function gutenberg_rest_api_crdt_post_meta() { |
| 106 | + if ( ! wp_is_collaboration_enabled() ) { | |
| 107 | + return; | |
| 108 | + } | |
| 109 | + | |
| 76 | 110 | // This string must match POST_META_KEY_FOR_CRDT_DOC_PERSISTENCE in @wordpress/core-data. |
| 77 | 111 | $persisted_crdt_post_meta_key = '_crdt_document'; |
| 78 | 112 | |
| 79 | 113 | register_meta( |
| @@ -107,108 +141,21 @@ | ||
| 107 | 141 | } |
| 108 | 142 | add_action( 'init', 'gutenberg_rest_api_crdt_post_meta' ); |
| 109 | 143 | } |
| 110 | 144 | |
| 111 | -if ( ! function_exists( 'wp_collaboration_inject_setting' ) ) { | |
| 112 | - /** | |
| 113 | - * Registers the real-time collaboration setting. | |
| 114 | - */ | |
| 115 | - function gutenberg_register_real_time_collaboration_setting() { | |
| 116 | - $option_name = 'wp_collaboration_enabled'; | |
| 117 | - | |
| 118 | - register_setting( | |
| 119 | - 'writing', | |
| 120 | - $option_name, | |
| 121 | - array( | |
| 122 | - 'type' => 'boolean', | |
| 123 | - 'description' => __( 'Enable Real-Time Collaboration', 'gutenberg' ), | |
| 124 | - 'sanitize_callback' => 'rest_sanitize_boolean', | |
| 125 | - 'default' => true, | |
| 126 | - 'show_in_rest' => true, | |
| 127 | - ) | |
| 128 | - ); | |
| 129 | - | |
| 130 | - add_settings_field( | |
| 131 | - $option_name, | |
| 132 | - __( 'Collaboration', 'gutenberg' ), | |
| 133 | - function () use ( $option_name ) { | |
| 134 | - $option_value = get_option( $option_name ); | |
| 135 | - | |
| 136 | - if ( wp_is_collaboration_allowed() ) : | |
| 137 | - ?> | |
| 138 | - <label for="wp_collaboration_enabled"> | |
| 139 | - <input name="wp_collaboration_enabled" type="checkbox" id="wp_collaboration_enabled" value="1" <?php checked( '1', $option_value ); ?>/> | |
| 140 | - <?php _e( "Enable early access to real-time collaboration. Real-time collaboration may affect your website's performance.", 'gutenberg' ); ?> | |
| 141 | - </label> | |
| 142 | - <?php else : ?> | |
| 143 | - <div class="notice notice-warning inline"> | |
| 144 | - <?php | |
| 145 | - printf( | |
| 146 | - /* translators: %s: Prefix "Note:". */ | |
| 147 | - '<p>' . __( '%s Real-time collaboration has been disabled.', 'gutenberg' ) . '</p>', | |
| 148 | - '<strong>' . __( 'Note:', 'gutenberg' ) . '</strong>' | |
| 149 | - ); | |
| 150 | - ?> | |
| 151 | - </div> | |
| 152 | - <?php | |
| 153 | - endif; | |
| 154 | - }, | |
| 155 | - 'writing' | |
| 156 | - ); | |
| 157 | - } | |
| 158 | - add_action( 'admin_init', 'gutenberg_register_real_time_collaboration_setting' ); | |
| 159 | -} | |
| 160 | - | |
| 161 | 145 | if ( ! function_exists( 'wp_is_collaboration_enabled' ) ) { |
| 162 | 146 | /** |
| 163 | 147 | * Determines whether real-time collaboration is enabled. |
| 164 | 148 | * |
| 165 | - * If the WP_ALLOW_COLLABORATION constant is false, | |
| 166 | - * collaboration is always disabled regardless of the database option. | |
| 167 | - * Otherwise, falls back to the 'wp_collaboration_enabled' option. | |
| 168 | - * | |
| 169 | 149 | * @since 7.0.0 |
| 170 | 150 | * |
| 171 | 151 | * @return bool Whether real-time collaboration is enabled. |
| 172 | 152 | */ |
| 173 | 153 | function wp_is_collaboration_enabled() { |
| 174 | - return ( wp_is_collaboration_allowed() && (bool) get_option( 'wp_collaboration_enabled' ) ); | |
| 154 | + return gutenberg_is_experiment_enabled( 'gutenberg-real-time-collaboration' ); | |
| 175 | 155 | } |
| 176 | 156 | } |
| 177 | 157 | |
| 178 | -if ( ! function_exists( 'wp_is_collaboration_allowed' ) ) { | |
| 179 | - /** | |
| 180 | - * Determines whether real-time collaboration is allowed. | |
| 181 | - * | |
| 182 | - * If the WP_ALLOW_COLLABORATION constant is false, | |
| 183 | - * collaboration is not allowed and cannot be enabled. | |
| 184 | - * The constant defaults to true, unless the WP_ALLOW_COLLABORATION | |
| 185 | - * environment variable is set to string "false". | |
| 186 | - * | |
| 187 | - * @since 7.0.0 | |
| 188 | - * | |
| 189 | - * @return bool Whether real-time collaboration is allowed. | |
| 190 | - */ | |
| 191 | - function wp_is_collaboration_allowed() { | |
| 192 | - if ( ! defined( 'WP_ALLOW_COLLABORATION' ) ) { | |
| 193 | - $env_value = getenv( 'WP_ALLOW_COLLABORATION' ); | |
| 194 | - if ( false === $env_value ) { | |
| 195 | - // Environment variable is not defined, default to allowing collaboration. | |
| 196 | - define( 'WP_ALLOW_COLLABORATION', true ); | |
| 197 | - } else { | |
| 198 | - /* | |
| 199 | - * Environment variable is defined, let's confirm it is actually set to | |
| 200 | - * "true" as it may still have a string value "false" – the preceding | |
| 201 | - * `if` branch only tests for the boolean `false`. | |
| 202 | - */ | |
| 203 | - define( 'WP_ALLOW_COLLABORATION', 'true' === $env_value ); | |
| 204 | - } | |
| 205 | - } | |
| 206 | - | |
| 207 | - return WP_ALLOW_COLLABORATION; | |
| 208 | - } | |
| 209 | -} | |
| 210 | - | |
| 211 | 158 | if ( ! function_exists( 'wp_is_post_type_collaboration_disabled' ) ) { |
| 212 | 159 | /** |
| 213 | 160 | * Determines whether real-time collaboration is disabled for a post type. |
| 214 | 161 | * |
| @@ -233,8 +180,38 @@ | ||
| 233 | 180 | return (bool) apply_filters( 'wp_is_post_type_collaboration_disabled', false, $post_type ); |
| 234 | 181 | } |
| 235 | 182 | } |
| 236 | 183 | |
| 184 | +/** | |
| 185 | + * Disables real-time collaboration for post types that cannot persist the | |
| 186 | + * CRDT document. | |
| 187 | + * | |
| 188 | + * Collaboration stores its CRDT document in post meta. The REST API only | |
| 189 | + * exposes post meta for post types that support custom fields, so enabling | |
| 190 | + * collaboration for other post types can cause stale sync updates to replace | |
| 191 | + * newer entity content. | |
| 192 | + * | |
| 193 | + * @param bool $disabled Whether real-time collaboration is disabled for the post type. | |
| 194 | + * @param string $post_type Post type name. | |
| 195 | + * @return bool Whether real-time collaboration is disabled for the post type. | |
| 196 | + */ | |
| 197 | +function gutenberg_disable_collaboration_for_post_types_without_custom_fields( $disabled, $post_type ) { | |
| 198 | + if ( $disabled ) { | |
| 199 | + return $disabled; | |
| 200 | + } | |
| 201 | + | |
| 202 | + /* | |
| 203 | + * The attachments REST controller always exposes meta, regardless of | |
| 204 | + * whether the attachment post type supports custom fields. | |
| 205 | + */ | |
| 206 | + if ( 'attachment' === $post_type ) { | |
| 207 | + return false; | |
| 208 | + } | |
| 209 | + | |
| 210 | + return ! post_type_supports( $post_type, 'custom-fields' ); | |
| 211 | +} | |
| 212 | +add_filter( 'wp_is_post_type_collaboration_disabled', 'gutenberg_disable_collaboration_for_post_types_without_custom_fields', 10, 2 ); | |
| 213 | + | |
| 237 | 214 | if ( ! function_exists( 'gutenberg_get_active_edit_lock_user' ) ) { |
| 238 | 215 | /** |
| 239 | 216 | * Returns the user ID recorded in a fresh edit lock. |
| 240 | 217 | * |
| @@ -270,28 +247,15 @@ | ||
| 270 | 247 | } |
| 271 | 248 | } |
| 272 | 249 | |
| 273 | 250 | /** |
| 274 | - * Injects the real-time collaboration setting into a global variable. | |
| 275 | - * | |
| 276 | - * @global string $pagenow The filename of the current screen. | |
| 251 | + * Injects the post types for which real-time collaboration is disabled. | |
| 277 | 252 | */ |
| 278 | -function gutenberg_inject_real_time_collaboration_setting() { | |
| 279 | - global $pagenow; | |
| 280 | - | |
| 253 | +function gutenberg_inject_collaboration_disabled_post_types() { | |
| 281 | 254 | if ( ! wp_is_collaboration_enabled() ) { |
| 282 | 255 | return; |
| 283 | 256 | } |
| 284 | 257 | |
| 285 | - // Disable real-time collaboration on the site editor. | |
| 286 | - $enabled = true; | |
| 287 | - if ( | |
| 288 | - 'site-editor.php' === $pagenow || | |
| 289 | - ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'site-editor-v2' === $_GET['page'] ) | |
| 290 | - ) { | |
| 291 | - $enabled = false; | |
| 292 | - } | |
| 293 | - | |
| 294 | 258 | $disabled_post_types = array_values( |
| 295 | 259 | array_filter( |
| 296 | 260 | get_post_types( array( 'show_in_rest' => true ) ), |
| 297 | 261 | 'wp_is_post_type_collaboration_disabled' |
| @@ -299,28 +263,15 @@ | ||
| 299 | 263 | ); |
| 300 | 264 | |
| 301 | 265 | wp_add_inline_script( |
| 302 | 266 | 'wp-core-data', |
| 303 | - 'window._wpCollaborationEnabled = ' . wp_json_encode( $enabled ) . ';' . | |
| 304 | 267 | 'window._wpCollaborationDisabledPostTypes = ' . wp_json_encode( $disabled_post_types ) . ';', |
| 305 | 268 | 'after' |
| 306 | 269 | ); |
| 307 | 270 | } |
| 308 | -add_action( 'admin_init', 'gutenberg_inject_real_time_collaboration_setting' ); | |
| 271 | +add_action( 'admin_init', 'gutenberg_inject_collaboration_disabled_post_types' ); | |
| 309 | 272 | |
| 310 | 273 | /** |
| 311 | - * Core adds an option with the default value, so we need to set the option to | |
| 312 | - * our intended default when the Gutenberg plugin is activated, provided | |
| 313 | - * collaboration is allowed. | |
| 314 | - */ | |
| 315 | -function gutenberg_set_collaboration_option_on_activation() { | |
| 316 | - if ( wp_is_collaboration_allowed() ) { | |
| 317 | - update_option( 'wp_collaboration_enabled', '1' ); | |
| 318 | - } | |
| 319 | -} | |
| 320 | -add_action( 'activate_' . plugin_basename( dirname( __DIR__, 3 ) . '/gutenberg.php' ), 'gutenberg_set_collaboration_option_on_activation' ); | |
| 321 | - | |
| 322 | -/** | |
| 323 | 274 | * Modifies the post list UI and heartbeat responses for real-time collaboration. |
| 324 | 275 | * |
| 325 | 276 | * When RTC is enabled, hides the lock icon and user avatar, replaces the |
| 326 | 277 | * user-specific lock text with "Currently being edited", changes the "Edit" |
| @@ -589,4 +540,55 @@ | ||
| 589 | 540 | ); |
| 590 | 541 | |
| 591 | 542 | return $actions; |
| 592 | 543 | } |
| 544 | + | |
| 545 | +/** | |
| 546 | + * Adds the autosave's CRDT snapshot to the block editor settings when | |
| 547 | + * real-time collaboration is enabled. | |
| 548 | + * | |
| 549 | + * The snapshot describes the document state the autosave captured. The editor | |
| 550 | + * verifies its own shared document against it, and suppresses the "there is a | |
| 551 | + * more recent autosave" notice when the shared document already contains | |
| 552 | + * everything the autosave holds. | |
| 553 | + * | |
| 554 | + * @param array $settings Editor settings. | |
| 555 | + * @param WP_Block_Editor_Context $block_editor_context The current block editor context. | |
| 556 | + * @return array Filtered editor settings. | |
| 557 | + */ | |
| 558 | +function gutenberg_add_autosave_details_to_editor_settings( $settings, $block_editor_context ) { | |
| 559 | + if ( ! isset( $settings['autosave'] ) || empty( $block_editor_context->post ) ) { | |
| 560 | + return $settings; | |
| 561 | + } | |
| 562 | + | |
| 563 | + if ( ! wp_is_collaboration_enabled() ) { | |
| 564 | + return $settings; | |
| 565 | + } | |
| 566 | + | |
| 567 | + $post = $block_editor_context->post; | |
| 568 | + | |
| 569 | + if ( wp_is_post_type_collaboration_disabled( $post->post_type ) ) { | |
| 570 | + return $settings; | |
| 571 | + } | |
| 572 | + | |
| 573 | + $autosave = wp_get_post_autosave( $post->ID ); | |
| 574 | + | |
| 575 | + if ( ! $autosave ) { | |
| 576 | + return $settings; | |
| 577 | + } | |
| 578 | + | |
| 579 | + $snapshot = get_post_meta( $autosave->ID, Gutenberg_REST_Autosaves_Controller::CRDT_SNAPSHOT_META_KEY, true ); | |
| 580 | + | |
| 581 | + /* | |
| 582 | + * Snapshots can be missing from a pre-collaboration autosave, classic editor autosave, | |
| 583 | + * and other paths. The worst case is a "more recent autosave" notice when newer CRDT | |
| 584 | + * content is already present in the shared document. | |
| 585 | + */ | |
| 586 | + if ( ! is_string( $snapshot ) || '' === $snapshot ) { | |
| 587 | + return $settings; | |
| 588 | + } | |
| 589 | + | |
| 590 | + $settings['autosave']['crdtSnapshot'] = $snapshot; | |
| 591 | + | |
| 592 | + return $settings; | |
| 593 | +} | |
| 594 | +add_filter( 'block_editor_settings_all', 'gutenberg_add_autosave_details_to_editor_settings', 10, 2 ); | |