Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
33.58% covered (danger)
33.58%
46 / 137
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 / 4
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-content-guidelines' => true,
186        'gutenberg-guidelines'         => true,
187    );
188}
189
190/**
191 * Remove Gutenberg's Experiments submenu item.
192 */
193function wpcomsh_remove_gutenberg_experimental_menu() {
194    remove_submenu_page( 'gutenberg', 'gutenberg-experiments' );
195}
196
197/**
198 * Enable wp-admin JS error reporting on sites participating in the `gutenberg-react-19`
199 * experiment, so that errors caused by the React 19 upgrade are captured.
200 *
201 * Reporting is enabled only for WP.com-connected users, who have accepted the WP.com
202 * terms of service and privacy policy. Local users have made no such agreement.
203 *
204 * @param bool $is_enabled Whether error reporting is enabled.
205 * @return bool
206 */
207function wpcomsh_enable_error_reporting_for_react_19( $is_enabled ) {
208    if ( $is_enabled ) {
209        return true;
210    }
211
212    if ( ! function_exists( 'is_user_connected' ) || ! is_user_connected( get_current_user_id() ) ) {
213        return false;
214    }
215
216    if ( function_exists( 'wpcomsh_is_site_sticker_active' ) && wpcomsh_is_site_sticker_active( 'gutenberg-react-19' ) ) {
217        return true;
218    }
219
220    $site_id = wpcomsh_get_atomic_site_id();
221    if ( ! $site_id ) {
222        return false;
223    }
224
225    $current_segment = 5; // Segment of sites that get error reporting, in %.
226    $site_segment    = $site_id % 100;
227
228    // Sites whose id ends in digits < $current_segment are in the segment.
229    return $site_segment < $current_segment;
230}
231add_filter( 'a8c_enable_error_reporting', 'wpcomsh_enable_error_reporting_for_react_19' );
232
233/**
234 * Hotfix a Gutenberg bug that inadvertently loads wp-reset-editor-syles stylesheet in the
235 * iframed site editor.
236 *
237 * We are attempting to merge the same changes into core Gutenberg. If successful, these
238 * changes can be removed. https://github.com/WordPress/gutenberg/pull/33522
239 */
240function wpcomsh_remove_site_editor_reset_styles() {
241    $current_screen = get_current_screen();
242
243    if ( ! $current_screen || $current_screen->base !== 'toplevel_page_gutenberg-edit-site' ) {
244        return;
245    }
246
247    /*
248     * Remove wp-reset-editor-styles css in the Site Editor, as it's not needed with an iframed editor,
249     * and can interfere with Global Styles if concatenated with other scripts.
250     */
251    if ( isset( wp_styles()->registered['wp-edit-blocks'] ) ) {
252        $wp_edit_blocks_dependencies                    = array_diff( wp_styles()->registered['wp-edit-blocks']->deps, array( 'wp-reset-editor-styles' ) );
253        wp_styles()->registered['wp-edit-blocks']->deps = $wp_edit_blocks_dependencies;
254    }
255}
256add_action( 'admin_enqueue_scripts', 'wpcomsh_remove_site_editor_reset_styles' );
257
258/**
259 * Overrides the WordPress enqueue_block_styles_assets function, which contains
260 * a bug eventually leading to a fatal error for full site editing enabled themes
261 * which do register custom block styles and don't have a template for requested
262 * template (eg.: 404)
263 *
264 * More details on the bug can be found in: https://core.trac.wordpress.org/ticket/54323
265 *
266 * @see enqueue_block_styles_assets
267 */
268function gutenberg_enqueue_block_styles_assets() {
269    $block_styles = WP_Block_Styles_Registry::get_instance()->get_all_registered();
270
271    foreach ( $block_styles as $block_name => $styles ) {
272        foreach ( $styles as $style_properties ) {
273            if ( isset( $style_properties['style_handle'] ) ) {
274
275                // If the site loads separate styles per-block, enqueue the stylesheet on render.
276                if ( wp_should_load_separate_core_block_assets() ) {
277                    add_filter(
278                        'render_block',
279                        function ( $html ) use ( $style_properties ) {
280                            wp_enqueue_style( $style_properties['style_handle'] );
281                            return $html;
282                        }
283                    );
284                } else {
285                    wp_enqueue_style( $style_properties['style_handle'] );
286                }
287            }
288            if ( isset( $style_properties['inline_style'] ) ) {
289
290                // Default to "wp-block-library".
291                $handle = 'wp-block-library';
292
293                // If the site loads separate styles per-block, check if the block has a stylesheet registered.
294                if ( wp_should_load_separate_core_block_assets() ) {
295                    $block_stylesheet_handle = generate_block_asset_handle( $block_name, 'style' );
296                    global $wp_styles;
297                    if ( isset( $wp_styles->registered[ $block_stylesheet_handle ] ) ) {
298                        $handle = $block_stylesheet_handle;
299                    }
300                }
301
302                // Add inline styles to the calculated handle.
303                wp_add_inline_style( $handle, $style_properties['inline_style'] );
304            }
305        }
306    }
307}
308
309/**
310 * Disable the Widgets Block Editor screen feature.
311 *
312 * @see D48850-code
313 * @see https://github.com/WordPress/gutenberg/pull/24843
314 */
315add_filter( 'gutenberg_use_widgets_block_editor', '__return_false', 100 );
316
317/**
318 * WPCOM Hack to allow rgba() as CSS property's values, which would otherwise be filtered by
319 * `safecss_filter_attr` in `wp-includes/kses.php`.
320 *
321 * For more context, see: p1670179266219969-slack-C02FMH4G8
322 * and p1670086796757129-slack-CBTN58FTJ.
323 *
324 * @param bool   $allow_css whether the current CSS property is allowed.
325 * @param string $css_test_string contains the actual CSS property to be tested.
326 * @return bool whether or not the given CSS property is allowed.
327 */
328function wpcom_safecss_filter_attr_allow_css_rgba( $allow_css, $css_test_string ) {
329    if ( false === $allow_css ) {
330        // Allow rgb and rgba values with numbers, commas, percents, dividers and decimal numbers in the following parenthesis only.
331        $css_test_string = preg_replace(
332            '/\b(?:rgb[a]?)\((?:[\.]?\d+[\.\s\,\%\/]*\d*){0,4}\)/',
333            '',
334            $css_test_string
335        );
336
337        $allow_css = ! preg_match( '%[\\\(&=}]|/\*%', $css_test_string );
338    }
339
340    return $allow_css;
341}
342add_filter( 'safecss_filter_attr_allow_css', 'wpcom_safecss_filter_attr_allow_css_rgba', 10, 2 );
343
344/**
345 * WPCOM Hack to allow additional inline CSS that would otherwise be filtered by `safecss_filter_attr` in
346 * `wp-includes/kses.php`.
347 *
348 * For more context, see: p1670179266219969-slack-C02FMH4G8
349 * and p1670086796757129-slack-CBTN58FTJ.
350 *
351 * @param array<string> $css_properties an array of allowed CSS properties.
352 * @return array<string> the array of allowed CSS properties.
353 */
354function wpcom_safecss_allow_additional_css_properties( $css_properties ) {
355    $css_properties[] = 'display';
356    $css_properties[] = 'transform';
357    $css_properties[] = 'position';
358
359    return $css_properties;
360}
361add_filter( 'safe_style_css', 'wpcom_safecss_allow_additional_css_properties', 10, 1 );
362
363/**
364 * Add guideline meta keys to the Jetpack sync post meta whitelist.
365 *
366 * @param array $whitelist Current post meta whitelist.
367 * @return array
368 */
369function wpcomsh_add_guideline_sync_meta_whitelist( $whitelist ) {
370    $guideline_meta_keys = array(
371        '_guideline_copy',
372        '_guideline_images',
373        '_guideline_site',
374        '_guideline_additional',
375        '_guideline_block_core_paragraph',
376        '_guideline_block_core_image',
377        '_guideline_block_core_heading',
378        '_guideline_block_core_list',
379        '_guideline_block_core_list_item',
380        '_guideline_block_core_quote',
381        '_guideline_block_core_code',
382        '_guideline_block_core_table',
383        '_guideline_block_core_video',
384        '_guideline_block_core_audio',
385        '_guideline_block_core_gallery',
386        '_guideline_block_core_cover',
387        '_guideline_block_core_pullquote',
388        '_guideline_block_core_preformatted',
389        '_guideline_block_core_verse',
390        '_guideline_block_core_button',
391        '_guideline_block_core_media_text',
392        '_guideline_block_core_freeform',
393        '_guideline_block_core_html',
394        '_guideline_block_core_embed',
395    );
396    return array_merge( $whitelist, $guideline_meta_keys );
397}
398add_filter( 'jetpack_sync_post_meta_whitelist', 'wpcomsh_add_guideline_sync_meta_whitelist' );