Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
87.34% |
69 / 79 |
|
50.00% |
4 / 8 |
CRAP | |
0.00% |
0 / 1 |
| Verbum_Block_Utils | |
87.34% |
69 / 79 |
|
50.00% |
4 / 8 |
37.48 | |
0.00% |
0 / 1 |
| remove_blocks | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
| has_disallowed_blocks | |
75.00% |
9 / 12 |
|
0.00% |
0 / 1 |
6.56 | |||
| remove_blocks_with_parse_blocks | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| filter_blocks_recursive | |
90.48% |
19 / 21 |
|
0.00% |
0 / 1 |
10.09 | |||
| filter_blocks | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
| render_verbum_blocks | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
| get_allowed_blocks | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
4.00 | |||
| should_show_verbum_comments | |
0.00% |
0 / 4 |
|
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 | */ |
| 11 | class 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 | } |