Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
62.34% covered (warning)
62.34%
48 / 77
44.44% covered (danger)
44.44%
4 / 9
CRAP
n/a
0 / 0
Automattic\Jetpack\Extensions\CookieConsent\should_register_block
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
Automattic\Jetpack\Extensions\CookieConsent\register_block
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
Automattic\Jetpack\Extensions\CookieConsent\load_assets
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
5.15
Automattic\Jetpack\Extensions\CookieConsent\notify_batcache_that_content_changed
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
Automattic\Jetpack\Extensions\CookieConsent\render_cookie_consent_template
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
6.29
Automattic\Jetpack\Extensions\CookieConsent\get_custom_css_rules
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
Automattic\Jetpack\Extensions\CookieConsent\print_late_custom_css_rules
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
Automattic\Jetpack\Extensions\CookieConsent\cookie_consent_register_settings
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
Automattic\Jetpack\Extensions\CookieConsent\add_css_fallback_values
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Cookie-consent Block.
4 *
5 * @since 12.0
6 *
7 * @package automattic/jetpack
8 */
9
10namespace Automattic\Jetpack\Extensions\CookieConsent;
11
12use Automattic\Jetpack\Blocks;
13use Jetpack_Gutenberg;
14
15if ( ! defined( 'ABSPATH' ) ) {
16    exit( 0 );
17}
18
19const COOKIE_NAME = 'eucookielaw';
20
21/**
22 * Should the block be registered?
23 * In wp-admin, we only want to show the block in the site editor.
24 *
25 * @since 12.0
26 *
27 * @return bool
28 */
29function should_register_block() {
30    global $pagenow;
31
32    // Always register the widget if we're on the front end
33    if ( ! is_admin() ) {
34        return true;
35    }
36
37    if ( is_admin() && $pagenow === 'site-editor.php' ) {
38        return true;
39    }
40}
41
42/**
43 * Registers the block for use in Gutenberg
44 * This is done via an action so that we can disable
45 * registration if we need to.
46 */
47function register_block() {
48    if ( ! should_register_block() ) {
49        return;
50    }
51
52    Blocks::jetpack_register_block(
53        __DIR__,
54        array(
55            'render_callback' => __NAMESPACE__ . '\load_assets',
56            'attributes'      => array(
57                'render_from_template' => array(
58                    'default' => false,
59                    'type'    => 'boolean',
60                ),
61            ),
62        )
63    );
64}
65add_action( 'init', __NAMESPACE__ . '\register_block' );
66
67/**
68 * Cookie-consent block registration/dependency declaration.
69 *
70 * @param array  $attr    Array containing the Cookie-consent block attributes.
71 * @param string $content String containing the Cookie-consent block content.
72 *
73 * @return string
74 */
75function load_assets( $attr, $content ) {
76    // We want to bust the cache even if the cookie isn't set.
77    // This is needed for when the cookie expires,
78    // and we should send fresh HTML with the cookie block in it.
79    notify_batcache_that_content_changed();
80
81    if (
82        /**
83         * Filters the display of the Cookie-consent Block e.g by GDPR CMP banner on WordAds sites.
84         *
85         * @since 13.2
86         *
87         * @param bool $disable_cookie_consent_block Whether to disable the Cookie-consent Block.
88         */
89        apply_filters( 'jetpack_disable_cookie_consent_block', false )
90    ) {
91        return '';
92    }
93
94    // If the user has already accepted the cookie consent, don't show the block.
95    if ( isset( $_COOKIE[ COOKIE_NAME ] ) ) {
96        return '';
97    }
98
99    $option = get_option( 'cookie_consent_template' );
100    if ( ! empty( $option ) && empty( $attr['render_from_template'] ) ) {
101        return '';
102    }
103
104    /*
105     * Enqueue necessary scripts and styles.
106     */
107    Jetpack_Gutenberg::load_assets_as_required( __DIR__ );
108
109    /*
110     * Add a fallback color to block styles.
111     */
112    $content = add_css_fallback_values( $content );
113
114    // The saved markup already carries the block classes; a wrapper would take core's Custom CSS class instead of the banner.
115    return $content;
116}
117
118/**
119 * Batcache busting: since the cookie consent is part of the cached response HTML, it can still render even when the cookie is set (when it shouldn't).
120 * Because, by default, the cache doesn't vary around the cookie's value. This makes the cookie value part of the cache key.
121 *
122 * See: https://github.com/Automattic/batcache/blob/d4f617b335e9772a61b6d03ad3498b55c8137592/advanced-cache.php#L29
123 */
124function notify_batcache_that_content_changed() {
125    if ( function_exists( 'vary_cache_on_function' ) ) {
126        vary_cache_on_function( 'return isset( $_COOKIE[ "' . COOKIE_NAME . '" ] );' );
127    }
128}
129
130/**
131 * Render the cookie consent template.
132 *
133 * @since 12.4
134 */
135function render_cookie_consent_template() {
136
137    if ( is_admin() ) {
138        return;
139    }
140
141    // Check whether block theme functions exist.
142    if ( ! function_exists( 'parse_blocks' ) ) {
143        return;
144    }
145
146    $template = get_option( 'cookie_consent_template' );
147
148    if ( empty( $template ) ) {
149        return;
150    }
151
152    $parsed = parse_blocks( $template );
153    if ( ! empty( $parsed[0] ) ) {
154        $parsed[0]['attrs']['render_from_template'] = true;
155
156        $custom_css_before = get_custom_css_rules();
157        $banner            = render_block( $parsed[0] );
158        if ( '' !== $banner ) {
159            print_late_custom_css_rules( $custom_css_before );
160            echo $banner; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
161        }
162    }
163}
164add_action( 'wp_footer', __NAMESPACE__ . '\render_cookie_consent_template' );
165
166/**
167 * Get the block Custom CSS rules queued so far.
168 *
169 * @since 16.4
170 *
171 * @return string[] Rules, empty when the core handle is not in use.
172 */
173function get_custom_css_rules() {
174    $rules = wp_styles()->get_data( 'wp-block-custom-css', 'after' );
175    return is_array( $rules ) ? $rules : array();
176}
177
178/**
179 * Print Custom CSS rules queued since the snapshot, if the core handle already printed in the head.
180 *
181 * The banner renders in the footer, too late for rules it queues to print on their own.
182 *
183 * @since 16.4
184 *
185 * @param string[] $rules_before Rules from get_custom_css_rules() before the render.
186 */
187function print_late_custom_css_rules( $rules_before ) {
188    if ( ! wp_style_is( 'wp-block-custom-css', 'done' ) ) {
189        return;
190    }
191
192    $new_rules = array_diff( get_custom_css_rules(), $rules_before );
193    if ( $new_rules ) {
194        $processor = new \WP_HTML_Tag_Processor( '<style></style>' );
195        $processor->next_tag();
196        $processor->set_attribute( 'id', 'jetpack-cookie-consent-custom-css' );
197        $processor->set_modifiable_text( implode( '', $new_rules ) );
198        echo $processor->get_updated_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
199    }
200}
201
202/**
203 * Register cookie_consent_template setting
204 *
205 * @since 12.4
206 */
207function cookie_consent_register_settings() {
208    register_setting(
209        'general',
210        'cookie_consent_template',
211        array(
212            'type'         => 'string',
213            'show_in_rest' => true,
214        )
215    );
216}
217
218add_action( 'rest_api_init', __NAMESPACE__ . '\cookie_consent_register_settings' );
219
220/**
221 * Add a fallback color to block styles.
222 *
223 * @since 15.1
224 *
225 * @param string $content The block content.
226 * @return string The content with fallback values added to CSS custom properties.
227 */
228function add_css_fallback_values( $content ) {
229    // Define the CSS custom properties and their fallback values.
230    $css_fallbacks = array(
231        'var(--wp--preset--color--contrast)' => 'var(--wp--preset--color--contrast, #000000)',
232        'var(--wp--preset--color--tertiary)' => 'var(--wp--preset--color--tertiary, #f0f0f0)',
233    );
234
235    // Replace CSS custom properties with their fallback versions.
236    foreach ( $css_fallbacks as $original => $with_fallback ) {
237        $content = str_replace( $original, $with_fallback, $content );
238    }
239
240    return $content;
241}