Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
9.06% covered (danger)
9.06%
30 / 331
0.00% covered (danger)
0.00%
0 / 14
CRAP
n/a
0 / 0
wpcom_should_limit_global_styles
56.25% covered (warning)
56.25%
9 / 16
0.00% covered (danger)
0.00%
0 / 1
13.36
wpcom_global_styles_enqueue_block_editor_assets
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
42
wpcom_global_styles_enqueue_assets
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_block_global_styles_frontend
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
6.97
wpcom_track_global_styles
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
wpcom_global_styles_in_use_by_wp_global_styles_post
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
wpcom_simple_sites_global_styles_current_user_can_edit_wp_global_styles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
wpcom_global_styles_in_use
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_premium_global_styles_is_site_exempt
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
306
wpcom_should_show_global_styles_admin_bar
21.62% covered (danger)
21.62%
8 / 37
0.00% covered (danger)
0.00%
0 / 1
233.34
wpcom_display_global_styles_notice_admin_bar
2.50% covered (danger)
2.50%
2 / 80
0.00% covered (danger)
0.00%
0 / 1
39.37
wpcom_is_previewing_global_styles
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
wpcom_global_styles_is_previewing_premium_theme_without_premium_plan
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
wpcom_get_global_styles_upsell_plan_slug
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Limit Global Styles on WP.com to paid plans.
4 *
5 * @package automattic/jetpack-mu-wpcom
6 */
7
8use Automattic\Jetpack\Jetpack_Mu_Wpcom;
9use Automattic\Jetpack\Jetpack_Mu_Wpcom\Common;
10use Automattic\Jetpack\Plans;
11
12/**
13 * Checks if Global Styles should be limited on the given site.
14 *
15 * @param  int $blog_id Blog ID.
16 * @return bool Whether Global Styles are limited.
17 */
18function wpcom_should_limit_global_styles( $blog_id = 0 ) {
19    /**
20     * Filter to force a limited Global Styles scenario. Useful for unit testing.
21     *
22     * @param bool $force_limit_global_styles Whether Global Styles are forced to be limited.
23     */
24    $force_limit_global_styles = apply_filters( 'wpcom_force_limit_global_styles', false );
25    if ( $force_limit_global_styles ) {
26        return true;
27    }
28
29    if ( ! $blog_id ) {
30        $blog_id = get_wpcom_blog_id();
31    }
32
33    // Do not limit Global Styles on theme demo sites.
34    if ( wpcom_has_blog_sticker( 'theme-demo-site', $blog_id ) ) {
35        return false;
36    }
37
38    // Do not limit Global Styles on Big Sky free trial sites. Those sites will
39    // have their own paywall to go through.
40    if ( wpcom_has_blog_sticker( 'big-sky-free-trial', $blog_id ) ) {
41        return false;
42    }
43
44    // Do not limit Global Styles if the site's plan grants the feature.
45    if ( wpcom_site_has_feature( WPCOM_Features::GLOBAL_STYLES, $blog_id ) ) {
46        return false;
47    }
48
49    // Do not limit Global Styles on sites created before we made it a paid feature (2022-12-15),
50    // that had already used Global Styles.
51    if ( wpcom_premium_global_styles_is_site_exempt( $blog_id ) ) {
52        return false;
53    }
54
55    // Do not limit Global Styles when live previewing a Premium theme without a Premium plan or higher
56    // because the live preview already shows an upgrade notice, and we avoid duplication.
57    if ( wpcom_global_styles_is_previewing_premium_theme_without_premium_plan( $blog_id ) ) {
58        return false;
59    }
60
61    return true;
62}
63
64/**
65 * Enqueues the WP.com Global Styles scripts and styles for the block editor.
66 *
67 * @return void
68 */
69function wpcom_global_styles_enqueue_block_editor_assets() {
70    if ( ! wpcom_should_limit_global_styles() ) {
71        return;
72    }
73
74    $calypso_domain = 'https://wordpress.com';
75    if (
76        ! empty( $_GET['origin'] ) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
77        in_array(
78            $_GET['origin'], // phpcs:ignore WordPress.Security.NonceVerification.Recommended
79            array(
80                'http://calypso.localhost:3000',
81                'https://wpcalypso.wordpress.com',
82                'https://horizon.wordpress.com',
83            ),
84            true
85        )
86    ) {
87        $calypso_domain = sanitize_text_field( wp_unslash( $_GET['origin'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
88    }
89
90    $site_slug = method_exists( '\WPCOM_Masterbar', 'get_calypso_site_slug' )
91        ? \WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() )
92        : wp_parse_url( home_url( '/' ), PHP_URL_HOST );
93
94    $asset_file = include Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-global-styles-editor/wpcom-global-styles-editor.asset.php';
95
96    wp_enqueue_script(
97        'wpcom-global-styles-editor',
98        plugins_url( 'build/wpcom-global-styles-editor/wpcom-global-styles-editor.js', Jetpack_Mu_Wpcom::BASE_FILE ),
99        $asset_file['dependencies'] ?? array(),
100        $asset_file['version'] ?? filemtime( Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-global-styles-editor/wpcom-global-styles-editor.js' ),
101        true
102    );
103    wp_set_script_translations( 'wpcom-global-styles-editor', 'jetpack-mu-wpcom' );
104
105    Common\wpcom_enqueue_tracking_scripts( 'wpcom-global-styles-editor' );
106
107    $learn_more_about_styles_support_url = 'https://wordpress.com/support/using-styles/#access-to-styles';
108    $learn_more_about_styles_post_id     = 192200;
109    if ( class_exists( 'WPCom_Languages' ) ) {
110        $learn_more_about_styles_post_id = WPCom_Languages::localize_url( $learn_more_about_styles_post_id );
111    }
112
113    $plan_slug   = wpcom_get_global_styles_upsell_plan_slug();
114    $plan_name   = Plans::get_plan_short_name( $plan_slug );
115    $upgrade_url = "$calypso_domain/plans/$site_slug?plan=$plan_slug&feature=style-customization";
116
117    wp_localize_script(
118        'wpcom-global-styles-editor',
119        'wpcomGlobalStyles',
120        array(
121            'upgradeUrl'                 => $upgrade_url,
122            'wpcomBlogId'                => get_wpcom_blog_id(),
123            'planName'                   => $plan_name,
124            'learnMoreAboutStylesUrl'    => $learn_more_about_styles_support_url,
125            'learnMoreAboutStylesPostId' => $learn_more_about_styles_post_id,
126            'hasCustomDesign'            => wpcom_site_has_feature( WPCOM_Features::CUSTOM_DESIGN ),
127        )
128    );
129    wp_enqueue_style(
130        'wpcom-global-styles-editor',
131        plugins_url( 'build/wpcom-global-styles-editor/wpcom-global-styles-editor.css', Jetpack_Mu_Wpcom::BASE_FILE ),
132        array(),
133        filemtime( Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-global-styles-editor/wpcom-global-styles-editor.css' )
134    );
135}
136add_action( 'enqueue_block_editor_assets', 'wpcom_global_styles_enqueue_block_editor_assets' );
137
138/**
139 * Enqueues the WP.com Global Styles scripts and styles for the front end.
140 *
141 * @return void
142 */
143function wpcom_global_styles_enqueue_assets() {
144    if ( ! wpcom_should_show_global_styles_admin_bar() ) {
145        return;
146    }
147
148    $asset_file = include Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-global-styles-frontend/wpcom-global-styles-frontend.asset.php';
149    wp_enqueue_script(
150        'wpcom-global-styles-frontend',
151        plugins_url( 'build/wpcom-global-styles-frontend/wpcom-global-styles-frontend.js', Jetpack_Mu_Wpcom::BASE_FILE ),
152        $asset_file['dependencies'] ?? array(),
153        $asset_file['version'] ?? filemtime( Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-global-styles-frontend/wpcom-global-styles-frontend.js' ),
154        true
155    );
156    wp_add_inline_script(
157        'wpcom-global-styles-frontend',
158        'const launchBarUserData = ' . wp_json_encode(
159            array(
160                'blogId' => get_wpcom_blog_id(),
161            ),
162            JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP
163        ),
164        'before'
165    );
166    Common\wpcom_enqueue_tracking_scripts( 'wpcom-global-styles-frontend' );
167
168    wp_enqueue_style(
169        'wpcom-global-styles-frontend',
170        plugins_url( 'build/wpcom-global-styles-frontend/wpcom-global-styles-frontend.css', Jetpack_Mu_Wpcom::BASE_FILE ),
171        array(),
172        filemtime( Jetpack_Mu_Wpcom::BASE_DIR . 'build/wpcom-global-styles-frontend/wpcom-global-styles-frontend.css' )
173    );
174}
175add_action( 'wp_enqueue_scripts', 'wpcom_global_styles_enqueue_assets' );
176
177/**
178 * Removes the user styles from a site with limited global styles.
179 *
180 * @param WP_Theme_JSON_Data $theme_json Class to access and update the underlying data.
181 * @return WP_Theme_JSON_Data Filtered data.
182 */
183function wpcom_block_global_styles_frontend( $theme_json ) {
184    if ( ! wpcom_should_limit_global_styles() || wpcom_is_previewing_global_styles() ) {
185        return $theme_json;
186    }
187
188    $limited_theme_json = array();
189
190    $theme_json_data = $theme_json->get_data();
191
192    /**
193     * If the site has Custom Design paid addon, we only want to return the CSS part of the styles.
194     */
195    if ( isset( $theme_json_data['styles']['css'] ) && wpcom_site_has_feature( WPCOM_Features::CUSTOM_DESIGN ) ) {
196        $limited_theme_json['styles']['css'] = $theme_json_data['styles']['css'];
197        $limited_theme_json['version']       = $theme_json_data['version'] ?? WP_Theme_JSON::LATEST_SCHEMA;
198    }
199
200    if ( class_exists( 'WP_Theme_JSON_Data' ) ) {
201        return new WP_Theme_JSON_Data( $limited_theme_json, 'custom' );
202    }
203
204    /*
205     * If `WP_Theme_JSON_Data` is missing, then the site is running an old
206     * version of WordPress we cannot block the user styles properly.
207     */
208    return $theme_json;
209}
210add_filter( 'wp_theme_json_data_user', 'wpcom_block_global_styles_frontend' );
211
212/**
213 * Tracks when global styles are updated or reset after the post has actually been saved.
214 *
215 * @param int     $blog_id Blog ID.
216 * @param WP_Post $post    Post data.
217 * @param bool    $updated This value is true if the post existed and was updated.
218 */
219function wpcom_track_global_styles( $blog_id, $post, $updated ) {
220    // If the post isn't updated then we know the gs cpt is being created.
221    $event_name = 'wpcom_core_global_styles_create';
222
223    // These properties are for debugging purposes and should be eventually edited or removed.
224    $event_props = array(
225        'should_limit' => (bool) wpcom_should_limit_global_styles(),
226        'is_simple'    => ! function_exists( 'wpcomsh_record_tracks_event' ),
227        'theme'        => get_stylesheet(),
228    );
229
230    if ( $updated ) {
231        // This is a fragile way of checking if the global styles cpt is being reset, we might need to update this condition in the future.
232        $global_style_keys      = array_keys( json_decode( $post->post_content, true ) ?? array() );
233        $is_empty_global_styles = count( array_diff( $global_style_keys, array( 'version', 'isGlobalStylesUserThemeJSON' ) ) ) === 0;
234
235        // By default, we know that we are at least updating.
236        $event_name = 'wpcom_core_global_styles_customize';
237
238        // If we are updating to empty contents then we know for sure we are resetting the contents.
239        if ( $is_empty_global_styles ) {
240            $event_name = 'wpcom_core_global_styles_reset';
241        }
242    }
243
244    // Invoke the correct function based on the underlying infrastructure.
245    if ( function_exists( 'wpcomsh_record_tracks_event' ) ) {
246        wpcomsh_record_tracks_event( $event_name, $event_props );
247    } elseif ( function_exists( 'require_lib' ) && function_exists( 'tracks_record_event' ) ) {
248        require_lib( 'tracks/client' );
249        tracks_record_event( get_current_user_id(), $event_name, $event_props );
250    }
251
252    // Delegate logging to the underlying infrastructure.
253    do_action( 'global_styles_log', $event_name );
254}
255add_action( 'save_post_wp_global_styles', 'wpcom_track_global_styles', 10, 3 );
256
257/**
258 * Check if a `wp_global_styles` post contains custom Global Styles.
259 *
260 * @param array $wp_global_styles_post The `wp_global_styles` post.
261 * @return bool Whether the post contains custom Global Styles.
262 */
263function wpcom_global_styles_in_use_by_wp_global_styles_post( array $wp_global_styles_post = array() ) {
264    if ( ! isset( $wp_global_styles_post['post_content'] ) ) {
265        return false;
266    }
267
268    $global_styles_content = json_decode( $wp_global_styles_post['post_content'], true ) ?? array();
269
270    // Some keys are ignored because they are not relevant to a custom style
271    // behaviours are not relevant if blank - as they where when included during GB16.4 and later removed.
272    $ignored_keys = array( 'version', 'isGlobalStylesUserThemeJSON' );
273
274    if ( wpcom_site_has_feature( WPCOM_Features::CUSTOM_DESIGN ) ) {
275        unset( $global_styles_content['styles']['css'] );
276    }
277
278    $theme_base_css = WP_Theme_JSON_Resolver::get_theme_data()->get_stylesheet( array( 'custom-css' ) ) ?? '';
279
280    $theme_base_css = preg_replace( '/\s+/', '', $theme_base_css );
281    $custom_css     = preg_replace( '/\s+/', '', $global_styles_content['styles']['css'] ?? '' );
282
283    if ( $theme_base_css === $custom_css || empty( $global_styles_content['styles']['css'] ) ) {
284        unset( $global_styles_content['styles']['css'] );
285    }
286
287    if ( empty( $global_styles_content['styles'] ) ) {
288        unset( $global_styles_content['styles'] );
289    }
290
291    if ( isset( $global_styles_content['behaviors'] ) && empty( $global_styles_content['behaviors'] ) ) {
292        $ignored_keys[] = 'behaviors';
293    }
294
295    $global_style_keys    = array_keys( $global_styles_content );
296    $global_styles_in_use = count( array_diff( $global_style_keys, $ignored_keys ) ) > 0;
297
298    return $global_styles_in_use;
299}
300
301/**
302 * Checks if the current user can edit the `wp_global_styles` post type.
303 *
304 * @param int $blog_id Blog ID.
305 * @return bool Whether the current user can edit the `wp_global_styles` post type.
306 */
307function wpcom_simple_sites_global_styles_current_user_can_edit_wp_global_styles( $blog_id ) {
308    switch_to_blog( $blog_id );
309    $wp_global_styles_cpt = get_post_type_object( 'wp_global_styles' );
310    restore_current_blog();
311    return current_user_can( $wp_global_styles_cpt->cap->publish_posts );
312}
313
314/**
315 * Checks if the current blog has custom styles in use.
316 *
317 * @return bool Returns true if custom styles are in use.
318 */
319function wpcom_global_styles_in_use() {
320    /*
321     * If `WP_Theme_JSON_Resolver` is missing, then the site is running an old version
322     * of WordPress, so we cannot determine whether the site has custom styles.
323     */
324    if ( ! class_exists( 'WP_Theme_JSON_Resolver' ) ) {
325        return false;
326    }
327
328    $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( wp_get_theme() );
329
330    $global_styles_in_use = wpcom_global_styles_in_use_by_wp_global_styles_post( $user_cpt );
331
332    if ( $global_styles_in_use ) {
333        do_action( 'global_styles_log', 'global_styles_in_use' );
334    } else {
335        do_action( 'global_styles_log', 'global_styles_not_in_use' );
336    }
337
338    return $global_styles_in_use;
339}
340
341/**
342 * Checks whether the site is exempt from Premium Global Styles because
343 * it was created before the Premium Global Styles launch date (2022-12-15)
344 * and had already customized its Global Styles.
345 *
346 * We use blog stickers and other strategies to only perform the intensive check
347 * when strictly needed.
348 *
349 * @param  int $blog_id Blog ID.
350 * @return bool Whether the site is exempt from Premium Global Styles.
351 */
352function wpcom_premium_global_styles_is_site_exempt( $blog_id = 0 ) {
353    if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
354        $blog      = get_blog_details( $blog_id, false );
355        $is_simple = is_blog_wpcom( $blog );
356        $is_atomic = is_blog_atomic( $blog );
357    } elseif ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) {
358        $is_simple = false;
359        $is_atomic = true;
360    } else {
361        $is_simple = false;
362        $is_atomic = false;
363    }
364
365    if ( ! $is_simple && ! $is_atomic ) {
366        return false;
367    }
368
369    // If the exemption check has already been performed, just return if the site is exempt.
370    if ( wpcom_has_blog_sticker( 'wpcom-premium-global-styles-exemption-checked', $blog_id ) ) {
371        return wpcom_has_blog_sticker( 'wpcom-premium-global-styles-exempt', $blog_id );
372    }
373
374    // Simple sites created after we made GS a paid feature (2022-12-15) are never exempt.
375    if ( $is_simple && $blog_id >= 213403000 ) {
376        return false;
377    }
378
379    // Atomic sites created before we started limiting GS on Summer Special sites (2025-10-11) are always exempt.
380    if ( $is_atomic && $blog_id <= 249177000 ) {
381        return true;
382    }
383
384    /**
385     * It's important to have this condition after the 'wpcom-premium-global-styles-exemption-checked' blog sticker is checked!
386     *
387     * Atomic sites do not have site exemptions because previously all Atomic sites were on Business/eCommerce.
388     * Also, the summer special applies only to new sites.
389     *
390     * If plugins will be available on all SITES, not just new sites, this might not be true.
391     * However, this shouldn't be a problem because the site should already have the sticker already applied on their site from the time they were on simple.
392     */
393    if ( ! $is_simple ) {
394        return false;
395    }
396
397    // If the current user cannot modify the `wp_global_styles` CPT, the exemption check is not needed;
398    // other conditions will determine whether they can use GS.
399    if ( ! wpcom_simple_sites_global_styles_current_user_can_edit_wp_global_styles( $blog_id ) ) {
400        return false;
401    }
402
403    switch_to_blog( $blog_id );
404
405    $note = 'Automated sticker. See https://wp.me/p7DVsv-fY6#comment-44778';
406    $user = 'a8c'; // A non-empty string avoids storing the current user as author of the sticker change.
407
408    add_blog_sticker( 'wpcom-premium-global-styles-exemption-checked', $note, $user, $blog_id );
409
410    $global_styles_used = false;
411
412    $wp_global_styles_posts = get_posts(
413        array(
414            'post_type'   => 'wp_global_styles',
415            'numberposts' => 100,
416        )
417    );
418    foreach ( $wp_global_styles_posts as $wp_global_styles_post ) {
419        if ( wpcom_global_styles_in_use_by_wp_global_styles_post( $wp_global_styles_post->to_array() ) ) {
420            $global_styles_used = true;
421            break;
422        }
423    }
424
425    if ( $global_styles_used ) {
426        add_blog_sticker( 'wpcom-premium-global-styles-exempt', $note, $user, $blog_id );
427    }
428
429    restore_current_blog();
430
431    return $global_styles_used;
432}
433
434/**
435 * Returns whether the global style notice should be shown or not in the admin bar.
436 *
437 * @return bool Whether the global styles notice should be rendered.
438 */
439function wpcom_should_show_global_styles_admin_bar() {
440    static $should_show_global_styles_admin_bar = null;
441    if ( $should_show_global_styles_admin_bar !== null ) {
442        return $should_show_global_styles_admin_bar;
443    }
444
445    // Only show notice in the frontend.
446    if ( is_admin() ) {
447        $should_show_global_styles_admin_bar = false;
448        return $should_show_global_styles_admin_bar;
449    }
450
451    $current_user_id = get_current_user_id();
452
453    if ( ! $current_user_id ) {
454        $should_show_global_styles_admin_bar = false;
455        return $should_show_global_styles_admin_bar;
456    }
457
458    $current_blog_id = get_wpcom_blog_id();
459
460    if ( ! (
461        is_user_member_of_blog( $current_user_id, $current_blog_id ) &&
462        current_user_can( 'manage_options' )
463    ) ) {
464        $should_show_global_styles_admin_bar = false;
465        return $should_show_global_styles_admin_bar;
466    }
467
468    if ( wpcom_has_blog_sticker( 'difm-lite-in-progress', $current_blog_id ) ) {
469        $should_show_global_styles_admin_bar = false;
470        return $should_show_global_styles_admin_bar;
471    }
472
473    // The site is being previewed in Calypso or Gutenberg.
474    if (
475        isset( $_GET['iframe'] ) && 'true' === $_GET['iframe'] && ( // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Not a form action
476            ( isset( $_GET['theme_preview'] ) && 'true' === $_GET['theme_preview'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Not a form action
477            ( isset( $_GET['preview'] ) && 'true' === $_GET['preview'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Not a form action
478        ) ||
479        isset( $_GET['widgetPreview'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Not a form action (Gutenberg < 9.2)
480        isset( $_GET['widget-preview'] ) || // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Not a form action (Gutenberg >= 9.2)
481        ( isset( $_GET['hide_banners'] ) && $_GET['hide_banners'] === 'true' )  // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Not a form action
482    ) {
483        $should_show_global_styles_admin_bar = false;
484        return $should_show_global_styles_admin_bar;
485    }
486
487    // Do not show the notice when previewed in the customizer
488    if ( is_customize_preview() ) {
489        $should_show_global_styles_admin_bar = false;
490        return $should_show_global_styles_admin_bar;
491    }
492
493    // No banner for agency-managed sites.
494    if ( ! empty( get_option( 'is_fully_managed_agency_site' ) ) ) {
495        $should_show_global_styles_admin_bar = false;
496        return $should_show_global_styles_admin_bar;
497    }
498
499    if ( ! wpcom_should_limit_global_styles() || ! wpcom_global_styles_in_use() ) {
500        $should_show_global_styles_admin_bar = false;
501        return $should_show_global_styles_admin_bar;
502    }
503
504    $should_show_global_styles_admin_bar = true;
505    return $should_show_global_styles_admin_bar;
506}
507/**
508 * Renders the global style notice in the admin bar.
509 *
510 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar core object.
511 */
512function wpcom_display_global_styles_notice_admin_bar( $wp_admin_bar ) {
513    if ( ! wpcom_should_show_global_styles_admin_bar() ) {
514        return;
515    }
516
517    if ( method_exists( '\WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
518        $site_slug = WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
519    } else {
520        $home_url  = home_url( '/' );
521        $site_slug = wp_parse_url( $home_url, PHP_URL_HOST );
522    }
523
524    $gs_upgrade_plan = wpcom_get_global_styles_upsell_plan_slug();
525    $upgrade_url     = "https://wordpress.com/plans/$site_slug?plan=$gs_upgrade_plan&feature=style-customization";
526
527    $support_url = function_exists( 'localized_wpcom_url' )
528        ? localized_wpcom_url( 'https://wordpress.com/support/using-styles/' )
529        : 'https://wordpress.com/support/using-styles/';
530
531    $message = sprintf(
532        /* translators: %1$s - documentation URL, %2$s - the name of the required plan */
533        __(
534            'Your site includes <a href="%1$s" target="_blank">premium styles</a> that are only visible to visitors after upgrading to the %2$s plan or higher.',
535            'jetpack-mu-wpcom'
536        ),
537        $support_url,
538        Plans::get_plan_short_name( $gs_upgrade_plan )
539    );
540
541    if ( wpcom_is_previewing_global_styles() ) {
542        $preview_url = add_query_arg( 'hide-global-styles', '' );
543    } else {
544        $preview_url = remove_query_arg( 'hide-global-styles' );
545    }
546
547    $wp_admin_bar->add_node(
548        array(
549            'id'    => 'wpcom-global-styles',
550            'title' => __( 'Upgrade required', 'jetpack-mu-wpcom' ),
551            'href'  => $upgrade_url,
552        )
553    );
554
555    $wp_admin_bar->add_node(
556        array(
557            'parent' => 'wpcom-global-styles',
558            'id'     => 'wpcom-global-styles-description',
559            'title'  =>
560                '<button class="wpcom-global-styles-close">
561                    <svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="m249 849-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z"/></svg>
562                </button>' .
563                wp_kses(
564                    $message,
565                    array(
566                        'a' => array(
567                            'href'   => array(),
568                            'target' => array(),
569                        ),
570                    )
571                ),
572        )
573    );
574
575    $wp_admin_bar->add_node(
576        array(
577            'parent' => 'wpcom-global-styles',
578            'id'     => 'wpcom-global-styles-upgrade',
579            'title'  => esc_html__( 'Upgrade now', 'jetpack-mu-wpcom' ),
580            'href'   => $upgrade_url,
581        )
582    );
583
584    $wp_admin_bar->add_node(
585        array(
586            'parent' => 'wpcom-global-styles',
587            'id'     => 'wpcom-global-styles-reset',
588            'title'  => esc_html__( 'Remove premium styles', 'jetpack-mu-wpcom' ),
589            'href'   => admin_url( 'site-editor.php?p=/styles' ),
590        )
591    );
592
593    $wp_admin_bar->add_group(
594        array(
595            'parent' => 'wpcom-global-styles',
596            'id'     => 'wpcom-global-styles-preview',
597            'meta'   => array(
598                'class' => 'ab-sub-secondary',
599            ),
600        )
601    );
602    $wp_admin_bar->add_node(
603        array(
604            'parent' => 'wpcom-global-styles-preview',
605            'id'     => 'wpcom-global-styles-preview-button',
606            'title'  => '<label><input type="checkbox" ' . ( wpcom_is_previewing_global_styles() ? 'checked' : '' ) . '><span></span></label>' . esc_html__( 'Preview premium styles', 'jetpack-mu-wpcom' ),
607            'href'   => $preview_url,
608        )
609    );
610}
611add_action( 'admin_bar_menu', 'wpcom_display_global_styles_notice_admin_bar', 499 ); // Before "Launch site".
612
613/**
614 * Include the Rest API that returns the global style information for a give WordPress site.
615 */
616require_once __DIR__ . '/api/class-global-styles-status-rest-api.php';
617
618/**
619 * Checks if the necessary conditions are met in order to establish that the supplied user should be considered as previewing Global Styles.
620 *
621 * @param int|null $user_id User id to check.
622 *
623 * @return bool
624 */
625function wpcom_is_previewing_global_styles( ?int $user_id = null ) {
626    if ( null === $user_id ) {
627        $user_id = get_current_user_id();
628    }
629
630    if ( 0 === $user_id ) {
631        return false;
632    }
633
634    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
635    return ! isset( $_GET['hide-global-styles'] ) && user_can( $user_id, 'manage_options' );
636}
637
638/**
639 * Checks whether the site has access to the Global Styles feature when the editor is live previewing a Premium theme without a Premium plan or higher.
640 *
641 * @param int $blog_id The WPCOM blog ID.
642 * @return bool Whether the site has access to Global Styles when live previewing.
643 */
644function wpcom_global_styles_is_previewing_premium_theme_without_premium_plan( $blog_id ) {
645    if ( ! isset( $_GET['wp_theme_preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
646        // Not live previewing.
647        return false;
648    }
649    $wp_theme_preview = sanitize_text_field( wp_unslash( $_GET['wp_theme_preview'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
650
651    $is_previewing_premium_theme = str_starts_with( $wp_theme_preview, 'premium/' );
652    if ( ! $is_previewing_premium_theme ) {
653        // Not a premium theme.
654        return false;
655    }
656
657    // Check for a Premium plan or higher by checking if can use global styles.
658    $has_premium_plan_or_higher = wpcom_site_has_feature( WPCOM_Features::GLOBAL_STYLES, $blog_id );
659
660    return ! $has_premium_plan_or_higher;
661}
662
663/**
664 * We return the upsell plan required for the current Global Styles plan requirement.
665 *
666 * @return string
667 */
668function wpcom_get_global_styles_upsell_plan_slug() {
669    return 'personal-bundle';
670}
671
672add_filter( 'wpcom_customize_css_plan_slug', 'wpcom_get_global_styles_upsell_plan_slug' );