Skip to content

Commit cd15410

Browse files
authored
Ensure block comments are always sanitized on save and render (#35246)
1 parent ea54fd3 commit cd15410

File tree

5 files changed

+210
-86
lines changed

5 files changed

+210
-86
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: security
3+
4+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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
14+
*
15+
* @param string $content - Text of the comment.
16+
* @return string
17+
*/
18+
public static function remove_blocks( $content ) {
19+
if ( ! has_blocks( $content ) ) {
20+
return $content;
21+
}
22+
23+
$allowed_blocks = self::get_allowed_blocks();
24+
// The block attributes come slashed and `parse_blocks` won't be able to parse them.
25+
$content = wp_unslash( $content );
26+
$blocks = parse_blocks( $content );
27+
$output = '';
28+
29+
foreach ( $blocks as $block ) {
30+
if ( in_array( $block['blockName'], $allowed_blocks, true ) ) {
31+
$output .= serialize_block( $block );
32+
}
33+
}
34+
35+
return ltrim( $output );
36+
}
37+
38+
/**
39+
* Filter blocks from content according to our allowed blocks
40+
*
41+
* @param string $content - The content to be processed.
42+
* @return array
43+
*/
44+
private static function filter_blocks( $content ) {
45+
$registry = new WP_Block_Type_Registry();
46+
$allowed_blocks = self::get_allowed_blocks();
47+
48+
foreach ( $allowed_blocks as $allowed_block ) {
49+
$registry->register( $allowed_block );
50+
}
51+
52+
$filtered_blocks = array();
53+
$blocks = parse_blocks( $content );
54+
55+
foreach ( $blocks as $block ) {
56+
$filtered_blocks[] = new WP_Block( $block, array(), $registry );
57+
}
58+
59+
return $filtered_blocks;
60+
}
61+
62+
/**
63+
* Render blocks in the comment content
64+
* Filters blocks that aren't allowed
65+
*
66+
* @param string $comment_content - Text of the comment.
67+
* @return string
68+
*/
69+
public static function render_verbum_blocks( $comment_content ) {
70+
if ( ! has_blocks( $comment_content ) ) {
71+
return $comment_content;
72+
}
73+
74+
$blocks = self::filter_blocks( $comment_content );
75+
$comment_content = '';
76+
77+
foreach ( $blocks as $block ) {
78+
$comment_content .= $block->render();
79+
}
80+
81+
return $comment_content;
82+
}
83+
84+
/**
85+
* Get a list of allowed blocks by looking at the allowed comment tags
86+
*
87+
* @return string[]
88+
*/
89+
public static function get_allowed_blocks() {
90+
global $allowedtags;
91+
92+
$allowed_blocks = array( 'core/paragraph', 'core/list', 'core/code', 'core/list-item', 'core/quote', 'core/image', 'core/embed' );
93+
$convert = array(
94+
'blockquote' => 'core/quote',
95+
'h1' => 'core/heading',
96+
'h2' => 'core/heading',
97+
'h3' => 'core/heading',
98+
'img' => 'core/image',
99+
'ul' => 'core/list',
100+
'ol' => 'core/list',
101+
'pre' => 'core/code',
102+
);
103+
104+
foreach ( array_keys( $allowedtags ) as $tag ) {
105+
if ( isset( $convert[ $tag ] ) ) {
106+
$allowed_blocks[] = $convert[ $tag ];
107+
}
108+
}
109+
110+
return $allowed_blocks;
111+
}
112+
}

projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-gutenberg-editor.php

+5-82
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
declare( strict_types = 1 );
99

