Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.74% covered (success)
94.74%
198 / 209
75.00% covered (warning)
75.00%
18 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
RTC
94.74% covered (success)
94.74%
198 / 209
75.00% covered (warning)
75.00%
18 / 24
71.73
0.00% covered (danger)
0.00%
0 / 1
 init
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
3.00
 is_allowed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_turned_on
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_enabled
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 uses_experiment
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 get_providers
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 register_rest_routes
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 register_providers
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
6
 unregister_rtc_setting
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 filter_rtc_option
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 default_rtc_option
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pre_rtc_option
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
4.59
 carry_over_opt_in
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 restore_opt_in
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 had_explicit_opt_in
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 get_stored_option
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 register_experiment_filters
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 filter_experiments
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 register_rtc_setting
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 render_rtc_setting_field
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 get_max_peers_per_room
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_plan_owner
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 get_site_slug
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 load_notices
100.00% covered (success)
100.00%
38 / 38
100.00% covered (success)
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
11namespace Automattic\Jetpack;
12
13use Automattic\Jetpack\RTC\REST_Connection_Log;
14use Automattic\Jetpack\RTC\REST_Pinghub_Token;
15use Automattic\Jetpack\RTC\REST_RTC_Notices;
16
17/**
18 * Main RTC class.
19 */
20class 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 self::is_allowed() && (bool) get_option( self::OPTION_NEW );
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        // RTC not allowed: force the option off, regardless of what's in the DB.
297        if ( ! self::is_allowed() ) {
298            return '0';
299        }
300
301        // RTC allowed: respect whatever is stored.
302        return $value;
303    }
304
305    /**
306     * Default the collaboration setting to off.
307     *
308     * Real-time collaboration is opt-in: Gutenberg 23.8 made it an experiment that sites
309     * must enable explicitly, and on earlier versions there is no longer a reason to have
310     * it on by default either. Sites that turned it on have a stored value, which
311     * `get_option()` returns without ever consulting this filter.
312     *
313     * @return string Always '0'.
314     */
315    public static function default_rtc_option() {
316        return '0';
317    }
318
319    /**
320     * Disable RTC for super admins that are not members of the blog to avoid
321     * accidentally exposing their presence to site users (e.g. during support).
322     * Skip this on the Writing settings page so they can still toggle the option.
323     *
324     * @return mixed
325     */
326    public static function pre_rtc_option() {
327        global $pagenow;
328
329        if ( 'options-writing.php' !== $pagenow && is_super_admin() && ! is_user_member_of_blog() ) {
330            return '0';
331        }
332
333        // Returning false to let `get_option` proceed normally.
334        return false;
335    }
336
337    /**
338     * Preserve an explicit opt-in across Gutenberg's collaboration migration.
339     *
340     * Gutenberg 23.8's `_gutenberg_migrate_remove_legacy_collaboration_options()` deletes
341     * `wp_collaboration_enabled` outright on `init` priority 20, without migrating it. For
342     * sites that never touched the setting that is exactly right â€” they were only ever on
343     * because the old default was on. But it also discards the choice of sites that
344     * deliberately switched collaboration on, and those we want to keep.
345     *
346     * The distinction is that the option only exists in the database when something
347     * actually wrote it: saving Settings > Writing, or a WP-CLI/REST update. An absent
348     * option means "never chosen", so reading the stored value â€” rather than the effective
349     * one â€” separates the two groups cleanly.
350     *
351     * Runs on `init` priority 0, so on the first request under Gutenberg 23.8 we read the
352     * value before the migration at priority 20 removes it.
353     *
354     * Note this only works if this package is deployed before Gutenberg 23.8 reaches the
355     * site. If the migration runs first the stored values are already gone and there is
356     * nothing left to carry.
357     *
358     * @return void
359     */
360    public static function carry_over_opt_in() {
361        if ( ! self::uses_experiment() ) {
362            return;
363        }
364
365        // Already looked. Recorded as '1' or '0', so anything but false means done.
366        if ( false !== get_option( self::OPTION_PRE_EXPERIMENT_OPT_IN, false ) ) {
367            return;
368        }
369
370        /*
371         * Record an answer for every site, including ones that cannot run RTC. Writing the
372         * marker unconditionally is what lets the check above short-circuit on subsequent
373         * requests; skipping disallowed sites would leave them re-evaluating this on every
374         * admin request forever. Whether RTC is allowed is still enforced at read time by
375         * `is_turned_on()`.
376         */
377        update_option( self::OPTION_PRE_EXPERIMENT_OPT_IN, self::had_explicit_opt_in() ? '1' : '0' );
378    }
379
380    /**
381     * Re-apply a carried opt-in after Gutenberg's migration has removed the option.
382     *
383     * Deliberately writes a real option value instead of leaning on
384     * `default_rtc_option()`. Unchecking the box on Settings > Writing does not store a
385     * '0' â€” it removes the row entirely â€” so anything expressed as a default would
386     * re-apply itself on the next request and make the setting impossible to switch off.
387     *
388     * The carried flag is consumed here so this runs once. From then on the stored option
389     * is the only thing that decides, exactly as it is for a site that never opted in.
390     *
391     * @return void
392     */
393    public static function restore_opt_in() {
394        if ( ! self::uses_experiment() ) {
395            return;
396        }
397
398        if ( '1' !== get_option( self::OPTION_PRE_EXPERIMENT_OPT_IN ) ) {
399            return;
400        }
401
402        // Consume first, so a failure below cannot leave this re-applying every request.
403        update_option( self::OPTION_PRE_EXPERIMENT_OPT_IN, '0' );
404
405        // Only restore into the gap the migration left. Never overwrite a real choice.
406        if ( null === self::get_stored_option( self::OPTION_NEW ) ) {
407            update_option( self::OPTION_NEW, '1' );
408        }
409    }
410
411    /**
412     * Whether collaboration was explicitly switched on before the experiment existed.
413     *
414     * @return bool
415     */
416    private static function had_explicit_opt_in() {
417        foreach ( array( self::OPTION_NEW, self::OPTION_OLD ) as $option ) {
418            $stored = self::get_stored_option( $option );
419
420            if ( null !== $stored ) {
421                // (bool) '0' is false, so a stored opt-out correctly reads as no opt-in.
422                return (bool) $stored;
423            }
424        }
425
426        return false;
427    }
428
429    /**
430     * Read an option's stored value, or null when nothing is stored.
431     *
432     * `get_option()` cannot answer "is this stored?" on its own here, because this class
433     * filters the option to a value whether or not one exists. Lifting our own filters for
434     * the duration of the read lets the missing-option default come through untouched.
435     *
436     * @param string $option Option name.
437     * @return mixed The stored value, or null when the option does not exist.
438     */
439    private static function get_stored_option( $option ) {
440        $filters = array(
441            array( 'pre_option_' . $option, 'pre_rtc_option', 10, 1 ),
442            array( 'option_' . $option, 'filter_rtc_option', 10, 1 ),
443            array( 'default_option_' . $option, 'default_rtc_option', 20, 2 ),
444        );
445
446        // Restore only what was actually hooked, so this cannot add filters of its own.
447        $hooked = array();
448        foreach ( $filters as $filter ) {
449            list( $hook, $method, $priority ) = $filter;
450
451            $hooked[ $hook ] = has_filter( $hook, array( __CLASS__, $method ) ) === $priority;
452            if ( $hooked[ $hook ] ) {
453                remove_filter( $hook, array( __CLASS__, $method ), $priority );
454            }
455        }
456
457        $stored = get_option( $option, null );
458
459        foreach ( $filters as $filter ) {
460            list( $hook, $method, $priority, $args ) = $filter;
461
462            if ( $hooked[ $hook ] ) {
463                add_filter( $hook, array( __CLASS__, $method ), $priority, $args );
464            }
465        }
466
467        return $stored;
468    }
469
470    /**
471     * Register the filters that drive the Gutenberg RTC experiment.
472     *
473     * Runs on `init` so that resolving `is_allowed()` â€” which fires the
474     * `jetpack_rtc_enabled` filter, and on WP.com reads site features and stickers â€”
475     * happens after plugins have finished loading. Everything in Gutenberg that reads
476     * the experiment does so on `init`, `admin_init`, `rest_api_init` or
477     * `block_editor_settings_all`, all of which run later.
478     *
479     * @return void
480     */
481    public static function register_experiment_filters() {
482        if ( ! self::uses_experiment() ) {
483            return;
484        }
485
486        // 'option_' fires when the option exists in the DB, 'default_option_' when it does not.
487        add_filter( 'option_' . self::EXPERIMENTS_OPTION, array( __CLASS__, 'filter_experiments' ) );
488
489        /*
490         * Priority 20 because Gutenberg registers `gutenberg-experiments` with a default
491         * of array() on `rest_api_init`. register_setting() hooks core's
492         * filter_default_option() onto `default_option_gutenberg-experiments` at priority
493         * 10, and that callback ignores the value it is handed and returns the registered
494         * default. At the same priority ours runs first and its result is thrown away, so
495         * the experiment would read as disabled on every REST request no matter what the
496         * setting says. Running after core's callback is the same reason default_rtc_option
497         * is registered at 20.
498         */
499        add_filter( 'default_option_' . self::EXPERIMENTS_OPTION, array( __CLASS__, 'filter_experiments' ), 20 );
500    }
501
502    /**
503     * Toggle the RTC experiment to match the site's collaboration setting.
504     *
505     * Keeping the setting as the single source of truth avoids the "collaboration on,
506     * no provider registered" state that Gutenberg PR #80658 was written to eliminate.
507     *
508     * @param mixed $experiments The `gutenberg-experiments` option value.
509     * @return array The experiments, with RTC toggled to match the setting.
510     */
511    public static function filter_experiments( $experiments ) {
512        if ( ! is_array( $experiments ) ) {
513            $experiments = array();
514        }
515
516        if ( self::is_turned_on() ) {
517            $experiments[ self::EXPERIMENT ] = true;
518        } else {
519            // Unset rather than leave alone, so the setting still wins if the experiment
520            // was enabled directly through the Gutenberg Experiments page.
521            unset( $experiments[ self::EXPERIMENT ] );
522        }
523
524        return $experiments;
525    }
526
527    /**
528     * Register the collaboration setting on Settings > Writing.
529     *
530     * Gutenberg 23.8+ removed its own field, leaving the Experiments page as the only
531     * way to turn RTC on. That page is hidden on WP.com Simple sites, and asking people
532     * to toggle an experiment is the wrong level of exposure for a hosted product, so we
533     * register a replacement. The wording matches the checkbox Gutenberg removed.
534     *
535     * @return void
536     */
537    public static function register_rtc_setting() {
538        if ( ! self::uses_experiment() || ! self::is_allowed() ) {
539            return;
540        }
541
542        register_setting(
543            'writing',
544            self::OPTION_NEW,
545            array(
546                'type'              => 'boolean',
547                'description'       => __( 'Enable Real-Time Collaboration', 'jetpack-rtc' ),
548                'sanitize_callback' => 'rest_sanitize_boolean',
549                'default'           => false,
550                'show_in_rest'      => true,
551            )
552        );
553
554        add_settings_field(
555            self::OPTION_NEW,
556            __( 'Collaboration', 'jetpack-rtc' ),
557            array( __CLASS__, 'render_rtc_setting_field' ),
558            'writing'
559        );
560    }
561
562    /**
563     * Render the collaboration checkbox on Settings > Writing.
564     *
565     * @return void
566     */
567    public static function render_rtc_setting_field() {
568        ?>
569        <label for="<?php echo esc_attr( self::OPTION_NEW ); ?>">
570            <input
571                name="<?php echo esc_attr( self::OPTION_NEW ); ?>"
572                type="checkbox"
573                id="<?php echo esc_attr( self::OPTION_NEW ); ?>"
574                value="1"
575                <?php checked( (bool) get_option( self::OPTION_NEW ) ); ?>
576            />
577            <?php esc_html_e( "Enable early access to real-time collaboration. Real-time collaboration may affect your website's performance.", 'jetpack-rtc' ); ?>
578        </label>
579        <?php
580    }
581
582    /**
583     * Get the maximum number of peers allowed per room.
584     *
585     * @return int Max peers per room.
586     */
587    public static function get_max_peers_per_room() {
588        return (int) apply_filters( 'jetpack_rtc_max_peers_per_room', 3 );
589    }
590
591    /**
592     * Check if the current user is the plan owner for this site.
593     * Works on Simple sites (via wpcom_get_blog_owner) and Atomic sites
594     * (via Jetpack connection master_user). Returns false on self-hosted
595     * since there is no WP.com plan to upgrade.
596     *
597     * @return bool
598     */
599    public static function is_plan_owner() {
600        $current_user_id = get_current_user_id();
601
602        // Simple sites: wpcom_get_blog_owner is the canonical source.
603        if ( function_exists( 'wpcom_get_blog_owner' ) ) {
604            $owner_id = wpcom_get_blog_owner( get_wpcom_blog_id() );
605            return (int) $current_user_id === (int) $owner_id;
606        }
607
608        // Atomic sites: the Jetpack connection master_user is the plan owner.
609        if ( class_exists( 'Jetpack_Options' ) ) {
610            $master_user = \Jetpack_Options::get_option( 'master_user' );
611            if ( $master_user ) {
612                return (int) $current_user_id === (int) $master_user;
613            }
614        }
615
616        return false;
617    }
618
619    /**
620     * Get the site slug for use in WP.com URLs.
621     *
622     * @return string
623     */
624    private static function get_site_slug() {
625        if ( function_exists( 'wpcom_get_site_slug' ) ) {
626            return wpcom_get_site_slug();
627        }
628
629        if ( class_exists( '\Automattic\Jetpack\Status' ) ) {
630            $jetpack_status = new \Automattic\Jetpack\Status();
631            return $jetpack_status->get_site_suffix();
632        }
633
634        return '';
635    }
636
637    /**
638     * Enqueue the assets that handle the RTC notices.
639     *
640     * @return void
641     */
642    public static function load_notices() {
643        if ( ! self::is_enabled() ) {
644            return;
645        }
646
647        $handle = 'jetpack-rtc-notices';
648
649        Assets::register_script(
650            $handle,
651            '../build/rtc-notices.js',
652            __FILE__,
653            array(
654                'in_footer'  => true,
655                'textdomain' => 'jetpack-rtc',
656                'enqueue'    => true,
657            )
658        );
659
660        $is_admin_user = current_user_can( 'manage_options' );
661        $is_plan_owner = self::is_plan_owner();
662        $post_type     = get_post_type();
663
664        $data = wp_json_encode(
665            array(
666                'assetsUrl'          => plugins_url( '../build/', __FILE__ ),
667                'isAdmin'            => $is_admin_user,
668                'isPlanOwner'        => $is_plan_owner,
669                'postId'             => get_the_ID(),
670                'postType'           => $post_type ? $post_type : null,
671                'userId'             => get_current_user_id(),
672                'postTitle'          => get_the_title(),
673                'postEditUrl'        => get_edit_post_link( get_the_ID(), 'raw' ),
674                'postsListUrl'       => admin_url( 'edit.php' ),
675                'siteSlug'           => self::get_site_slug(),
676                'maxPeersPerRoom'    => self::get_max_peers_per_room(),
677                'enableLimitNotices' => apply_filters( 'jetpack_rtc_enable_limit_notices', false ),
678            ),
679            JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
680        );
681
682        wp_add_inline_script(
683            $handle,
684            "var jetpackRtcNotices = $data;",
685            'before'
686        );
687    }
688}