Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.79% covered (success)
94.79%
200 / 211
75.00% covered (warning)
75.00%
18 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
RTC
94.79% covered (success)
94.79%
200 / 211
75.00% covered (warning)
75.00%
18 / 24
72.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%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 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 (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}