Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
94.79% |
200 / 211 |
|
75.00% |
18 / 24 |
CRAP | |
0.00% |
0 / 1 |
| RTC | |
94.79% |
200 / 211 |
|
75.00% |
18 / 24 |
72.73 | |
0.00% |
0 / 1 |
| init | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
3.00 | |||
| is_allowed | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| is_turned_on | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| is_enabled | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
5 | |||
| uses_experiment | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| get_providers | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
3 | |||
| register_rest_routes | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| register_providers | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
6 | |||
| unregister_rtc_setting | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| filter_rtc_option | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| default_rtc_option | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| pre_rtc_option | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
4.59 | |||
| carry_over_opt_in | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| restore_opt_in | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
| had_explicit_opt_in | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| get_stored_option | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
| register_experiment_filters | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| filter_experiments | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| register_rtc_setting | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
3 | |||
| render_rtc_setting_field | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
| get_max_peers_per_room | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| is_plan_owner | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
4.18 | |||
| get_site_slug | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
| load_notices | |
100.00% |
38 / 38 |
|
100.00% |
1 / 1 |
3 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Real-time Collaboration (RTC) websocket transport support. |
| 4 | * |
| 5 | * Extends Gutenberg's RTC feature with PingHub websocket transport |
| 6 | * using the WordPress.com infrastructure. |
| 7 | * |
| 8 | * @package automattic/jetpack-rtc |
| 9 | */ |
| 10 | |
| 11 | namespace Automattic\Jetpack; |
| 12 | |
| 13 | use Automattic\Jetpack\RTC\REST_Connection_Log; |
| 14 | use Automattic\Jetpack\RTC\REST_Pinghub_Token; |
| 15 | use Automattic\Jetpack\RTC\REST_RTC_Notices; |
| 16 | |
| 17 | /** |
| 18 | * Main RTC class. |
| 19 | */ |
| 20 | class RTC { |
| 21 | |
| 22 | const PACKAGE_VERSION = '0.1.0'; |
| 23 | |
| 24 | /** |
| 25 | * Option names for the RTC setting. |
| 26 | * The old name was used until Gutenberg PR #76643 renamed it. |
| 27 | * Both are supported for backwards compatibility. |
| 28 | */ |
| 29 | const OPTION_OLD = 'wp_enable_real_time_collaboration'; |
| 30 | const OPTION_NEW = 'wp_collaboration_enabled'; |
| 31 | |
| 32 | /** |
| 33 | * The Gutenberg experiment that gates real-time collaboration. |
| 34 | * |
| 35 | * Added by Gutenberg PR #80658 (Gutenberg 23.8), which moved collaboration and its |
| 36 | * bundled HTTP polling provider behind a single experiment. |
| 37 | */ |
| 38 | const EXPERIMENT = 'gutenberg-real-time-collaboration'; |
| 39 | |
| 40 | /** |
| 41 | * The option Gutenberg stores its experiments in. |
| 42 | */ |
| 43 | const EXPERIMENTS_OPTION = 'gutenberg-experiments'; |
| 44 | |
| 45 | /** |
| 46 | * Records whether this site had explicitly opted into collaboration before the |
| 47 | * Gutenberg experiment existed. |
| 48 | * |
| 49 | * '1' when it had, '0' when it had not. Absent until we have looked. |
| 50 | */ |
| 51 | const OPTION_PRE_EXPERIMENT_OPT_IN = 'jetpack_rtc_pre_experiment_opt_in'; |
| 52 | |
| 53 | /** |
| 54 | * Whether the hooks have been initialized. |
| 55 | * |
| 56 | * @var bool |
| 57 | */ |
| 58 | private static $initialized = false; |
| 59 | |
| 60 | /** |
| 61 | * Initialize the RTC package by registering hooks. |
| 62 | * |
| 63 | * @return void |
| 64 | */ |
| 65 | public static function init() { |
| 66 | if ( self::$initialized ) { |
| 67 | return; |
| 68 | } |
| 69 | self::$initialized = true; |
| 70 | |
| 71 | add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) ); |
| 72 | add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'register_providers' ) ); |
| 73 | add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'load_notices' ) ); |
| 74 | add_action( 'load-options-writing.php', array( __CLASS__, 'unregister_rtc_setting' ) ); |
| 75 | |
| 76 | /* |
| 77 | * Gutenberg 23.8+ only. Both callbacks no-op on older Gutenberg, and the |
| 78 | * experiment filters are registered on `init` rather than here because |
| 79 | * resolving `is_allowed()` runs the `jetpack_rtc_enabled` filter, whose |
| 80 | * WP.com callbacks are not dependable while plugins are still loading. |
| 81 | */ |
| 82 | add_action( 'init', array( __CLASS__, 'carry_over_opt_in' ), 0 ); |
| 83 | add_action( 'init', array( __CLASS__, 'register_experiment_filters' ), 1 ); |
| 84 | // Priority 30 so it lands after Gutenberg's migration deletes the option at 20. |
| 85 | add_action( 'init', array( __CLASS__, 'restore_opt_in' ), 30 ); |
| 86 | add_action( 'admin_init', array( __CLASS__, 'register_rtc_setting' ) ); |
| 87 | |
| 88 | // Hook into both old and new option names for backwards compatibility. |
| 89 | foreach ( array( self::OPTION_OLD, self::OPTION_NEW ) as $option ) { |
| 90 | add_filter( 'option_' . $option, array( __CLASS__, 'filter_rtc_option' ), 10 ); |
| 91 | add_filter( 'default_option_' . $option, array( __CLASS__, 'default_rtc_option' ), 20, 2 ); |
| 92 | add_filter( 'pre_option_' . $option, array( __CLASS__, 'pre_rtc_option' ) ); |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Determine whether RTC is allowed. |
| 98 | * |
| 99 | * @return bool |
| 100 | */ |
| 101 | public static function is_allowed() { |
| 102 | /** |
| 103 | * Filter whether RTC can be enabled. |
| 104 | * |
| 105 | * @param bool $is_enabled Whether RTC can be enabled. |
| 106 | */ |
| 107 | return apply_filters( 'jetpack_rtc_enabled', false ); |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Determine whether RTC is allowed and switched on for this site. |
| 112 | * |
| 113 | * Unlike is_enabled(), this ignores the current admin screen, so it describes the |
| 114 | * site's configuration rather than the current request. The experiment gate uses |
| 115 | * it for that reason: registering the sync storage post type and REST routes must |
| 116 | * not depend on which admin page happens to be loading. |
| 117 | * |
| 118 | * @return bool |
| 119 | */ |
| 120 | public static function is_turned_on() { |
| 121 | return (bool) get_option( self::OPTION_NEW ) && self::is_allowed(); |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Determine whether RTC is enabled. |
| 126 | * |
| 127 | * @return bool |
| 128 | */ |
| 129 | public static function is_enabled() { |
| 130 | global $pagenow; |
| 131 | |
| 132 | // Real-time collaboration is not enabled in the site editor. |
| 133 | if ( |
| 134 | 'site-editor.php' === $pagenow || |
| 135 | ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'site-editor-v2' === sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 136 | ) { |
| 137 | return false; |
| 138 | } |
| 139 | |
| 140 | return self::is_turned_on(); |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * Determine whether the loaded Gutenberg gates RTC behind the experiment. |
| 145 | * |
| 146 | * Gutenberg PR #80658 (Gutenberg 23.8) repointed `wp_is_collaboration_enabled()` at |
| 147 | * the `gutenberg-real-time-collaboration` experiment, removed |
| 148 | * `wp_is_collaboration_allowed()`, dropped the collaboration field from |
| 149 | * Settings > Writing, and deleted the `wp_collaboration_enabled` option in a |
| 150 | * migration. |
| 151 | * |
| 152 | * This feature-detects that pair of functions instead of comparing version numbers, |
| 153 | * because what matters is the shape of the API rather than the release string, and |
| 154 | * WordPress.com can load `dev` and `-fuzz` builds whose versions do not sort |
| 155 | * meaningfully. |
| 156 | * |
| 157 | * @return bool |
| 158 | */ |
| 159 | public static function uses_experiment() { |
| 160 | $uses_experiment = function_exists( 'wp_is_collaboration_enabled' ) && ! function_exists( 'wp_is_collaboration_allowed' ); |
| 161 | |
| 162 | /** |
| 163 | * Filter whether RTC is gated by the Gutenberg experiment. |
| 164 | * |
| 165 | * An escape hatch for builds the detection above cannot classify. |
| 166 | * |
| 167 | * @param bool $uses_experiment Whether the loaded Gutenberg gates RTC behind the experiment. |
| 168 | */ |
| 169 | return (bool) apply_filters( 'jetpack_rtc_uses_experiment', $uses_experiment ); |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Get the list of active RTC providers. |
| 174 | * |
| 175 | * @return string[] |
| 176 | */ |
| 177 | public static function get_providers() { |
| 178 | if ( ! self::is_enabled() ) { |
| 179 | return array(); |
| 180 | } |
| 181 | |
| 182 | $allowed_providers = array( 'http-polling', 'pinghub' ); |
| 183 | |
| 184 | $default_providers = array( 'pinghub' ); |
| 185 | |
| 186 | /** |
| 187 | * Filter the list of RTC providers. |
| 188 | * |
| 189 | * @param string[] $providers List of provider identifiers. |
| 190 | */ |
| 191 | $providers = apply_filters( 'jetpack_rtc_providers', $default_providers ); |
| 192 | if ( ! is_array( $providers ) ) { |
| 193 | return array(); |
| 194 | } |
| 195 | |
| 196 | return array_values( |
| 197 | array_filter( |
| 198 | $providers, |
| 199 | function ( $provider ) use ( $allowed_providers ) { |
| 200 | return in_array( $provider, $allowed_providers, true ); |
| 201 | } |
| 202 | ) |
| 203 | ); |
| 204 | } |
| 205 | |
| 206 | /** |
| 207 | * Register REST API routes for the PingHub token endpoint. |
| 208 | * |
| 209 | * @return void |
| 210 | */ |
| 211 | public static function register_rest_routes() { |
| 212 | ( new REST_Pinghub_Token() )->register_routes(); |
| 213 | ( new REST_RTC_Notices() )->register_routes(); |
| 214 | |
| 215 | if ( function_exists( 'log2logstash' ) ) { |
| 216 | ( new REST_Connection_Log() )->register_routes(); |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | /** |
| 221 | * Enqueue the assets that extend the RTC providers. |
| 222 | * |
| 223 | * @return void |
| 224 | */ |
| 225 | public static function register_providers() { |
| 226 | if ( ! self::is_enabled() ) { |
| 227 | return; |
| 228 | } |
| 229 | |
| 230 | $providers = self::get_providers(); |
| 231 | |
| 232 | // If HTTP polling (Gutenberg's built-in default provider when this script isn't enqueued) |
| 233 | // is the only provider being used, then we don't need to inject any assets since that's |
| 234 | // already the default behavior. |
| 235 | if ( count( $providers ) === 1 && in_array( 'http-polling', $providers, true ) ) { |
| 236 | return; |
| 237 | } |
| 238 | |
| 239 | $handle = 'jetpack-rtc-providers'; |
| 240 | |
| 241 | Assets::register_script( |
| 242 | $handle, |
| 243 | '../build/rtc-providers.js', |
| 244 | __FILE__, |
| 245 | array( |
| 246 | 'in_footer' => true, |
| 247 | 'textdomain' => 'jetpack-rtc', |
| 248 | 'enqueue' => true, |
| 249 | ) |
| 250 | ); |
| 251 | |
| 252 | $data = wp_json_encode( |
| 253 | array( |
| 254 | 'providers' => $providers, |
| 255 | 'connectionLogging' => function_exists( 'log2logstash' ), |
| 256 | 'currentPostType' => get_post_type() ? get_post_type() : null, |
| 257 | 'currentPostId' => get_the_ID() ? get_the_ID() : null, |
| 258 | ), |
| 259 | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP |
| 260 | ); |
| 261 | |
| 262 | wp_add_inline_script( |
| 263 | $handle, |
| 264 | "var jetpackRTC = $data;", |
| 265 | 'before' |
| 266 | ); |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Unregister the RTC setting field on the Writing page if RTC is not allowed. |
| 271 | * |
| 272 | * @return void |
| 273 | */ |
| 274 | public static function unregister_rtc_setting() { |
| 275 | if ( self::is_allowed() ) { |
| 276 | return; |
| 277 | } |
| 278 | |
| 279 | global $wp_settings_fields; |
| 280 | |
| 281 | foreach ( array( self::OPTION_OLD, self::OPTION_NEW ) as $option ) { |
| 282 | if ( isset( $wp_settings_fields['writing']['default'][ $option ] ) ) { |
| 283 | unset( $wp_settings_fields['writing']['default'][ $option ] ); |
| 284 | } |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * When RTC is not allowed, always force the option off. |
| 290 | * When RTC is allowed, respect the stored option value. |
| 291 | * |
| 292 | * @param mixed $value The value of the option. |
| 293 | * @return mixed |
| 294 | */ |
| 295 | public static function filter_rtc_option( $value ) { |
| 296 | // An explicitly disabled setting needs no eligibility check. |
| 297 | if ( '0' === $value ) { |
| 298 | return $value; |
| 299 | } |
| 300 | |
| 301 | // RTC not allowed: force the option off, regardless of what's in the DB. |
| 302 | if ( ! self::is_allowed() ) { |
| 303 | return '0'; |
| 304 | } |
| 305 | |
| 306 | // RTC allowed: respect whatever is stored. |
| 307 | return $value; |
| 308 | } |
| 309 | |
| 310 | /** |
| 311 | * Default the collaboration setting to off. |
| 312 | * |
| 313 | * Real-time collaboration is opt-in: Gutenberg 23.8 made it an experiment that sites |
| 314 | * must enable explicitly, and on earlier versions there is no longer a reason to have |
| 315 | * it on by default either. Sites that turned it on have a stored value, which |
| 316 | * `get_option()` returns without ever consulting this filter. |
| 317 | * |
| 318 | * @return string Always '0'. |
| 319 | */ |
| 320 | public static function default_rtc_option() { |
| 321 | return '0'; |
| 322 | } |
| 323 | |
| 324 | /** |
| 325 | * Disable RTC for super admins that are not members of the blog to avoid |
| 326 | * accidentally exposing their presence to site users (e.g. during support). |
| 327 | * Skip this on the Writing settings page so they can still toggle the option. |
| 328 | * |
| 329 | * @return mixed |
| 330 | */ |
| 331 | public static function pre_rtc_option() { |
| 332 | global $pagenow; |
| 333 | |
| 334 | if ( 'options-writing.php' !== $pagenow && is_super_admin() && ! is_user_member_of_blog() ) { |
| 335 | return '0'; |
| 336 | } |
| 337 | |
| 338 | // Returning false to let `get_option` proceed normally. |
| 339 | return false; |
| 340 | } |
| 341 | |
| 342 | /** |
| 343 | * Preserve an explicit opt-in across Gutenberg's collaboration migration. |
| 344 | * |
| 345 | * Gutenberg 23.8's `_gutenberg_migrate_remove_legacy_collaboration_options()` deletes |
| 346 | * `wp_collaboration_enabled` outright on `init` priority 20, without migrating it. For |
| 347 | * sites that never touched the setting that is exactly right — they were only ever on |
| 348 | * because the old default was on. But it also discards the choice of sites that |
| 349 | * deliberately switched collaboration on, and those we want to keep. |
| 350 | * |
| 351 | * The distinction is that the option only exists in the database when something |
| 352 | * actually wrote it: saving Settings > Writing, or a WP-CLI/REST update. An absent |
| 353 | * option means "never chosen", so reading the stored value — rather than the effective |
| 354 | * one — separates the two groups cleanly. |
| 355 | * |
| 356 | * Runs on `init` priority 0, so on the first request under Gutenberg 23.8 we read the |
| 357 | * value before the migration at priority 20 removes it. |
| 358 | * |
| 359 | * Note this only works if this package is deployed before Gutenberg 23.8 reaches the |
| 360 | * site. If the migration runs first the stored values are already gone and there is |
| 361 | * nothing left to carry. |
| 362 | * |
| 363 | * @return void |
| 364 | */ |
| 365 | public static function carry_over_opt_in() { |
| 366 | if ( ! self::uses_experiment() ) { |
| 367 | return; |
| 368 | } |
| 369 | |
| 370 | // Already looked. Recorded as '1' or '0', so anything but false means done. |
| 371 | if ( false !== get_option( self::OPTION_PRE_EXPERIMENT_OPT_IN, false ) ) { |
| 372 | return; |
| 373 | } |
| 374 | |
| 375 | /* |
| 376 | * Record an answer for every site, including ones that cannot run RTC. Writing the |
| 377 | * marker unconditionally is what lets the check above short-circuit on subsequent |
| 378 | * requests; skipping disallowed sites would leave them re-evaluating this on every |
| 379 | * admin request forever. Whether RTC is allowed is still enforced at read time by |
| 380 | * `is_turned_on()`. |
| 381 | */ |
| 382 | update_option( self::OPTION_PRE_EXPERIMENT_OPT_IN, self::had_explicit_opt_in() ? '1' : '0' ); |
| 383 | } |
| 384 | |
| 385 | /** |
| 386 | * Re-apply a carried opt-in after Gutenberg's migration has removed the option. |
| 387 | * |
| 388 | * Deliberately writes a real option value instead of leaning on |
| 389 | * `default_rtc_option()`. Unchecking the box on Settings > Writing does not store a |
| 390 | * '0' — it removes the row entirely — so anything expressed as a default would |
| 391 | * re-apply itself on the next request and make the setting impossible to switch off. |
| 392 | * |
| 393 | * The carried flag is consumed here so this runs once. From then on the stored option |
| 394 | * is the only thing that decides, exactly as it is for a site that never opted in. |
| 395 | * |
| 396 | * @return void |
| 397 | */ |
| 398 | public static function restore_opt_in() { |
| 399 | if ( ! self::uses_experiment() ) { |
| 400 | return; |
| 401 | } |
| 402 | |
| 403 | if ( '1' !== get_option( self::OPTION_PRE_EXPERIMENT_OPT_IN ) ) { |
| 404 | return; |
| 405 | } |
| 406 | |
| 407 | // Consume first, so a failure below cannot leave this re-applying every request. |
| 408 | update_option( self::OPTION_PRE_EXPERIMENT_OPT_IN, '0' ); |
| 409 | |
| 410 | // Only restore into the gap the migration left. Never overwrite a real choice. |
| 411 | if ( null === self::get_stored_option( self::OPTION_NEW ) ) { |
| 412 | update_option( self::OPTION_NEW, '1' ); |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | /** |
| 417 | * Whether collaboration was explicitly switched on before the experiment existed. |
| 418 | * |
| 419 | * @return bool |
| 420 | */ |
| 421 | private static function had_explicit_opt_in() { |
| 422 | foreach ( array( self::OPTION_NEW, self::OPTION_OLD ) as $option ) { |
| 423 | $stored = self::get_stored_option( $option ); |
| 424 | |
| 425 | if ( null !== $stored ) { |
| 426 | // (bool) '0' is false, so a stored opt-out correctly reads as no opt-in. |
| 427 | return (bool) $stored; |
| 428 | } |
| 429 | } |
| 430 | |
| 431 | return false; |
| 432 | } |
| 433 | |
| 434 | /** |
| 435 | * Read an option's stored value, or null when nothing is stored. |
| 436 | * |
| 437 | * `get_option()` cannot answer "is this stored?" on its own here, because this class |
| 438 | * filters the option to a value whether or not one exists. Lifting our own filters for |
| 439 | * the duration of the read lets the missing-option default come through untouched. |
| 440 | * |
| 441 | * @param string $option Option name. |
| 442 | * @return mixed The stored value, or null when the option does not exist. |
| 443 | */ |
| 444 | private static function get_stored_option( $option ) { |
| 445 | $filters = array( |
| 446 | array( 'pre_option_' . $option, 'pre_rtc_option', 10, 1 ), |
| 447 | array( 'option_' . $option, 'filter_rtc_option', 10, 1 ), |
| 448 | array( 'default_option_' . $option, 'default_rtc_option', 20, 2 ), |
| 449 | ); |
| 450 | |
| 451 | // Restore only what was actually hooked, so this cannot add filters of its own. |
| 452 | $hooked = array(); |
| 453 | foreach ( $filters as $filter ) { |
| 454 | list( $hook, $method, $priority ) = $filter; |
| 455 | |
| 456 | $hooked[ $hook ] = has_filter( $hook, array( __CLASS__, $method ) ) === $priority; |
| 457 | if ( $hooked[ $hook ] ) { |
| 458 | remove_filter( $hook, array( __CLASS__, $method ), $priority ); |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | $stored = get_option( $option, null ); |
| 463 | |
| 464 | foreach ( $filters as $filter ) { |
| 465 | list( $hook, $method, $priority, $args ) = $filter; |
| 466 | |
| 467 | if ( $hooked[ $hook ] ) { |
| 468 | add_filter( $hook, array( __CLASS__, $method ), $priority, $args ); |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | return $stored; |
| 473 | } |
| 474 | |
| 475 | /** |
| 476 | * Register the filters that drive the Gutenberg RTC experiment. |
| 477 | * |
| 478 | * Runs on `init` so that resolving `is_allowed()` — which fires the |
| 479 | * `jetpack_rtc_enabled` filter, and on WP.com reads site features and stickers — |
| 480 | * happens after plugins have finished loading. Everything in Gutenberg that reads |
| 481 | * the experiment does so on `init`, `admin_init`, `rest_api_init` or |
| 482 | * `block_editor_settings_all`, all of which run later. |
| 483 | * |
| 484 | * @return void |
| 485 | */ |
| 486 | public static function register_experiment_filters() { |
| 487 | if ( ! self::uses_experiment() ) { |
| 488 | return; |
| 489 | } |
| 490 | |
| 491 | // 'option_' fires when the option exists in the DB, 'default_option_' when it does not. |
| 492 | add_filter( 'option_' . self::EXPERIMENTS_OPTION, array( __CLASS__, 'filter_experiments' ) ); |
| 493 | |
| 494 | /* |
| 495 | * Priority 20 because Gutenberg registers `gutenberg-experiments` with a default |
| 496 | * of array() on `rest_api_init`. register_setting() hooks core's |
| 497 | * filter_default_option() onto `default_option_gutenberg-experiments` at priority |
| 498 | * 10, and that callback ignores the value it is handed and returns the registered |
| 499 | * default. At the same priority ours runs first and its result is thrown away, so |
| 500 | * the experiment would read as disabled on every REST request no matter what the |
| 501 | * setting says. Running after core's callback is the same reason default_rtc_option |
| 502 | * is registered at 20. |
| 503 | */ |
| 504 | add_filter( 'default_option_' . self::EXPERIMENTS_OPTION, array( __CLASS__, 'filter_experiments' ), 20 ); |
| 505 | } |
| 506 | |
| 507 | /** |
| 508 | * Toggle the RTC experiment to match the site's collaboration setting. |
| 509 | * |
| 510 | * Keeping the setting as the single source of truth avoids the "collaboration on, |
| 511 | * no provider registered" state that Gutenberg PR #80658 was written to eliminate. |
| 512 | * |
| 513 | * @param mixed $experiments The `gutenberg-experiments` option value. |
| 514 | * @return array The experiments, with RTC toggled to match the setting. |
| 515 | */ |
| 516 | public static function filter_experiments( $experiments ) { |
| 517 | if ( ! is_array( $experiments ) ) { |
| 518 | $experiments = array(); |
| 519 | } |
| 520 | |
| 521 | if ( self::is_turned_on() ) { |
| 522 | $experiments[ self::EXPERIMENT ] = true; |
| 523 | } else { |
| 524 | // Unset rather than leave alone, so the setting still wins if the experiment |
| 525 | // was enabled directly through the Gutenberg Experiments page. |
| 526 | unset( $experiments[ self::EXPERIMENT ] ); |
| 527 | } |
| 528 | |
| 529 | return $experiments; |
| 530 | } |
| 531 | |
| 532 | /** |
| 533 | * Register the collaboration setting on Settings > Writing. |
| 534 | * |
| 535 | * Gutenberg 23.8+ removed its own field, leaving the Experiments page as the only |
| 536 | * way to turn RTC on. That page is hidden on WP.com Simple sites, and asking people |
| 537 | * to toggle an experiment is the wrong level of exposure for a hosted product, so we |
| 538 | * register a replacement. The wording matches the checkbox Gutenberg removed. |
| 539 | * |
| 540 | * @return void |
| 541 | */ |
| 542 | public static function register_rtc_setting() { |
| 543 | if ( ! self::uses_experiment() || ! self::is_allowed() ) { |
| 544 | return; |
| 545 | } |
| 546 | |
| 547 | register_setting( |
| 548 | 'writing', |
| 549 | self::OPTION_NEW, |
| 550 | array( |
| 551 | 'type' => 'boolean', |
| 552 | 'description' => __( 'Enable Real-Time Collaboration', 'jetpack-rtc' ), |
| 553 | 'sanitize_callback' => 'rest_sanitize_boolean', |
| 554 | 'default' => false, |
| 555 | 'show_in_rest' => true, |
| 556 | ) |
| 557 | ); |
| 558 | |
| 559 | add_settings_field( |
| 560 | self::OPTION_NEW, |
| 561 | __( 'Collaboration', 'jetpack-rtc' ), |
| 562 | array( __CLASS__, 'render_rtc_setting_field' ), |
| 563 | 'writing' |
| 564 | ); |
| 565 | } |
| 566 | |
| 567 | /** |
| 568 | * Render the collaboration checkbox on Settings > Writing. |
| 569 | * |
| 570 | * @return void |
| 571 | */ |
| 572 | public static function render_rtc_setting_field() { |
| 573 | ?> |
| 574 | <label for="<?php echo esc_attr( self::OPTION_NEW ); ?>"> |
| 575 | <input |
| 576 | name="<?php echo esc_attr( self::OPTION_NEW ); ?>" |
| 577 | type="checkbox" |
| 578 | id="<?php echo esc_attr( self::OPTION_NEW ); ?>" |
| 579 | value="1" |
| 580 | <?php checked( (bool) get_option( self::OPTION_NEW ) ); ?> |
| 581 | /> |
| 582 | <?php esc_html_e( "Enable early access to real-time collaboration. Real-time collaboration may affect your website's performance.", 'jetpack-rtc' ); ?> |
| 583 | </label> |
| 584 | <?php |
| 585 | } |
| 586 | |
| 587 | /** |
| 588 | * Get the maximum number of peers allowed per room. |
| 589 | * |
| 590 | * @return int Max peers per room. |
| 591 | */ |
| 592 | public static function get_max_peers_per_room() { |
| 593 | return (int) apply_filters( 'jetpack_rtc_max_peers_per_room', 3 ); |
| 594 | } |
| 595 | |
| 596 | /** |
| 597 | * Check if the current user is the plan owner for this site. |
| 598 | * Works on Simple sites (via wpcom_get_blog_owner) and Atomic sites |
| 599 | * (via Jetpack connection master_user). Returns false on self-hosted |
| 600 | * since there is no WP.com plan to upgrade. |
| 601 | * |
| 602 | * @return bool |
| 603 | */ |
| 604 | public static function is_plan_owner() { |
| 605 | $current_user_id = get_current_user_id(); |
| 606 | |
| 607 | // Simple sites: wpcom_get_blog_owner is the canonical source. |
| 608 | if ( function_exists( 'wpcom_get_blog_owner' ) ) { |
| 609 | $owner_id = wpcom_get_blog_owner( get_wpcom_blog_id() ); |
| 610 | return (int) $current_user_id === (int) $owner_id; |
| 611 | } |
| 612 | |
| 613 | // Atomic sites: the Jetpack connection master_user is the plan owner. |
| 614 | if ( class_exists( 'Jetpack_Options' ) ) { |
| 615 | $master_user = \Jetpack_Options::get_option( 'master_user' ); |
| 616 | if ( $master_user ) { |
| 617 | return (int) $current_user_id === (int) $master_user; |
| 618 | } |
| 619 | } |
| 620 | |
| 621 | return false; |
| 622 | } |
| 623 | |
| 624 | /** |
| 625 | * Get the site slug for use in WP.com URLs. |
| 626 | * |
| 627 | * @return string |
| 628 | */ |
| 629 | private static function get_site_slug() { |
| 630 | if ( function_exists( 'wpcom_get_site_slug' ) ) { |
| 631 | return wpcom_get_site_slug(); |
| 632 | } |
| 633 | |
| 634 | if ( class_exists( '\Automattic\Jetpack\Status' ) ) { |
| 635 | $jetpack_status = new \Automattic\Jetpack\Status(); |
| 636 | return $jetpack_status->get_site_suffix(); |
| 637 | } |
| 638 | |
| 639 | return ''; |
| 640 | } |
| 641 | |
| 642 | /** |
| 643 | * Enqueue the assets that handle the RTC notices. |
| 644 | * |
| 645 | * @return void |
| 646 | */ |
| 647 | public static function load_notices() { |
| 648 | if ( ! self::is_enabled() ) { |
| 649 | return; |
| 650 | } |
| 651 | |
| 652 | $handle = 'jetpack-rtc-notices'; |
| 653 | |
| 654 | Assets::register_script( |
| 655 | $handle, |
| 656 | '../build/rtc-notices.js', |
| 657 | __FILE__, |
| 658 | array( |
| 659 | 'in_footer' => true, |
| 660 | 'textdomain' => 'jetpack-rtc', |
| 661 | 'enqueue' => true, |
| 662 | ) |
| 663 | ); |
| 664 | |
| 665 | $is_admin_user = current_user_can( 'manage_options' ); |
| 666 | $is_plan_owner = self::is_plan_owner(); |
| 667 | $post_type = get_post_type(); |
| 668 | |
| 669 | $data = wp_json_encode( |
| 670 | array( |
| 671 | 'assetsUrl' => plugins_url( '../build/', __FILE__ ), |
| 672 | 'isAdmin' => $is_admin_user, |
| 673 | 'isPlanOwner' => $is_plan_owner, |
| 674 | 'postId' => get_the_ID(), |
| 675 | 'postType' => $post_type ? $post_type : null, |
| 676 | 'userId' => get_current_user_id(), |
| 677 | 'postTitle' => get_the_title(), |
| 678 | 'postEditUrl' => get_edit_post_link( get_the_ID(), 'raw' ), |
| 679 | 'postsListUrl' => admin_url( 'edit.php' ), |
| 680 | 'siteSlug' => self::get_site_slug(), |
| 681 | 'maxPeersPerRoom' => self::get_max_peers_per_room(), |
| 682 | 'enableLimitNotices' => apply_filters( 'jetpack_rtc_enable_limit_notices', false ), |
| 683 | ), |
| 684 | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP |
| 685 | ); |
| 686 | |
| 687 | wp_add_inline_script( |
| 688 | $handle, |
| 689 | "var jetpackRtcNotices = $data;", |
| 690 | 'before' |
| 691 | ); |
| 692 | } |
| 693 | } |