Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions src/wp-includes/formatting.php
Original file line number Diff line number Diff line change
Expand Up @@ -660,13 +660,33 @@ function get_html_split_regex() {
. $cdata
. ')';

// Keep in sync with the element pattern in _get_wptexturize_split_regex().
$element =
'(?(?=[a-zA-Z])' // Does a tag name follow?
. '(?:' // Unroll the loop: Consume everything until > is found.
. '[^>=]++' // Consume anything but > and =.
. '|'
. '=[ \t\f\r\n]*' // Attribute value follows.
. '(?:'
. '"[^"]*"' // Double-quoted value, which may contain >.
. '|'
. '\'[^\']*\'' // Single-quoted value, which may contain >.
. '|'
. '[^> \t\f\r\n]*+' // Unquoted value.
. ')'
. ')*+' // Loop possessively.
. '|' // ...else not a tag, so quotes are not special.
. '[^>]*+'
. ')'
. '>?'; // End of element. If not found, match all input.

$regex =
'/(' // Capture the entire match.
. '<' // Find start of element.
. '(?' // Conditional expression follows.
. $escaped // Find end of escaped element.
. '|' // ...else...
. '[^>]*>?' // Find end of normal element.
. $element // Find end of element, allowing > inside quoted attribute values.
. ')'
. ')/';
// phpcs:enable
Expand Down Expand Up @@ -699,12 +719,32 @@ function _get_wptexturize_split_regex( $shortcode_regex = '' ) {
. ')*+' // Loop possessively.
. '(?:-->)?'; // End of comment. If not found, match all input.

// Keep in sync with the element pattern in get_html_split_regex().
$element =
'(?(?=[a-zA-Z])' // Does a tag name follow?
. '(?:' // Unroll the loop: Consume everything until > is found.
. '[^>=]++' // Consume anything but > and =.
. '|'
. '=[ \t\f\r\n]*' // Attribute value follows.
. '(?:'
. '"[^"]*"' // Double-quoted value, which may contain >.
. '|'
. '\'[^\']*\'' // Single-quoted value, which may contain >.
. '|'
. '[^> \t\f\r\n]*+' // Unquoted value.
. ')'
. ')*+' // Loop possessively.
. '|' // ...else not a tag, so quotes are not special.
. '[^>]*+'
. ')'
. '>?'; // End of element. If not found, match all input.

$html_regex = // Needs replaced with wp_html_split() per Shortcode API Roadmap.
'<' // Find start of element.
. '(?(?=!--)' // Is this a comment?
. $comment_regex // Find end of comment.
. '|'
. '[^>]*>?' // Find end of element. If not found, match all input.
. $element // Find end of element.
. ')';
// phpcs:enable
}
Expand Down
132 changes: 132 additions & 0 deletions tests/phpunit/tests/formatting/wpHtmlSplit.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,138 @@ public static function data_basic_features() {
);
}

/**
* Ensure that > inside quoted attribute values does not cause premature
* tag splitting in wp_html_split().
*
* @ticket 63997
* @ticket 57381
* @ticket 63426
*
* @dataProvider data_gt_in_quoted_attribute_values
*
* @param non-falsy-string $input Input HTML.
* @param list<string> $output Expected split.
*/
public function test_gt_in_quoted_attribute_values( string $input, array $output ): void {
$this->assertSame( $output, wp_html_split( $input ) );
}

/**
* Data provider for {@see self::test_gt_in_quoted_attribute_values()}.
*
* @return list<array{ 0: non-falsy-string, 1: list<string> }>
*/
public static function data_gt_in_quoted_attribute_values(): array {
return array(
array(
'<div data-test="a > b">content</div>',
array( '', '<div data-test="a > b">', 'content', '</div>', '' ),
),
array(
'<div data-test=\'a > b\'>content</div>',
array( '', '<div data-test=\'a > b\'>', 'content', '</div>', '' ),
),
array(
'<div data-test="a > b" data-other="c &gt; d">content</div>',
array( '', '<div data-test="a > b" data-other="c &gt; d">', 'content', '</div>', '' ),
),
array(
'<div data-template="Label <% value %>" data-label="{A \'message\' string}">Content</div>',
array( '', '<div data-template="Label <% value %>" data-label="{A \'message\' string}">', 'Content', '</div>', '' ),
),
array(
'<iframe srcdoc="<body></body>"></iframe>',
array( '', '<iframe srcdoc="<body></body>">', '', '</iframe>', '' ),
),
array(
'<div class="swiper [&>.swiper-pagination]:static">content</div>',
array( '', '<div class="swiper [&>.swiper-pagination]:static">', 'content', '</div>', '' ),
),
);
}

/**
* Ensure that a quote in an unquoted attribute value does not pair with a
* quote after the tag.
*
* @ticket 63997
*/
public function test_quote_in_unquoted_attribute_value_does_not_pair_across_tag(): void {
$input = "<p>Hi</p>\n<a href=\"/x\" title=it's>It's here</a>";

$this->assertSame(
array( '', '<p>', 'Hi', '</p>', "\n", '<a href="/x" title=it\'s>', 'It\'s here', '</a>', '' ),
wp_html_split( $input )
);
}

/**
* Ensure that an unclosed tag matches the remaining input.
*
* @ticket 63997
*/
public function test_unclosed_tag_matches_remaining_input(): void {
$input = "<p>Truncated <a href=\"http://example.com/it's";

$this->assertSame(
array( '', '<p>', 'Truncated ', '<a href="http://example.com/it\'s', '' ),
wp_html_split( $input )
);
}

