Skip to content

Commit 94f3236

Browse files
committed
HTML API: Ensure slash in attribute value is not a self-closing flag.
A trailing slash in an unquoted attribute value was incorrectly treated as a self-closing flag. For example, `<div id=test/>` is a tag with the `id` attribute value `test/`, not a self-closing tag. Developed in WordPress#12319. Props jonsurrell, dmsnell. See #65372. git-svn-id: https://develop.svn.wordpress.org/trunk@62595 602fd350-edb4-49c9-b593-d223f7449a82
1 parent aa4e8ec commit 94f3236

3 files changed

Lines changed: 76 additions & 25 deletions

File tree

src/wp-includes/html-api/class-wp-html-tag-processor.php

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,15 @@ class WP_HTML_Tag_Processor {
627627
*/
628628
private $token_length;
629629

630+
/**
631+
* Whether the current tag token has the self-closing flag.
632+
*
633+
* @since 7.1.0
634+
*
635+
* @var bool
636+
*/
637+
private $has_self_closing_flag = false;
638+
630639
/**
631640
* Byte offset in input document where current tag name starts.
632641
*
@@ -1074,11 +1083,12 @@ private function base_class_next_token(): bool {
10741083
* the closing tag to point to the opening of the special atomic
10751084
* tag sequence.
10761085
*/
1077-
$tag_name_starts_at = $this->tag_name_starts_at;
1078-
$tag_name_length = $this->tag_name_length;
1079-
$tag_ends_at = $this->token_starts_at + $this->token_length;
1080-
$attributes = $this->attributes;
1081-
$duplicate_attributes = $this->duplicate_attributes;
1086+
$tag_name_starts_at = $this->tag_name_starts_at;
1087+
$tag_name_length = $this->tag_name_length;
1088+
$tag_ends_at = $this->token_starts_at + $this->token_length;
1089+
$has_self_closing_flag = $this->has_self_closing_flag;
1090+
$attributes = $this->attributes;
1091+
$duplicate_attributes = $this->duplicate_attributes;
10821092

10831093
// Find the closing tag if necessary.
10841094
switch ( $tag_name ) {
@@ -1128,14 +1138,15 @@ private function base_class_next_token(): bool {
11281138
* functions that skip the contents have moved all the internal cursors past
11291139
* the inner content of the tag.
11301140
*/
1131-
$this->token_starts_at = $was_at;
1132-
$this->token_length = $this->bytes_already_parsed - $this->token_starts_at;
1133-
$this->text_starts_at = $tag_ends_at;
1134-
$this->text_length = $this->tag_name_starts_at - $this->text_starts_at;
1135-
$this->tag_name_starts_at = $tag_name_starts_at;
1136-
$this->tag_name_length = $tag_name_length;
1137-
$this->attributes = $attributes;
1138-
$this->duplicate_attributes = $duplicate_attributes;
1141+
$this->token_starts_at = $was_at;
1142+
$this->token_length = $this->bytes_already_parsed - $this->token_starts_at;
1143+
$this->text_starts_at = $tag_ends_at;
1144+
$this->text_length = $this->tag_name_starts_at - $this->text_starts_at;
1145+
$this->tag_name_starts_at = $tag_name_starts_at;
1146+
$this->tag_name_length = $tag_name_length;
1147+
$this->has_self_closing_flag = $has_self_closing_flag;
1148+
$this->attributes = $attributes;
1149+
$this->duplicate_attributes = $duplicate_attributes;
11391150

11401151
return true;
11411152
}
@@ -2140,13 +2151,34 @@ private function parse_next_attribute(): bool {
21402151
$doc_length = strlen( $this->html );
21412152

21422153
// Skip whitespace and slashes.
2143-
$this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed );
2154+
$skipped_length = strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed );
2155+
$this->bytes_already_parsed += $skipped_length;
21442156
if ( $this->bytes_already_parsed >= $doc_length ) {
21452157
$this->parser_state = self::STATE_INCOMPLETE_INPUT;
21462158

21472159
return false;
21482160
}
21492161

2162+
/**
2163+
* This block serves two purposes:
2164+
*
2165+
* - A fast path for common tag-ending `>`.
2166+
* - A check for the self-closing flag which must appear as `/>`.
2167+
*
2168+
* In a tag like `<g attr=/>`, `/` is the attribute value, not a self-closing
2169+
* flag. When it appears in this form, the parser has already consumed the
2170+
* attribute value, `$skipped_length` is 0, and this checks below correctly
2171+
* identify whether there is a self-closing flag.
2172+
*
2173+
* Note: Both start and end tags may have the self-closing flag.
2174+
*/
2175+
if ( '>' === $this->html[ $this->bytes_already_parsed ] ) {
2176+
if ( $skipped_length > 0 && '/' === $this->html[ $this->bytes_already_parsed - 1 ] ) {
2177+
$this->has_self_closing_flag = true;
2178+
}
2179+
return false;
2180+
}
2181+
21502182
/*
21512183
* Treat the equal sign as a part of the attribute
21522184
* name if it is the first encountered byte.
@@ -2324,6 +2356,7 @@ private function after_tag(): void {
23242356

23252357
$this->token_starts_at = null;
23262358
$this->token_length = null;
2359+
$this->has_self_closing_flag = false;
23272360
$this->tag_name_starts_at = null;
23282361
$this->tag_name_length = null;
23292362
$this->text_starts_at = 0;
@@ -3332,15 +3365,7 @@ public function has_self_closing_flag(): bool {
33323365
return false;
33333366
}
33343367

3335-
/*
3336-
* The self-closing flag is the solidus at the _end_ of the tag, not the beginning.
3337-
*
3338-
* Example:
3339-
*
3340-
* <figure />
3341-
* ^ this appears one character before the end of the closing ">".
3342-
*/
3343-
return '/' === $this->html[ $this->token_starts_at + $this->token_length - 2 ];
3368+
return $this->has_self_closing_flag;
33443369
}
33453370

