diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index e8647bf52653..511160e49db0 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -74,11 +74,11 @@ function has_blocks( $post = null ) { * @since 5.0.0 * @see parse_blocks() * - * @param string $block_type Full Block type to look for. + * @param string $block_name Full Block type to look for. * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. Defaults to global $post. * @return bool Whether the post content contains the specified block. */ -function has_block( $block_type, $post = null ) { +function has_block( $block_name, $post = null ) { if ( ! has_blocks( $post ) ) { return false; } @@ -90,7 +90,30 @@ function has_block( $block_type, $post = null ) { } } - return false !== strpos( $post, '', $serialized_block_name, $serialized_attributes ); + } + + return sprintf( + '%s', + $serialized_block_name, + $serialized_attributes, + $block_content, + $serialized_block_name + ); +} + +/** + * Returns the content of a block, including comment delimiters, serializing all + * attributes from the given parsed block. + * + * This should be used when preparing a block to be saved to post content. + * Prefer `render_block` when preparing a block for display. Unlike + * `render_block`, this does not evaluate a block's `render_callback`, and will + * instead preserve the markup as parsed. + * + * @since 5.3.1 + * + * @param WP_Block_Parser_Block $block A single parsed block object. + * @return string String of rendered HTML. + */ +function serialize_block( $block ) { + $block_content = ''; + + $index = 0; + foreach ( $block['innerContent'] as $chunk ) { + $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); + } + + if ( ! is_array( $block['attrs'] ) ) { + $block['attrs'] = array(); + } + + return get_comment_delimited_block_content( + $block['blockName'], + $block['attrs'], + $block_content + ); +} + +/** + * Returns a joined string of the aggregate serialization of the given parsed + * blocks. + * + * @since 5.3.1 + * + * @param WP_Block_Parser_Block[] $blocks Parsed block objects. + * @return string String of rendered HTML. + */ +function serialize_blocks( $blocks ) { + return implode( '', array_map( 'serialize_block', $blocks ) ); +} + +/** + * Filters and sanitizes block content to remove non-allowable HTML from + * parsed block attribute values. + * + * @since 5.3.1 + * + * @param string $text Text that may contain block content. + * @param array[]|string $allowed_html An array of allowed HTML elements + * and attributes, or a context name + * such as 'post'. + * @param string[] $allowed_protocols Array of allowed URL protocols. + * @return string The filtered and sanitized content result. + */ +function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) { + $result = ''; + + $blocks = parse_blocks( $text ); + foreach ( $blocks as $block ) { + $block = filter_block_kses( $block, $allowed_html, $allowed_protocols ); + $result .= serialize_block( $block ); + } + + return $result; +} + +/** + * Filters and sanitizes a parsed block to remove non-allowable HTML from block + * attribute values. + * + * @since 5.3.1 + * + * @param WP_Block_Parser_Block $block The parsed block object. + * @param array[]|string $allowed_html An array of allowed HTML + * elements and attributes, or a + * context name such as 'post'. + * @param string[] $allowed_protocols Allowed URL protocols. + * @return array The filtered and sanitized block object result. + */ +function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) { + $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols ); + + if ( is_array( $block['innerBlocks'] ) ) { + foreach ( $block['innerBlocks'] as $i => $inner_block ) { + $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols ); + } + } + + return $block; +} + +/** + * Filters and sanitizes a parsed block attribute value to remove non-allowable + * HTML. + * + * @since 5.3.1 + * + * @param mixed $value The attribute value to filter. + * @param array[]|string $allowed_html An array of allowed HTML elements + * and attributes, or a context name + * such as 'post'. + * @param string[] $allowed_protocols Array of allowed URL protocols. + * @return array The filtered and sanitized result. + */ +function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array() ) { + if ( is_array( $value ) ) { + foreach ( $value as $key => $inner_value ) { + $filtered_key = filter_block_kses_value( $key, $allowed_html, $allowed_protocols ); + $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols ); + + if ( $filtered_key !== $key ) { + unset( $value[ $key ] ); + } + + $value[ $filtered_key ] = $filtered_value; + } + } elseif ( is_string( $value ) ) { + return wp_kses( $value, $allowed_html, $allowed_protocols ); + } + + return $value; +} + /** * Parses blocks out of a content string, and renders those appropriate for the excerpt. * diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 26cdfeba20d5..50e66c15ef21 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -243,6 +243,7 @@ add_filter( 'tiny_mce_before_init', '_mce_set_direction' ); add_filter( 'teeny_mce_before_init', '_mce_set_direction' ); add_filter( 'pre_kses', 'wp_pre_kses_less_than' ); +add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 ); add_filter( 'sanitize_title', 'sanitize_title_with_dashes', 10, 3 ); add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); add_filter( 'comment_flood_filter', 'wp_throttle_comment_flood', 10, 3 ); diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 0147ff9d77b0..42d913e6d76d 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -4902,6 +4902,31 @@ function wp_pre_kses_less_than_callback( $matches ) { return $matches[0]; } +/** + * Remove non-allowable HTML from parsed block attribute values when filtering + * in the post context. + * + * @since 5.3.1 + * + * @param string $string Content to be run through KSES. + * @param array[]|string $allowed_html An array of allowed HTML elements + * and attributes, or a context name + * such as 'post'. + * @param string[] $allowed_protocols Array of allowed URL protocols. + * @return string Filtered text to run through KSES. + */ +function wp_pre_kses_block_attributes( $string, $allowed_html, $allowed_protocols ) { + /* + * `filter_block_content` is expected to call `wp_kses`. Temporarily remove + * the filter to avoid recursion. + */ + remove_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10 ); + $string = filter_block_content( $string, $allowed_html, $allowed_protocols ); + add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 ); + + return $string; +} + /** * WordPress implementation of PHP sprintf() with filters. * diff --git a/tests/phpunit/tests/blocks/block-type.php b/tests/phpunit/tests/blocks/block-type.php index 63e0809a13f7..57433e45af53 100644 --- a/tests/phpunit/tests/blocks/block-type.php +++ b/tests/phpunit/tests/blocks/block-type.php @@ -304,6 +304,24 @@ public function test_post_has_block() { $this->assertFalse( has_block( 'core/fake' ) ); } + public function test_post_has_block_serialized_name() { + $content = ''; + + $this->assertTrue( has_block( 'core/serialized', $content ) ); + + /* + * Technically, `has_block` should receive a "full" (normalized, parsed) + * block name. But this test conforms to expected pre-5.3.1 behavior. + */ + $this->assertTrue( has_block( 'serialized', $content ) ); + $this->assertTrue( has_block( 'core/normalized', $content ) ); + $this->assertTrue( has_block( 'normalized', $content ) ); + $this->assertFalse( has_block( 'plugin/normalized', $content ) ); + $this->assertFalse( has_block( 'plugin/serialized', $content ) ); + $this->assertFalse( has_block( 'third-party', $content ) ); + $this->assertFalse( has_block( 'core/third-party', $content ) ); + } + /** * Renders a test block without content. *