/**
* Ensure that a quote in an attribute name does not extend the tag past the
* next >.
*
* @ticket 63997
*/
public function test_quote_in_attribute_name_does_not_extend_tag(): void {
$this->assertSame(
array( '', '<div "=noquote >', ' id="quote">' ),
wp_html_split( '<div "=noquote > id="quote">' )
);
}

/**
* Ensure that a quote-named attribute splits the same as before.
*
* @ticket 63997
*/
public function test_quote_named_attribute_is_one_element(): void {
$this->assertSame(
array( '', '<div "=noquote id="quote">', 'x' ),
wp_html_split( '<div "=noquote id="quote">x' )
);
}

/**
* Ensure that a quote after a < that does not start a tag does not pair
* with a quote after the next >.
*
* @ticket 63997
*/
public function test_quote_after_non_tag_lt_does_not_pair_across_gt(): void {
$this->assertSame(
array( '', '<p>', 'x', "<=' then</p>", '', '<p>', "don't", '</p>', '' ),
wp_html_split( "<p>x<=' then</p><p>don't</p>" )
);
}

/**
* Ensure that a CDATA section containing > is a single element.
*
* @ticket 63997
*/
public function test_cdata_with_gt_is_one_element(): void {
$input = '<script>/*<![CDATA[*/ if (a > b && c) {} /*]]>*/</script>';

$this->assertSame(
array( '', '<script>', '/*', '<![CDATA[*/ if (a > b && c) {} /*]]>', '*/', '</script>', '' ),
wp_html_split( $input )
);
}

/**
* Automated performance testing of the main regex.
*
Expand Down
117 changes: 117 additions & 0 deletions tests/phpunit/tests/formatting/wpTexturize.php
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,123 @@ public function test_trailing_less_than() {
$this->assertSame( 'F&#8211;oo<', wptexturize( 'F--oo<', true ) );
}

/**
* Ensure that > inside quoted attribute values does not cause premature
* tag splitting, which would leave quotes exposed to texturization.
*
* @ticket 63997
* @ticket 57381
* @ticket 63426
*
* @dataProvider data_gt_in_quoted_attribute_values
*
* @param string $input Input HTML.
* @param string $output Expected output.
*/
public function test_gt_in_quoted_attribute_values( string $input, string $output ): void {
$this->assertSame( $output, wptexturize( $input ) );
}

/**
* Data provider for {@see self::test_gt_in_quoted_attribute_values()}.
*
* @return list<array{0: string, 1: string}>
*/
public static function data_gt_in_quoted_attribute_values(): array {
return array(
array(
'<div data-test=\'a > 0 ? "yes" : "no"\'>content</div>',
'<div data-test=\'a > 0 ? "yes" : "no"\'>content</div>',
),
array(
"<div data-test=\"a > 0 ? 'yes' : 'no'\">content</div>",
"<div data-test=\"a > 0 ? 'yes' : 'no'\">content</div>",
),
array(
'<div data-a="x > y" data-b="z">content</div>',
'<div data-a="x > y" data-b="z">content</div>',
),
array(
'<div data-test=\'a &gt; 0 ? "yes" : "no"\'>content</div>',
'<div data-test=\'a &gt; 0 ? "yes" : "no"\'>content</div>',
),
array(
'<div data-template="Label <% value %>" data-label="{A \'message\' string}">Content</div>',
'<div data-template="Label <% value %>" data-label="{A \'message\' string}">Content</div>',
),
array(
'<iframe srcdoc="<body></body>"></iframe>',
'<iframe srcdoc="<body></body>"></iframe>',
),
array(
'<div class="swiper [&>.swiper-pagination]:static">content</div>',
'<div class="swiper [&#038;>.swiper-pagination]:static">content</div>',
),
);
}

/**
* Ensure that an ampersand in a quoted attribute value is still encoded.
*
* @ticket 57381
*/
public function test_ampersand_in_quoted_attribute_value_is_encoded(): void {
$this->assertSame(
'<a href="?a[]=1&#038;a[]=2">x</a>',
wptexturize( '<a href="?a[]=1&a[]=2">x</a>' )
);
}

/**
* Ensure that a quote in an unquoted attribute value does not pair with a
* quote after the tag and swallow the following text.
*
* @ticket 63997
*/
public function test_quote_in_unquoted_attribute_value_does_not_pair_across_tag(): void {
$input = "<p>Hi</p>\n<a href=\"/x\" title=it's>It's here</a>";

$this->assertSame(
"<p>Hi</p>\n<a href=\"/x\" title=it's>It&#8217;s here</a>",
wptexturize( $input )
);
}

/**
* Ensure that a quote after a < that does not start a tag does not pair
* with a quote in the following text.
*
* @ticket 63997
*/
public function test_quote_after_non_tag_lt_does_not_pair_across_gt(): void {
$this->assertSame(
"<p>x<=' then</p><p>don&#8217;t</p>",
wptexturize( "<p>x<=' then</p><p>don't</p>" )
);
}

/**
* Ensure that an unclosed tag is not texturized.
*
* @ticket 63997
*/
public function test_unclosed_tag_is_not_texturized(): void {
$input = "<p>Truncated <a href=\"http://example.com/it's";

$this->assertSame( $input, wptexturize( $input ) );
}

/**
* Ensure that an ampersand inside a CDATA section is not encoded.
*
* @ticket 63997
*/
public function test_ampersand_in_cdata_is_not_encoded(): void {
$input = '<script>/*<![CDATA[*/ if (a > b && c) {} /*]]>*/</script>';

$this->assertSame( $input, wptexturize( $input ) );
}

public static function data_whole_posts() {
require_once DIR_TESTDATA . '/formatting/whole-posts.php';
return data_whole_posts();
Expand Down
Loading