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
36 changes: 26 additions & 10 deletions src/wp-includes/html-api/class-wp-html-tag-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,15 @@ class WP_HTML_Tag_Processor {
*/
private $token_length;

/**
* Whether the current tag token has the self-closing flag.
*
* @since 7.1.0
*
* @var bool
*/
private $self_closing_flag = false;

/**
* Byte offset in input document where current tag name starts.
*
Expand Down Expand Up @@ -1079,6 +1088,7 @@ private function base_class_next_token(): bool {
$tag_name_starts_at = $this->tag_name_starts_at;
$tag_name_length = $this->tag_name_length;
$tag_ends_at = $this->token_starts_at + $this->token_length;
$self_closing_flag = $this->self_closing_flag;
$attributes = $this->attributes;
$duplicate_attributes = $this->duplicate_attributes;

Expand Down Expand Up @@ -1136,6 +1146,7 @@ private function base_class_next_token(): bool {
$this->text_length = $this->tag_name_starts_at - $this->text_starts_at;
$this->tag_name_starts_at = $tag_name_starts_at;
$this->tag_name_length = $tag_name_length;
$this->self_closing_flag = $self_closing_flag;
$this->attributes = $attributes;
$this->duplicate_attributes = $duplicate_attributes;

Expand Down Expand Up @@ -2143,7 +2154,19 @@ private function parse_next_attribute(): bool {
$doc_length = strlen( $this->html );

// Skip whitespace and slashes.
$this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed );
$skipped_start = $this->bytes_already_parsed;
$this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $skipped_start );

// A slash inside an unquoted attribute value will not have been skipped here.
if (
$this->bytes_already_parsed < $doc_length &&
$this->bytes_already_parsed > $skipped_start &&
'/' === $this->html[ $this->bytes_already_parsed - 1 ] &&
'>' === $this->html[ $this->bytes_already_parsed ]
) {
$this->self_closing_flag = true;
}

if ( $this->bytes_already_parsed >= $doc_length ) {
$this->parser_state = self::STATE_INCOMPLETE_INPUT;

Expand Down Expand Up @@ -2327,6 +2350,7 @@ private function after_tag(): void {

$this->token_starts_at = null;
$this->token_length = null;
$this->self_closing_flag = false;
$this->tag_name_starts_at = null;
$this->tag_name_length = null;
$this->text_starts_at = 0;
Expand Down Expand Up @@ -3335,15 +3359,7 @@ public function has_self_closing_flag(): bool {
return false;
}

/*
* The self-closing flag is the solidus at the _end_ of the tag, not the beginning.
*
* Example:
*
* <figure />
* ^ this appears one character before the end of the closing ">".
*/
return '/' === $this->html[ $this->token_starts_at + $this->token_length - 2 ];
return $this->self_closing_flag;
}

/**
Expand Down
23 changes: 23 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,29 @@ public function test_expects_closer_foreign_content_self_closing() {
$this->assertTrue( $processor->expects_closer() );
}

/**
* Ensures a trailing slash in an unquoted attribute value does not close foreign content.
*
* @ticket 61576
*/
public function test_trailing_slash_in_unquoted_attribute_value_does_not_self_close_foreign_content() {
$processor = WP_HTML_Processor::create_fragment( '<math><mtext a=b/>This mtext tag is not self-closing, it has [a="b/"] attribute.' );

$this->assertTrue( $processor->next_tag( 'MTEXT' ), 'Could not find MTEXT tag: check test setup.' );
$this->assertSame( 'b/', $processor->get_attribute( 'a' ), 'Trailing slash in unquoted attribute value should belong to the attribute value.' );
$this->assertFalse(
$processor->has_self_closing_flag(),
'Trailing slash in unquoted attribute value should not be interpreted as a self-closing flag.'
);

$this->assertTrue( $processor->next_token(), 'Could not find text following MTEXT tag: check test setup.' );
$this->assertSame(
array( 'HTML', 'BODY', 'MATH', 'MTEXT', '#text' ),
$processor->get_breadcrumbs(),
'Text following the MTEXT tag should remain inside the MTEXT element.'
);
}

/**
* Ensures that expects_closer works for void-like elements in foreign content.
*
Expand Down
4 changes: 3 additions & 1 deletion tests/phpunit/tests/html-api/wpHtmlTagProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,12 @@ public static function data_has_self_closing_flag() {
'No self-closing flag on a foreign element' => array( '<circle>', false ),
// These involve syntax peculiarities.
'Self-closing flag after extra spaces' => array( '<div />', true ),
'Self-closing flag after attribute' => array( '<div id=test/>', true ),
'Self-closing flag after attribute' => array( '<div id=test />', true ),
'Slash inside unquoted attribute value' => array( '<div id=test/>', false ),
'Self-closing flag after quoted attribute' => array( '<div id="test"/>', true ),
'Self-closing flag after boolean attribute' => array( '<div enabled/>', true ),
'Boolean attribute that looks like a self-closer' => array( '<div / >', false ),
'Self-closing flag on internally consumed special element closer' => array( '<title>x</title/>', false ),
);
}

Expand Down
Loading