From 65c6ad5f463c9d0cf9c9a20561c0aa3e2db9d6bb Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 7 Aug 2026 15:47:53 +0400 Subject: [PATCH 1/4] HTML API: Test form feed in special end tags. Form feed is ASCII whitespace. It terminates an appropriate RCDATA or RAWTEXT end tag name, but skip_rcdata() rejects it and leaves the entire special element incomplete. Cover every element routed through that scanner. SCRIPT uses a separate scanner and already has this coverage. --- .../tests/html-api/wpHtmlTagProcessor.php | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php index 66e01dbdbed3e..7f81252dcf1ea 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php @@ -2369,6 +2369,35 @@ public static function data_script_tag(): Generator { yield 'Script tag double-escaped with array( "", false ); } + /** + * Ensures that form feed terminates tag names in RCDATA and RAWTEXT end tags. + * + * @dataProvider data_rcdata_and_rawtext_tag_names + * + * @param string $tag_name The RCDATA or RAWTEXT tag name. + */ + public function test_rcdata_and_rawtext_end_tags_accept_form_feed( string $tag_name ) { + $processor = new WP_HTML_Tag_Processor( "<{$tag_name}>content
" ); + + $this->assertTrue( $processor->next_token(), "Expected to find complete {$tag_name} tag." ); + $this->assertSame( strtoupper( $tag_name ), $processor->get_tag() ); + $this->assertSame( 'content', $processor->get_modifiable_text() ); + $this->assertTrue( $processor->next_tag( 'DIV' ), 'Expected to find DIV after the special element.' ); + } + + /** + * Data provider. + */ + public static function data_rcdata_and_rawtext_tag_names(): Generator { + yield 'IFRAME' => array( 'iframe' ); + yield 'NOEMBED' => array( 'noembed' ); + yield 'NOFRAMES' => array( 'noframes' ); + yield 'STYLE' => array( 'style' ); + yield 'XMP' => array( 'xmp' ); + yield 'TEXTAREA' => array( 'textarea' ); + yield 'TITLE' => array( 'title' ); + } + /** * Invalid tag names are comments on tag closers. * From 85366584ebb9f5aa3769ffa43fce515a619621f2 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 7 Aug 2026 15:50:21 +0400 Subject: [PATCH 2/4] HTML API: Recognize form feed after special end tag names. skip_rcdata() omits form feed from the tag-name terminator set even though it is ASCII whitespace. Valid RCDATA and RAWTEXT closers then look like text, so the processor pauses on a complete element. Accept form feed before handing the closer to the existing attribute scanner. --- src/wp-includes/html-api/class-wp-html-tag-processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 7ca5191a0f162..32cb30b6afb82 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -1475,7 +1475,7 @@ private function skip_rcdata( string $tag_name ): bool { * though "textarea" is found within the text. */ $c = $html[ $at ]; - if ( ' ' !== $c && "\t" !== $c && "\r" !== $c && "\n" !== $c && '/' !== $c && '>' !== $c ) { + if ( ' ' !== $c && "\t" !== $c && "\f" !== $c && "\r" !== $c && "\n" !== $c && '/' !== $c && '>' !== $c ) { continue; } From 95fc0e030f120f22587671a70dfe9a1e7a77c2be Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 7 Aug 2026 19:32:53 +0400 Subject: [PATCH 3/4] HTML API: Declare form feed test return types. The test and provider leave their return contracts partly implicit. State void and the exact generator key and value shape so static analysis does not have to guess. --- tests/phpunit/tests/html-api/wpHtmlTagProcessor.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php index 7f81252dcf1ea..9d1e39583985e 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php @@ -2376,7 +2376,7 @@ public static function data_script_tag(): Generator { * * @param string $tag_name The RCDATA or RAWTEXT tag name. */ - public function test_rcdata_and_rawtext_end_tags_accept_form_feed( string $tag_name ) { + public function test_rcdata_and_rawtext_end_tags_accept_form_feed( string $tag_name ): void { $processor = new WP_HTML_Tag_Processor( "<{$tag_name}>content
" ); $this->assertTrue( $processor->next_token(), "Expected to find complete {$tag_name} tag." ); @@ -2387,6 +2387,8 @@ public function test_rcdata_and_rawtext_end_tags_accept_form_feed( string $tag_n /** * Data provider. + * + * @return Generator Test cases. */ public static function data_rcdata_and_rawtext_tag_names(): Generator { yield 'IFRAME' => array( 'iframe' ); From c80fb05f46f684b3ff9a8f9935c726def0475126 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 7 Aug 2026 19:38:54 +0400 Subject: [PATCH 4/4] HTML API: Cover every special end tag terminator. The regression test names only form feed, but skip_rcdata() implements a seven-character boundary contract. Testing one byte leaves the other accepted paths free to drift. Keep the terminators in a reusable provider and run the full Cartesian matrix as independent cases. --- .../tests/html-api/wpHtmlTagProcessor.php | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php index 9d1e39583985e..8733f5718ba6c 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessor.php @@ -2370,34 +2370,49 @@ public static function data_script_tag(): Generator { } /** - * Ensures that form feed terminates tag names in RCDATA and RAWTEXT end tags. + * Ensures that tag-name-terminating characters close RCDATA and RAWTEXT elements. * - * @dataProvider data_rcdata_and_rawtext_tag_names + * @dataProvider data_rcdata_and_rawtext_tag_name_terminators * - * @param string $tag_name The RCDATA or RAWTEXT tag name. + * @param string $tag_name The RCDATA or RAWTEXT tag name. + * @param string $tag_name_terminator The tag-name-terminating character. */ - public function test_rcdata_and_rawtext_end_tags_accept_form_feed( string $tag_name ): void { - $processor = new WP_HTML_Tag_Processor( "<{$tag_name}>content
" ); + public function test_rcdata_and_rawtext_end_tags_accept_tag_name_terminators( string $tag_name, string $tag_name_terminator ): void { + $end_tag_closer = '>' === $tag_name_terminator ? '' : '>'; + $processor = new WP_HTML_Tag_Processor( "<{$tag_name}>content" ); $this->assertTrue( $processor->next_token(), "Expected to find complete {$tag_name} tag." ); $this->assertSame( strtoupper( $tag_name ), $processor->get_tag() ); $this->assertSame( 'content', $processor->get_modifiable_text() ); - $this->assertTrue( $processor->next_tag( 'DIV' ), 'Expected to find DIV after the special element.' ); + $this->assertTrue( $processor->next_tag( 'DIV' ), "Expected to find DIV after the {$tag_name} element." ); } /** - * Data provider. + * Provides every RCDATA and RAWTEXT tag with every tag-name-terminating character. + * + * @return Generator Test cases. + */ + public static function data_rcdata_and_rawtext_tag_name_terminators(): Generator { + foreach ( array( 'IFRAME', 'NOEMBED', 'NOFRAMES', 'STYLE', 'XMP', 'TEXTAREA', 'TITLE' ) as $tag_name ) { + foreach ( self::data_tag_name_terminators() as $terminator_name => $terminator_data ) { + yield "{$tag_name} + {$terminator_name}" => array( strtolower( $tag_name ), $terminator_data[0] ); + } + } + } + + /** + * Provides tag-name-terminating characters. * * @return Generator Test cases. */ - public static function data_rcdata_and_rawtext_tag_names(): Generator { - yield 'IFRAME' => array( 'iframe' ); - yield 'NOEMBED' => array( 'noembed' ); - yield 'NOFRAMES' => array( 'noframes' ); - yield 'STYLE' => array( 'style' ); - yield 'XMP' => array( 'xmp' ); - yield 'TEXTAREA' => array( 'textarea' ); - yield 'TITLE' => array( 'title' ); + public static function data_tag_name_terminators(): Generator { + yield 'SPACE' => array( ' ' ); + yield 'TAB' => array( "\t" ); + yield 'LINE FEED' => array( "\n" ); + yield 'FORM FEED' => array( "\f" ); + yield 'CARRIAGE RETURN' => array( "\r" ); + yield 'SOLIDUS' => array( '/' ); + yield 'GREATER-THAN SIGN' => array( '>' ); } /**