33463371
/**

tests/phpunit/tests/html-api/wpHtmlProcessor.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,29 @@ public function test_expects_closer_foreign_content_self_closing() {
583583
$this->assertTrue( $processor->expects_closer() );
584584
}
585585

586+
/**
587+
* Ensures a slash-only unquoted attribute value does not close foreign content.
588+
*
589+
* @ticket 65372
590+
*/
591+
public function test_unquoted_slash_attribute_does_not_self_close_foreign_content(): void {
592+
$processor = WP_HTML_Processor::create_fragment( '<math><mi a=/>math:mi is not self-closing, it has [a="/"] attribute.' );
593+
594+
$this->assertTrue( $processor->next_tag( 'MI' ), 'Failed to find the MI tag: check test setup.' );
595+
$this->assertSame( '/', $processor->get_attribute( 'a' ), 'Failed to treat the slash as the unquoted attribute value.' );
596+
$this->assertFalse(
597+
$processor->has_self_closing_flag(),
598+
'Failed to avoid interpreting the slash-only unquoted attribute value as a self-closing flag.'
599+
);
600+
601+
$this->assertTrue( $processor->next_token(), 'Failed to find text following the MI tag: check test setup.' );
602+
$this->assertSame(
603+
array( 'HTML', 'BODY', 'MATH', 'MI', '#text' ),
604+
$processor->get_breadcrumbs(),
605+
'Failed to keep text following the MI tag inside the MI element.'
606+
);
607+
}
608+
586609
/**
587610
* Ensures that expects_closer works for void-like elements in foreign content.
588611
*

tests/phpunit/tests/html-api/wpHtmlTagProcessor.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,13 @@ public static function data_has_self_closing_flag() {
111111
'No self-closing flag on a foreign element' => array( '<circle>', false ),
112112
// These involve syntax peculiarities.
113113
'Self-closing flag after extra spaces' => array( '<div />', true ),
114-
'Self-closing flag after attribute' => array( '<div id=test/>', true ),
114+
'Self-closing flag after attribute' => array( '<div id=test />', true ),
115+
'Slash inside unquoted attribute value' => array( '<div id=test/>', false ),
116+
'Slash only unquoted attribute value' => array( '<div attr=/>', false ),
117+
'Attribute "=" with value ""' => array( '<div =/>', false ),
115118
'Self-closing flag after quoted attribute' => array( '<div id="test"/>', true ),
116119
'Self-closing flag after boolean attribute' => array( '<div enabled/>', true ),
117-
'Boolean attribute that looks like a self-closer' => array( '<div / >', false ),
120+
'Ignored "/" and whitespace' => array( '<div / >', false ),
118121
);
119122
}
120123

0 commit comments

Comments
 (0)