Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
33.82% covered (danger)
33.82%
46 / 136
31.25% covered (danger)
31.25%
5 / 16
CRAP
n/a
0 / 0
wpcomsh_register_ignore_outdated_gutenberg_plugin
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
wpcomsh_is_wp_beta_version
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
wpcomsh_get_core_bundled_gutenberg_version
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
wpcomsh_get_installed_gutenberg_plugin_version
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
wpcomsh_should_ignore_gutenberg_plugin
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
wpcomsh_is_gutenberg_plugin_ignored
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
wpcomsh_ignore_outdated_gutenberg_plugin
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
wpcomsh_remove_gutenberg_experiments
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_filter_gutenberg_experiments
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_remove_gutenberg_experimental_menu
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_enable_error_reporting_for_react_19
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
wpcomsh_remove_site_editor_reset_styles
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
gutenberg_enqueue_block_styles_assets
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
wpcom_safecss_filter_attr_allow_css_rgba
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
wpcom_safecss_allow_additional_css_properties
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
wpcomsh_add_guideline_sync_meta_whitelist
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Customizations for the Gutenberg plugin.
4 *
5 * Since we'll be trying to keep up with latest Gutenberg versions both on Simple and Atomic sites,
6 * we need to ensure that some experimental functionality is not exposed yet.
7 *
8 * @package wpcomsh
9 */
10
11// Enable allowed experiments early so Gutenberg's load.php include-time check picks them up.
12// This runs at MU plugin load time, before Gutenberg (a regular plugin) is loaded.
13// Both filters are needed: default_option_ fires when the option doesn't exist in the DB,
14// option_ fires when it does.
15add_filter( 'default_option_gutenberg-experiments', 'wpcomsh_filter_gutenberg_experiments' );
16add_filter( 'option_gutenberg-experiments', 'wpcomsh_filter_gutenberg_experiments' );
17
18// Ignore the Gutenberg plugin when it is older than the version bundled in core, on beta-track sites.
19add_action( 'muplugins_loaded', 'wpcomsh_register_ignore_outdated_gutenberg_plugin' );
20
21/**
22 * Minimum Gutenberg version bundled in each WordPress major release.
23 *
24 * @see https://developer.wordpress.org/block-editor/contributors/versions-in-wordpress/
25 */
26const WPCOMSH_CORE_BUNDLED_GUTENBERG_VERSIONS = array(
27    '7.1' => '23.6.0',
28);
29
30/**
31 * Attach the active-plugins filter only while core loads plugins, and only on the beta track.
32 *
33 * The filter is registered on `muplugins_loaded` and removed on `plugins_loaded`, so it only
34 * affects the read that core makes to load plugins. Outside that window the stored option is
35 * returned untouched, so the trimmed list can never be read and saved back to the database
36 * (which would deactivate the plugin permanently).
37 */
38function wpcomsh_register_ignore_outdated_gutenberg_plugin() {
39    if ( ! wpcomsh_is_wp_beta_version() ) {
40        return;
41    }
42
43    add_filter( 'option_active_plugins', 'wpcomsh_ignore_outdated_gutenberg_plugin' );
44    add_action(
45        'plugins_loaded',
46        static function () {
47            remove_filter( 'option_active_plugins', 'wpcomsh_ignore_outdated_gutenberg_plugin' );
48        },
49        0
50    );
51}
52
53/**
54 * Whether the site is running a pre-release (alpha/beta/RC) build of WordPress core.
55 *
56 * @return bool
57 */
58function wpcomsh_is_wp_beta_version() {
59    global $wp_version;
60
61    return is_string( $wp_version ) && (bool) preg_match( '/-(alpha|beta|RC)/i', $wp_version );
62}
63
64/**
65 * Returns the minimum Gutenberg version bundled in the running WordPress core, or null if unknown.
66 *
67 * @return string|null
68 */
69function wpcomsh_get_core_bundled_gutenberg_version() {
70    global $wp_version;
71
72    if ( ! is_string( $wp_version ) || ! preg_match( '/^(\d+\.\d+)/', $wp_version, $matches ) ) {
73        return null;
74    }
75
76    return WPCOMSH_CORE_BUNDLED_GUTENBERG_VERSIONS[ $matches[1] ] ?? null;
77}
78
79/**
80 * Returns the installed Gutenberg plugin version, or null if it can't be read.
81 *
82 * Reads the plugin header, as this runs before the plugin (and GUTENBERG_VERSION) loads.
83 *
84 * @return string|null
85 */
86function wpcomsh_get_installed_gutenberg_plugin_version() {
87    $plugin_file = WP_PLUGIN_DIR . '/gutenberg/gutenberg.php';
88
89    if ( ! is_readable( $plugin_file ) ) {
90        return null;
91    }
92
93    $data = get_file_data( $plugin_file, array( 'Version' => 'Version' ) );
94
95    return empty( $data['Version'] ) ? null : $data['Version'];
96}
97
98/**
99 * Whether the plugin should be ignored: the site is on the beta track and the installed
100 * plugin is older than core's bundle (both versions must be known).
101 *
102 * @param string|null $plugin_version      Installed Gutenberg plugin version.
103 * @param string|null $min_bundled_version Minimum Gutenberg version bundled in the running core.
104 * @return bool
105 */
106function wpcomsh_should_ignore_gutenberg_plugin( $plugin_version, $min_bundled_version ) {
107    if ( ! wpcomsh_is_wp_beta_version() || null === $plugin_version || null === $min_bundled_version ) {
108        return false;
109    }
110
111    return version_compare( $plugin_version, $min_bundled_version, '<' );
112}
113
114/**
115 * Whether the installed Gutenberg plugin is being ignored in favor of core's bundled Gutenberg.
116 *
117 * @return bool
118 */
119function wpcomsh_is_gutenberg_plugin_ignored() {
120    return wpcomsh_should_ignore_gutenberg_plugin(
121        wpcomsh_get_installed_gutenberg_plugin_version(),
122        wpcomsh_get_core_bundled_gutenberg_version()
123    );
124}
125
126/**
127 * Drop the Gutenberg plugin from the active plugins list when it is older than core's bundle.
128 *
129 * The stored option is left untouched, so the plugin returns once it is updated past the bundle.
130 *
131 * @param mixed $plugins Value of the active_plugins option.
132 * @return mixed
133 */
134function wpcomsh_ignore_outdated_gutenberg_plugin( $plugins ) {
135    if ( ! is_array( $plugins ) ) {
136        return $plugins;
137    }
138
139    $key = array_search( 'gutenberg/gutenberg.php', $plugins, true );
140    if ( false === $key || ! wpcomsh_is_gutenberg_plugin_ignored() ) {
141        return $plugins;
142    }
143
144    unset( $plugins[ $key ] );
145
146    return array_values( $plugins );
147}
148
149/**
150 * Disable all Gutenberg experiments except explicitly allowed ones.
151 *
152 * @see https://github.com/WordPress/gutenberg/blob/e6d8284b03799136915495654e821ca6212ae6d8/lib/load.php#L22
153 */
154function wpcomsh_remove_gutenberg_experiments() {
155    $jetpack_options = get_option( 'jetpack_options' );
156    if ( is_array( $jetpack_options ) && isset( $jetpack_options['id'] ) ) {
157        $blog_id = (int) $jetpack_options['id'];
158    } else {
159        $blog_id = get_current_blog_id();
160    }
161
162    $allowed_blogs = array(
163        211453162, // wpmovies.dev
164    );
165
166    if ( in_array( $blog_id, $allowed_blogs, true ) ) {
167        // Undo the early filters for allowed blogs so they can still manage Gutenberg experiments.
168        remove_filter( 'default_option_gutenberg-experiments', 'wpcomsh_filter_gutenberg_experiments' );
169        remove_filter( 'option_gutenberg-experiments', 'wpcomsh_filter_gutenberg_experiments' );
170        return;
171    }
172    add_action( 'admin_menu', 'wpcomsh_remove_gutenberg_experimental_menu' );
173}
174add_action( 'init', 'wpcomsh_remove_gutenberg_experiments' );
175
176/**
177 * Disable all Gutenberg experiments except explicitly allowed ones.
178 *
179 * Allowed experiments are always enabled regardless of the option value in the database.
180 *
181 * @return array List of the enabled experiments.
182 */
183function wpcomsh_filter_gutenberg_experiments() {
184    return array(
185        'gutenberg-guidelines' => true,
186    );
187}
188
189/**
190 * Remove Gutenberg's Experiments submenu item.
191 */
192function wpcomsh_remove_gutenberg_experimental_menu() {
193    remove_submenu_page( 'options-general.php', 'experiments-wp-admin' );
194}
195
196/**
197 * Enable wp-admin JS error reporting on sites participating in the `gutenberg-react-19`
198 * experiment, so that errors caused by the React 19 upgrade are captured.
199 *
200 * Reporting is enabled only for WP.com-connected users, who have accepted the WP.com
201 * terms of service and privacy policy. Local users have made no such agreement.
202 *
203 * @param bool $is_enabled Whether error reporting is enabled.
204 * @return bool
205 */
206function wpcomsh_enable_error_reporting_for_react_19( $is_enabled ) {
207    if ( $is_enabled ) {
208        return true;
209    }
210
211    if ( ! function_exists( 'is_user_connected' ) || ! is_user_connected( get_current_user_id() ) ) {
212        return false;
213    }
214
215    if ( function_exists( 'wpcomsh_is_site_sticker_active' ) && wpcomsh_is_site_sticker_active( 'gutenberg-react-19' ) ) {
216        return true;
217    }
218
219    $site_id = wpcomsh_get_atomic_site_id();
220    if ( ! $site_id ) {
221        return false;
222    }
223
224    $current_segment = 70; // Segment of sites that get error reporting, in %.
225    $site_segment    = $site_id % 100;
226
227    // Sites whose id ends in digits < $current_segment are in the segment.
228    return $site_segment < $current_segment;
229}
230add_filter( 'a8c_enable_error_reporting', 'wpcomsh_enable_error_reporting_for_react_19' );
231
232/**
233 * Hotfix a Gutenberg bug that inadvertently loads wp-reset-editor-syles stylesheet in the
234 * iframed site editor.
235 *
236 * We are attempting to merge the same changes into core Gutenberg. If successful, these
237 * changes can be removed. https://github.com/WordPress/gutenberg/pull/33522
238 */
239function wpcomsh_remove_site_editor_reset_styles() {
240    $current_screen = get_current_screen();
241
242    if ( ! $current_screen || $current_screen->base !== 'toplevel_page_gutenberg-edit-site' ) {
243        return;
244    }
245
246    /*
247     * Remove wp-reset-editor-styles css in the Site Editor, as it's not needed with an iframed editor,
248     * and can interfere with Global Styles if concatenated with other scripts.
249     */
250    if ( isset( wp_styles()->registered['wp-edit-blocks'] ) ) {
251        $wp_edit_blocks_dependencies                    = array_diff( wp_styles()->registered['wp-edit-blocks']->deps, array( 'wp-reset-editor-styles' ) );
252        wp_styles()->registered['wp-edit-blocks']->deps = $wp_edit_blocks_dependencies;
253    }
254}
255add_action( 'admin_enqueue_scripts', 'wpcomsh_remove_site_editor_reset_styles' );
256
257/**
258 * Overrides the WordPress enqueue_block_styles_assets function, which contains
259 * a bug eventually leading to a fatal error for full site editing enabled themes
260 * which do register custom block styles and don't have a template for requested
261 * template (eg.: 404)
262 *
263 * More details on the bug can be found in: https://core.trac.wordpress.org/ticket/54323
264 *
265 * @see enqueue_block_styles_assets
266 */
267function gutenberg_enqueue_block_styles_assets() {
268    $block_styles = WP_Block_Styles_Registry::get_instance()->get_all_registered();
269
270    foreach ( $block_styles as $block_name => $styles ) {
271        foreach ( $styles as $style_properties ) {
272            if ( isset( $style_properties['style_handle'] ) ) {
273
274                // If the site loads separate styles per-block, enqueue the stylesheet on render.
275                if ( wp_should_load_separate_core_block_assets() ) {
276                    add_filter(
277                        'render_block',
278                        function ( $html ) use ( $style_properties ) {
279                            wp_enqueue_style( $style_properties['style_handle'] );
280                            return $html;
281                        }
282                    );
283                } else {
284                    wp_enqueue_style( $style_properties['style_handle'] );
285                }
286            }
287            if ( isset( $style_properties['inline_style'] ) ) {
288
289                // Default to "wp-block-library".
290                $handle = 'wp-block-library';
291
292                // If the site loads separate styles per-block, check if the block has a stylesheet registered.
293                if ( wp_should_load_separate_core_block_assets() ) {
294                    $block_stylesheet_handle = generate_block_asset_handle( $block_name, 'style' );
295                    global $wp_styles;
296                    if ( isset( $wp_styles->registered[ $block_stylesheet_handle ] ) ) {
297                        $handle = $block_stylesheet_handle;
298                    }
299                }
300
301                // Add inline styles to the calculated handle.
302                wp_add_inline_style( $handle, $style_properties['inline_style'] );
303            }
304        }
305    }
306}
307
308/**
309 * Disable the Widgets Block Editor screen feature.
310 *
311 * @see D48850-code
312 * @see https://github.com/WordPress/gutenberg/pull/24843
313 */
314add_filter( 'gutenberg_use_widgets_block_editor', '__return_false', 100 );
315
316/**
317 * WPCOM Hack to allow rgba() as CSS property's values, which would otherwise be filtered by
318 * `safecss_filter_attr` in `wp-includes/kses.php`.
319 *
320 * For more context, see: p1670179266219969-slack-C02FMH4G8
321 * and p1670086796757129-slack-CBTN58FTJ.
322 *
323 * @param bool   $allow_css whether the current CSS property is allowed.
324 * @param string $css_test_string contains the actual CSS property to be tested.
325 * @return bool whether or not the given CSS property is allowed.
326 */
327function wpcom_safecss_filter_attr_allow_css_rgba( $allow_css, $css_test_string ) {
328    if ( false === $allow_css ) {
329        // Allow rgb and rgba values with numbers, commas, percents, dividers and decimal numbers in the following parenthesis only.
330        $css_test_string = preg_replace(
331            '/\b(?:rgb[a]?)\((?:[\.]?\d+[\.\s\,\%\/]*\d*){0,4}\)/',
332            '',
333            $css_test_string
334        );
335
336        $allow_css = ! preg_match( '%[\\\(&=}]|/\*%', $css_test_string );
337    }
338
339    return $allow_css;
340}
341add_filter( 'safecss_filter_attr_allow_css', 'wpcom_safecss_filter_attr_allow_css_rgba', 10, 2 );
342
343/**
344 * WPCOM Hack to allow additional inline CSS that would otherwise be filtered by `safecss_filter_attr` in
345 * `wp-includes/kses.php`.
346 *
347 * For more context, see: p1670179266219969-slack-C02FMH4G8
348 * and p1670086796757129-slack-CBTN58FTJ.
349 *
350 * @param array<string> $css_properties an array of allowed CSS properties.
351 * @return array<string> the array of allowed CSS properties.
352 */
353function wpcom_safecss_allow_additional_css_properties( $css_properties ) {
354    $css_properties[] = 'display';
355    $css_properties[] = 'transform';
356    $css_properties[] = 'position';
357
358    return $css_properties;
359}
360add_filter( 'safe_style_css', 'wpcom_safecss_allow_additional_css_properties', 10, 1 );
361
362/**
363 * Add guideline meta keys to the Jetpack sync post meta whitelist.
364 *
365 * @param array $whitelist Current post meta whitelist.
366 * @return array
367 */
368function wpcomsh_add_guideline_sync_meta_whitelist( $whitelist ) {
369    $guideline_meta_keys = array(
370        '_guideline_copy',
371        '_guideline_images',
372        '_guideline_site',
373        '_guideline_additional',
374        '_guideline_block_core_paragraph',
375        '_guideline_block_core_image',
376        '_guideline_block_core_heading',
377        '_guideline_block_core_list',
378        '_guideline_block_core_list_item',
379        '_guideline_block_core_quote',
380        '_guideline_block_core_code',
381        '_guideline_block_core_table',
382        '_guideline_block_core_video',
383        '_guideline_block_core_audio',
384        '_guideline_block_core_gallery',
385        '_guideline_block_core_cover',
386        '_guideline_block_core_pullquote',
387        '_guideline_block_core_preformatted',
388        '_guideline_block_core_verse',
389        '_guideline_block_core_button',
390        '_guideline_block_core_media_text',
391        '_guideline_block_core_freeform',
392        '_guideline_block_core_html',
393        '_guideline_block_core_embed',
394    );
395    return array_merge( $whitelist, $guideline_meta_keys );
396}
397add_filter( 'jetpack_sync_post_meta_whitelist', 'wpcomsh_add_guideline_sync_meta_whitelist' );