Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.34% covered (warning)
87.34%
69 / 79
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Verbum_Block_Utils
87.34% covered (warning)
87.34%
69 / 79
50.00% covered (danger)
50.00%
4 / 8
37.48
0.00% covered (danger)
0.00%
0 / 1
 remove_blocks
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 has_disallowed_blocks
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
6.56
 remove_blocks_with_parse_blocks
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 filter_blocks_recursive
90.48% covered (success)
90.48%
19 / 21
0.00% covered (danger)
0.00%
0 / 1
10.09
 filter_blocks
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 render_verbum_blocks
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 get_allowed_blocks
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
4.00
 should_show_verbum_comments
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * Verbum Block Utils
4 *
5 * @package automattic/jetpack-mu-plugins
6 */
7
8/**
9 * Verbum_Block_Utils offer utility functions for sanitizing and parsing blocks.
10 */
11class Verbum_Block_Utils {
12    /**
13     * Remove blocks that aren't allowed using hybrid Block_Scanner optimization
14     *
15     * Uses Block_Scanner for fast pre-filtering when possible, falling back to
16     * parse_blocks approach only when disallowed blocks are detected.
17     *
18     * @param string $content - Text of the comment.
19     * @return string
20     */
21    public static function remove_blocks( $content ) {
22        if ( ! has_blocks( $content ) ) {
23            return $content;
24        }
25
26        if ( ! self::has_disallowed_blocks( $content ) ) {
27            return $content;
28        }
29
30        // Unslash for parse_blocks: slashed JSON attributes can't be parsed.
31        // Re-slash after: pre_comment_content filters must return slashed data.
32        return wp_slash( self::remove_blocks_with_parse_blocks( wp_unslash( $content ) ) );
33    }
34
35    /**
36     * Quick verification using Block_Scanner to detect if content contains disallowed blocks
37     *
38     * This method provides significant performance benefits by avoiding expensive
39     * parse_blocks() processing when all blocks are allowed (the common case).
40     *
41     * @param string $content Content to scan (may be slashed).
42     * @return bool True if disallowed blocks found, false if all blocks are allowed.
43     */
44    private static function has_disallowed_blocks( $content ) {
45        if ( ! class_exists( '\\Automattic\\Block_Scanner' ) ) {
46            return true;
47        }
48
49        try {
50            $scanner        = \Automattic\Block_Scanner::create( $content );
51            $allowed_blocks = self::get_allowed_blocks();
52
53            while ( $scanner->next_delimiter() ) {
54                if ( $scanner->opens_block() ) {
55                    $block_type = $scanner->get_block_type();
56                    if ( ! in_array( $block_type, $allowed_blocks, true ) ) {
57                        return true; // Found disallowed block
58                    }
59                }
60            }
61
62            return false; // All blocks are allowed
63        } catch ( \Exception $e ) {
64            return true;
65        }
66    }
67
68    /**
69     * Remove disallowed blocks using parse_blocks
70     *
71     * @param string $unslashed_content Content with blocks (already unslashed).
72     * @return string Filtered content with disallowed blocks removed.
73     */
74    private static function remove_blocks_with_parse_blocks( $unslashed_content ) {
75        $blocks          = parse_blocks( $unslashed_content );
76        $filtered_blocks = self::filter_blocks_recursive( $blocks );
77        return serialize_blocks( $filtered_blocks );
78    }
79
80    /**
81     * Recursively filter blocks and their inner blocks
82     *
83     * @param array $blocks Array of blocks to filter.
84     * @return array Filtered blocks.
85     */
86    private static function filter_blocks_recursive( $blocks ) {
87        $allowed_blocks = self::get_allowed_blocks();
88        $filtered       = array();
89
90        foreach ( $blocks as $block ) {
91            if ( ! in_array( $block['blockName'], $allowed_blocks, true ) ) {
92                continue;
93            }
94
95            if ( ! empty( $block['innerBlocks'] ) ) {
96                $block['innerBlocks'] = self::filter_blocks_recursive( $block['innerBlocks'] );
97
98                // Reconstruct innerContent to match filtered innerBlocks
99                $inner_content = array();
100                $block_index   = 0;
101                foreach ( $block['innerContent'] as $chunk ) {
102                    if ( is_string( $chunk ) ) {
103                        $inner_content[] = $chunk;
104                    } elseif ( isset( $block['innerBlocks'][ $block_index ] ) ) {
105                        $inner_content[] = null;
106                        ++$block_index;
107                    }
108                }
109                $block['innerContent'] = $inner_content;
110            }
111
112            $block['innerHTML'] = isset( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) ? $block['innerHTML'] : '';
113
114            if ( empty( $block['innerContent'] ) ) {
115                $block['innerContent'] = array( $block['innerHTML'] );
116            }
117
118            $filtered[] = $block;
119        }
120
121        return $filtered;
122    }
123
124    /**
125     * Filter blocks from content according to our allowed blocks
126     *
127     * @param string $content - The content to be processed.
128     * @return array
129     */
130    private static function filter_blocks( $content ) {
131        $registry       = new WP_Block_Type_Registry();
132        $allowed_blocks = self::get_allowed_blocks();
133
134        foreach ( $allowed_blocks as $allowed_block ) {
135            $registry->register( $allowed_block );
136        }
137
138        $filtered_blocks = array();
139        $blocks          = parse_blocks( $content );
140
141        foreach ( $blocks as $block ) {
142            $filtered_blocks[] = new WP_Block( $block, array(), $registry );
143        }
144
145        return $filtered_blocks;
146    }
147
148    /**
149     * Render blocks in the comment content
150     * Filters blocks that aren't allowed
151     *
152     * @param string $comment_content - Text of the comment.
153     * @return string
154     */
155    public static function render_verbum_blocks( $comment_content ) {
156        if ( ! has_blocks( $comment_content ) ) {
157            return $comment_content;
158        }
159
160        $blocks          = self::filter_blocks( $comment_content );
161        $comment_content = '';
162
163        foreach ( $blocks as $block ) {
164            $comment_content .= $block->render();
165        }
166
167        return $comment_content;
168    }
169
170    /**
171     * Get a list of allowed blocks by looking at the allowed comment tags
172     *
173     * @return string[]
174     */
175    public static function get_allowed_blocks() {
176        global $allowedtags;
177
178        // Validate $allowedtags integrity - use local variable to avoid override warning
179        $validated_allowedtags = $allowedtags;
180        if ( ! is_array( $validated_allowedtags ) ) {
181            $validated_allowedtags = wp_kses_allowed_html( 'post' );
182        }
183
184        $allowed_blocks = array( 'core/paragraph', 'core/list', 'core/code', 'core/list-item', 'core/quote', 'core/image', 'core/embed' );
185        $convert        = array(
186            'blockquote' => 'core/quote',
187            'h1'         => 'core/heading',
188            'h2'         => 'core/heading',
189            'h3'         => 'core/heading',
190            'img'        => 'core/image',
191            'ul'         => 'core/list',
192            'ol'         => 'core/list',
193            'pre'        => 'core/code',
194        );
195
196        foreach ( array_keys( $validated_allowedtags ) as $tag ) {
197            if ( isset( $convert[ $tag ] ) ) {
198                $allowed_blocks[] = $convert[ $tag ];
199            }
200        }
201
202        return $allowed_blocks;
203    }
204
205    /**
206     * Check if we should show the Verbum comments.
207     *
208     * This is used to determine if the Verbum comments should be shown on the current page.
209     *
210     * @return bool
211     */
212    public static function should_show_verbum_comments() {
213        return (
214            ( is_singular() && comments_open() )
215            || ( is_front_page() && is_page() && comments_open() )
216        );
217    }
218}