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    '7.2' => '24.3.0',
29);
30
31/**
32 * Attach the active-plugins filter only while core loads plugins, and only on the beta track.
33 *
34 * The filter is registered on `muplugins_loaded` and removed on `plugins_loaded`, so it only
35 * affects the read that core makes to load plugins. Outside that window the stored option is
36 * returned untouched, so the trimmed list can never be read and saved back to the database
37 * (which would deactivate the plugin permanently).
38 */
39function wpcomsh_register_ignore_outdated_gutenberg_plugin() {
40    if ( ! wpcomsh_is_wp_beta_version() ) {
41        return;
42    }
43
44    add_filter( 'option_active_plugins', 'wpcomsh_ignore_outdated_gutenberg_plugin' );
45    add_action(
46        'plugins_loaded',
47        static function () {
48            remove_filter( 'option_active_plugins', 'wpcomsh_ignore_outdated_gutenberg_plugin' );
49        },
50        0
51    );
52}
53
54/**
55 * Whether the site is running a pre-release (alpha/beta/RC) build of WordPress core.
56 *
57 * @return bool
58 */
59function wpcomsh_is_wp_beta_version() {
60    global $wp_version;
61
62    return is_string( $wp_version ) && (bool) preg_match( '/-(alpha|beta|RC)/i', $wp_version );
63}
64
65/**
66 * Returns the minimum Gutenberg version bundled in the running WordPress core, or null if unknown.
67 *
68 * @return string|null
69 */
70function wpcomsh_get_core_bundled_gutenberg_version() {
71    global $wp_version;
72
73    if ( ! is_string( $wp_version ) || ! preg_match( '/^(\d+\.\d+)/', $wp_version, $matches ) ) {
74        return null;
75    }
76
77    return WPCOMSH_CORE_BUNDLED_GUTENBERG_VERSIONS[ $matches[1] ] ?? null;
78}
79
80/**
81 * Returns the installed Gutenberg plugin version, or null if it can't be read.
82 *
83 * Reads the plugin header, as this runs before the plugin (and GUTENBERG_VERSION) loads.
84 *
85 * @return string|null
86 */
87function wpcomsh_get_installed_gutenberg_plugin_version() {
88    $plugin_file = WP_PLUGIN_DIR . '/gutenberg/gutenberg.php';
89
90    if ( ! is_readable( $plugin_file ) ) {
91        return null;
92    }
93
94    $data = get_file_data( $plugin_file, array( 'Version' => 'Version' ) );
95
96    return empty( $data['Version'] ) ? null : $data['Version'];
97}
98
99/**
100 * Whether the plugin should be ignored: the site is on the beta track and the installed
101 * plugin is older than core's bundle (both versions must be known).
102 *
103 * @param string|null $plugin_version      Installed Gutenberg plugin version.
104 * @param string|null $min_bundled_version Minimum Gutenberg version bundled in the running core.
105 * @return bool
106 */
107function wpcomsh_should_ignore_gutenberg_plugin( $plugin_version, $min_bundled_version ) {
108    if ( ! wpcomsh_is_wp_beta_version() || null === $plugin_version || null === $min_bundled_version ) {
109        return false;
110    }
111
112    return version_compare( $plugin_version, $min_bundled_version, '<' );
113}
114
115/**
116 * Whether the installed Gutenberg plugin is being ignored in favor of core's bundled Gutenberg.
117 *
118 * @return bool
119 */
120function wpcomsh_is_gutenberg_plugin_ignored() {
121    return wpcomsh_should_ignore_gutenberg_plugin(
122        wpcomsh_get_installed_gutenberg_plugin_version(),
123        wpcomsh_get_core_bundled_gutenberg_version()
124    );
125}
126
127/**
128 * Drop the Gutenberg plugin from the active plugins list when it is older than core's bundle.
129 *
130 * The stored option is left untouched, so the plugin returns once it is updated past the bundle.
131 *
132 * @param mixed $plugins Value of the active_plugins option.
133 * @return mixed
134 */
135function wpcomsh_ignore_outdated_gutenberg_plugin( $plugins ) {
136    if ( ! is_array( $plugins ) ) {
137        return $plugins;
138    }
139
140    $key = array_search( 'gutenberg/gutenberg.php', $plugins, true );
141    if ( false === $key || ! wpcomsh_is_gutenberg_plugin_ignored() ) {
142        return $plugins;
143    }
144
145    unset( $plugins[ $key ] );
146
147    return array_values( $plugins );
148}
149
150/**
151 * Disable all Gutenberg experiments except explicitly allowed ones.
152 *
153 * @see https://github.com/WordPress/gutenberg/blob/e6d8284b03799136915495654e821ca6212ae6d8/lib/load.php#L22
154 */
155function wpcomsh_remove_gutenberg_experiments() {
156    $jetpack_options = get_option( 'jetpack_options' );
157    if ( is_array( $jetpack_options ) && isset( $jetpack_options['id'] ) ) {
158        $blog_id = (int) $jetpack_options['id'];
159    } else {
160        $blog_id = get_current_blog_id();
161    }
162
163    $allowed_blogs = array(
164        211453162, // wpmovies.dev
165    );
166
167    if ( in_array( $blog_id, $allowed_blogs, true ) ) {
168        // Undo the early filters for allowed blogs so they can still manage Gutenberg experiments.
169        remove_filter( 'default_option_gutenberg-experiments', 'wpcomsh_filter_gutenberg_experiments' );
170        remove_filter( 'option_gutenberg-experiments', 'wpcomsh_filter_gutenberg_experiments' );
171        return;
172    }
173    add_action( 'admin_menu', 'wpcomsh_remove_gutenberg_experimental_menu' );
174}
175add_action( 'init', 'wpcomsh_remove_gutenberg_experiments' );
176
177/**
178 * Disable all Gutenberg experiments except explicitly allowed ones.
179 *
180 * Allowed experiments are always enabled regardless of the option value in the database.
181 *
182 * @return array List of the enabled experiments.
183 */
184function wpcomsh_filter_gutenberg_experiments() {
185    return array(
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( 'options-general.php', 'experiments-wp-admin' );
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 = 70; // 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' );