10+
require_once __DIR__ . '/class-verbum-block-utils.php';
11+
1012
/**
1113
* Verbum_Gutenberg_Editor is responsible for loading the Gutenberg editor for comments.
1214
*
@@ -29,10 +31,11 @@ function () {
2931
},
3032
9999
3133
);
34+
3235
add_filter( 'init', array( $this, 'remove_strict_kses_filters' ) );
33-
add_filter( 'comment_text', array( $this, 'render_verbum_blocks' ) );
34-
add_filter( 'pre_comment_content', array( $this, 'remove_blocks' ) );
3536
add_filter( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
37+
add_filter( 'comment_text', array( \Verbum_Block_Utils::class, 'render_verbum_blocks' ) );
38+
add_filter( 'pre_comment_content', array( \Verbum_Block_Utils::class, 'remove_blocks' ) );
3639
}
3740

3841
/**
@@ -59,84 +62,4 @@ public function enqueue_assets() {
5962
$vbe_cache_buster
6063
);
6164
}
62-
63-
/**
64-
* Render blocks in the comment content
65-
* Filters blocks that aren't allowed
66-
*
67-
* @param string $comment_content - Text of the comment.
68-
* @return string
69-
*/
70-
public function render_verbum_blocks( $comment_content ) {
71-
if ( ! has_blocks( $comment_content ) ) {
72-
return $comment_content;
73-
}
74-
75-
$blocks = parse_blocks( $comment_content );
76-
$comment_content = '';
77-
78-
$allowed_blocks = self::get_allowed_blocks();
79-
foreach ( $blocks as $block ) {
80-
if ( in_array( $block['blockName'], $allowed_blocks, true ) ) {
81-
$comment_content .= render_block( $block );
82-
}
83-
}
84-
85-
return $comment_content;
86-
}
87-
88-
/**
89-
* Remove blocks that aren't allowed
90-
*
91-
* @param string $content - Text of the comment.
92-
* @return string
93-
*/
94-
public function remove_blocks( $content ) {
95-
if ( ! has_blocks( $content ) ) {
96-
return $content;
97-
}
98-
99-
$allowed_blocks = self::get_allowed_blocks();
100-
// The block attributes come slashed and `parse_blocks` won't be able to parse them.
101-
$content = wp_unslash( $content );
102-
$blocks = parse_blocks( $content );
103-
$output = '';
104-
105-
foreach ( $blocks as $block ) {
106-
if ( in_array( $block['blockName'], $allowed_blocks, true ) ) {
107-
$output .= serialize_block( $block );
108-
}
109-
}
110-
111-
return ltrim( $output );
112-
}
113-
114-
/**
115-
* Get a list of allowed blocks by looking at the allowed comment tags
116-
*
117-
* @return string[]
118-
*/
119-
public static function get_allowed_blocks() {
120-
global $allowedtags;
121-
122-
$allowed_blocks = array( 'core/paragraph', 'core/list', 'core/code', 'core/list-item', 'core/quote', 'core/image', 'core/embed' );
123-
$convert = array(
124-
'blockquote' => 'core/quote',
125-
'h1' => 'core/heading',
126-
'h2' => 'core/heading',
127-
'h3' => 'core/heading',
128-
'img' => 'core/image',
129-
'ul' => 'core/list',
130-
'ol' => 'core/list',
131-
'pre' => 'core/code',
132-
);
133-
134-
foreach ( array_keys( $allowedtags ) as $tag ) {
135-
if ( isset( $convert[ $tag ] ) ) {
136-
$allowed_blocks[] = $convert[ $tag ];
137-
}
138-
}
139-
140-
return $allowed_blocks;
141-
}
14265
}

projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php

+5-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
require_once __DIR__ . '/assets/class-wpcom-rest-api-v2-verbum-auth.php';
1414
require_once __DIR__ . '/assets/class-wpcom-rest-api-v2-verbum-oembed.php';
1515
require_once __DIR__ . '/assets/class-verbum-gutenberg-editor.php';
16+
require_once __DIR__ . '/assets/class-verbum-block-utils.php';
1617

1718
/**
1819
* Verbum Comments Experience
@@ -247,7 +248,7 @@ public function enqueue_assets() {
247248
'enableSubscriptionModal' => boolval( $this->should_show_subscription_modal() ),
248249
'currentLocale' => $locale,
249250
'isJetpackComments' => is_jetpack_comments(),
250-
'allowedBlocks' => \Verbum_Gutenberg_Editor::get_allowed_blocks(),
251+
'allowedBlocks' => \Verbum_Block_Utils::get_allowed_blocks(),
251252
'embedNonce' => wp_create_nonce( 'embed_nonce' ),
252253
'verbumBundleUrl' => plugins_url( 'dist/index.js', __FILE__ ),
253254
'isRTL' => is_rtl( $locale ),
@@ -560,9 +561,9 @@ public function should_load_gutenberg_comments() {
560561
return false;
561562
}
562563

563-
$blog_id = verbum_get_blog_id();
564-
$e2e_tests = has_blog_sticker( 'a8c-e2e-test-blog', $blog_id );
565-
$has_blocks_flag = has_blog_sticker( 'verbum-block-comments', $blog_id );
564+
$blog_id = $this->blog_id;
565+
$e2e_tests = function_exists( 'has_blog_sticker' ) && has_blog_sticker( 'a8c-e2e-test-blog', $blog_id );
566+
$has_blocks_flag = function_exists( 'has_blog_sticker' ) && has_blog_sticker( 'verbum-block-comments', $blog_id );
566567
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
567568
$gutenberg_query_param = isset( $_GET['verbum_gutenberg'] ) ? intval( $_GET['verbum_gutenberg'] ) : null;
568569
// This will release to 50% of sites.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
/**
3+
* Test class for Verbum_Block_Utils.
4+
*
5+
* @package automattic/jetpack-mu-wpcom
6+
*/
7+
8+
use Automattic\Jetpack\Jetpack_Mu_Wpcom;
9+
require_once Jetpack_Mu_Wpcom::PKG_DIR . 'src/features/verbum-comments/assets/class-verbum-block-utils.php';
10+
11+
/**
12+
* Test class for Verbum_Block_Utils.
13+
*
14+
* @coversDefaultClass Verbum_Block_Utils
15+
*/
16+
class Verbum_Block_Utils_Test extends \WorDBless\BaseTestCase {
17+
/**
18+
* Ensure string comments are not modified when 'render_verbum_blocks' is applied
19+
*
20+
* @covers Verbum_Block_Utils::render_verbum_blocks
21+
*/
22+
public function test_comment_text_string_comment() {
23+
$comment_content = 'This is a test comment';
24+
$filtered_content = Verbum_Block_Utils::render_verbum_blocks( $comment_content );
25+
$this->assertEquals( $comment_content, $filtered_content );
26+
}
27+
28+
/**
29+
* Ensure blocks are filtered when 'render_verbum_blocks' is applied
30+
*
31+
* @covers Verbum_Block_Utils::render_verbum_blocks
32+
*/
33+
public function test_comment_text_block_sanitization() {
34+
$comment_content = '<!-- wp:paragraph -->Testing<!-- /wp:paragraph --><!-- wp:latest-posts -->';
35+
$filtered_content = Verbum_Block_Utils::render_verbum_blocks( $comment_content );
36+
$this->assertEquals( 'Testing', $filtered_content );
37+
}
38+
39+
/**
40+
* Ensure blocks are rendered properly
41+
*
42+
* @covers Verbum_Block_Utils::render_verbum_blocks
43+
*/
44+
public function test_comment_text_block_sanitization_sanity_check() {
45+
$comment_content = '<!-- wp:paragraph --><p>test</p><!-- /wp:paragraph --><!-- wp:list --><ul><!-- wp:list-item --><li>1</li><!-- /wp:list-item --><!-- wp:list-item --><li>2</li><!-- /wp:list-item --><!-- wp:list-item --><li>3</li><!-- /wp:list-item --></ul><!-- /wp:list --><!-- wp:quote --><blockquote class="wp-block-quote"><!-- wp:paragraph --><p>something</p><!-- /wp:paragraph --><cite>someone</cite></blockquote><!-- /wp:quote -->';
46+
$filtered_content = preg_replace( '/\R+/', '', Verbum_Block_Utils::render_verbum_blocks( $comment_content ) );
47+
48+
$expected_content = '<p>test</p><ul><li>1</li><li>2</li><li>3</li></ul><blockquote class="wp-block-quote"><p>something</p><cite>someone</cite></blockquote>';
49+
$this->assertEquals( $expected_content, $filtered_content );
50+
}
51+
52+
/**
53+
* Ensure innerBlocks are filtered when 'render_verbum_blocks' is applied
54+
*
55+
* @covers Verbum_Block_Utils::render_verbum_blocks
56+
*/
57+
public function test_comment_text_block_sanitization_inner_blocks() {
58+
$comment_content = '<!-- wp:paragraph {} --><!-- wp:latest-posts --><!-- /wp:paragraph -->';
59+
$filtered_content = Verbum_Block_Utils::render_verbum_blocks( $comment_content );
60+
$this->assertSame( '', $filtered_content );
61+
}
62+
63+
/**
64+
* Ensure string comments are not modified when 'pre_comment_content' is applied
65+
*
66+
* @covers Verbum_Block_Utils::remove_blocks
67+
*/
68+
public function test_pre_comment_content_string_comment() {
69+
$comment_content = 'This is a test comment';
70+
$filtered_content = Verbum_Block_Utils::remove_blocks( $comment_content );
71+
$this->assertEquals( $comment_content, $filtered_content );
72+
}
73+
74+
/**
75+
* Ensure blocks are filtered when 'pre_comment_content' is applied
76+
*
77+
* @covers Verbum_Block_Utils::remove_blocks
78+
*/
79+
public function test_pre_comment_content__block_sanitization() {
80+
$comment_content = '<!-- wp:paragraph -->Testing<!-- /wp:paragraph --><!-- wp:latest-posts -->';
81+
$filtered_content = Verbum_Block_Utils::remove_blocks( $comment_content );
82+
$this->assertEquals( '<!-- wp:paragraph -->Testing<!-- /wp:paragraph -->', $filtered_content );
83+
}
84+
}

0 commit comments

Comments
 (0)