From 81b255d946e9011cd313339be74057ba587be415 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 11 Jun 2026 23:18:45 +0200 Subject: [PATCH 01/10] HTML API: Preserve raw text contents in serialize. The serializer was discarding the raw-text contents of IFRAME, NOEMBED, and NOFRAMES even though get_modifiable_text() already returns the browser-equivalent raw text for those elements. Let those elements follow the same raw emission path as SCRIPT and STYLE, preserving contents while retaining existing NUL and newline normalization. See #65372. --- .../html-api/class-wp-html-processor.php | 3 -- .../html-api/wpHtmlProcessor-serialize.php | 30 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-processor.php b/src/wp-includes/html-api/class-wp-html-processor.php index 35d91fad3129c..1c1499c298a33 100644 --- a/src/wp-includes/html-api/class-wp-html-processor.php +++ b/src/wp-includes/html-api/class-wp-html-processor.php @@ -1493,9 +1493,6 @@ public function serialize_token(): string { case 'IFRAME': case 'NOEMBED': case 'NOFRAMES': - $text = ''; - break; - case 'SCRIPT': case 'STYLE': break; diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php index e516addb6c314..7016cff756a4a 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php @@ -134,6 +134,36 @@ public function test_style_contents_are_not_escaped() { ); } + /** + * Ensures that IFRAME, NOEMBED, and NOFRAMES contents are not escaped, as they are not parsed like text nodes are. + * + * @ticket 65372 + * + * @dataProvider data_raw_text_elements_with_unescaped_contents + * + * @param string $tag_name Tag name under test. + */ + public function test_iframe_noembed_noframes_contents_are_not_escaped( string $tag_name ) { + $this->assertSame( + WP_HTML_Processor::normalize( "<{$tag_name}>apples > or\x00anges < p &" ), + "<{$tag_name}>apples > or\u{FFFD}anges < p &", + "Should have preserved text inside an {$tag_name} element, except for replacing NULL bytes." + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_raw_text_elements_with_unescaped_contents() { + return array( + 'IFRAME' => array( 'iframe' ), + 'NOEMBED' => array( 'noembed' ), + 'NOFRAMES' => array( 'noframes' ), + ); + } + public function test_unexpected_closing_tags_are_removed() { $this->assertSame( WP_HTML_Processor::normalize( 'onetwothree' ), From 2f02318d2095ce4b6e2a974a61ec7e2fb3c165ff Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 12 Jun 2026 11:24:16 +0200 Subject: [PATCH 02/10] HTML API: Expand rawtext serialize coverage --- .../html-api/wpHtmlProcessor-serialize.php | 128 ++++++++++++++---- 1 file changed, 98 insertions(+), 30 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php index 7016cff756a4a..053d8023d4929 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php @@ -134,36 +134,6 @@ public function test_style_contents_are_not_escaped() { ); } - /** - * Ensures that IFRAME, NOEMBED, and NOFRAMES contents are not escaped, as they are not parsed like text nodes are. - * - * @ticket 65372 - * - * @dataProvider data_raw_text_elements_with_unescaped_contents - * - * @param string $tag_name Tag name under test. - */ - public function test_iframe_noembed_noframes_contents_are_not_escaped( string $tag_name ) { - $this->assertSame( - WP_HTML_Processor::normalize( "<{$tag_name}>apples > or\x00anges < p &" ), - "<{$tag_name}>apples > or\u{FFFD}anges < p &", - "Should have preserved text inside an {$tag_name} element, except for replacing NULL bytes." - ); - } - - /** - * Data provider. - * - * @return array[] - */ - public static function data_raw_text_elements_with_unescaped_contents() { - return array( - 'IFRAME' => array( 'iframe' ), - 'NOEMBED' => array( 'noembed' ), - 'NOFRAMES' => array( 'noframes' ), - ); - } - public function test_unexpected_closing_tags_are_removed() { $this->assertSame( WP_HTML_Processor::normalize( 'onetwothree' ), @@ -311,10 +281,108 @@ public static function data_tokens_with_null_bytes() { 'Foreign content text' => array( "one\x00two", "one\u{FFFD}two" ), 'SCRIPT content' => array( "", "" ), 'STYLE content' => array( "", "" ), + 'IFRAME content' => array( "", "" ), + 'NOEMBED content' => array( "a\x00b", "a\u{FFFD}b" ), + 'NOFRAMES content' => array( "a\x00b", "a\u{FFFD}b" ), 'Comment text' => array( "", "" ), ); } + /** + * Ensures that the contents of IFRAME, NOEMBED, and NOFRAMES elements are + * preserved when serializing. + * + * These elements contain raw text which is part of the parsed document. + * Dropping it would change the document's contents across a serialize and + * re-parse cycle. + * + * @ticket 65372 + * + * @dataProvider data_rawtext_elements_with_contents + * + * @param string $html Normalized HTML containing a rawtext element with contents. + */ + public function test_rawtext_element_contents_are_preserved_when_normalizing( string $html ) { + $this->assertSame( + $html, + WP_HTML_Processor::normalize( $html ), + 'Should have preserved the rawtext element contents.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_rawtext_elements_with_contents() { + return array( + 'IFRAME with following text' => array( 'y' ), + 'NOEMBED with following text' => array( 'xy' ), + 'NOFRAMES with following text' => array( '
xy
' ), + 'NOFRAMES before comment' => array( '
x
' ), + 'IFRAME with markup-like contents' => array( '' ), + 'NOEMBED with character reference' => array( '&amp;' ), + 'NOFRAMES with character reference' => array( '&lt;' ), + ); + } + + /** + * Ensures that the contents of IFRAME, NOEMBED, and NOFRAMES elements are + * preserved when serializing full documents, including NOFRAMES elements + * in the HEAD or after a FRAMESET. + * + * @ticket 65372 + * + * @dataProvider data_full_documents_with_rawtext_elements + * + * @param string $html Input HTML document. + * @param string $expected Expected serialization of the full document. + */ + public function test_rawtext_element_contents_are_preserved_in_full_documents( string $html, string $expected ) { + $processor = WP_HTML_Processor::create_full_parser( $html ); + + $this->assertSame( + $expected, + $processor->serialize(), + 'Should have preserved the rawtext element contents.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_full_documents_with_rawtext_elements() { + return array( + 'IFRAME in BODY' => array( + 'y', + 'y', + ), + 'NOEMBED in BODY' => array( + 'ax', + 'ax', + ), + 'NOFRAMES in BODY' => array( + 'ax', + 'ax', + ), + 'NOFRAMES in HEAD' => array( + 'xz', + 'xz', + ), + 'NOFRAMES in FRAMESET' => array( + 'x', + 'x', + ), + 'IFRAME before a comment' => array( + '

', + '

', + ), + ); + } + /** * @ticket 62396 * From 7712560d9aaae51e3ebe6a5d76471bd3b0e591e0 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 23 Jun 2026 20:12:28 +0200 Subject: [PATCH 03/10] Combine tests with XMP rawtext --- .../html-api/wpHtmlProcessor-serialize.php | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php index cebab6bb46b53..80cd51cfd64d3 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php @@ -257,22 +257,6 @@ public function test_style_contents_are_not_escaped() { ); } - /** - * XMP contents are parsed using the generic raw text element parsing algorithm. - * Their contents should not be escaped with HTML character references on normalization. - * - * @ticket 65372 - */ - public function test_xmp_contents_are_not_escaped() { - $normalized = WP_HTML_Processor::normalize( " < > & \" ' \x00 " ); - - $this->assertSame( - " < > & \" ' \u{FFFD} ", - $normalized, - 'Should have preserved text inside an XMP element, except for replacing NULL bytes.' - ); - } - public function test_unexpected_closing_tags_are_removed() { $this->assertSame( WP_HTML_Processor::normalize( 'one
twothree' ), @@ -411,7 +395,7 @@ public function test_replaces_null_bytes_appropriately( string $html_with_nulls, * * @return array[] */ - public static function data_tokens_with_null_bytes() { + public static function data_tokens_with_null_bytes(): array { return array( 'Tag name' => array( "", "" ), 'Attribute name' => array( "", "" ), @@ -453,17 +437,15 @@ public function test_rawtext_element_contents_are_preserved_when_normalizing( st /** * Data provider. * - * @return array[] + * @return array */ - public static function data_rawtext_elements_with_contents() { + public static function data_rawtext_elements_with_contents(): array { return array( - 'IFRAME with following text' => array( 'y' ), - 'NOEMBED with following text' => array( 'xy' ), - 'NOFRAMES with following text' => array( '
xy
' ), - 'NOFRAMES before comment' => array( '
x
' ), - 'IFRAME with markup-like contents' => array( '' ), - 'NOEMBED with character reference' => array( '&amp;' ), - 'NOFRAMES with character reference' => array( '&lt;' ), + 'IFRAME' => array( 'beforeafter' ), + 'NOEMBED' => array( 'before < > &amp; " \' after' ), + 'NOFRAMES' => array( 'before < > &amp; " \' after' ), + 'XMP' => array( 'before < > &amp; " \' after' ), + ); } From d99da2807e97515b4e3274923cf93e78cc42a2dc Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Tue, 23 Jun 2026 20:14:33 +0200 Subject: [PATCH 04/10] Clean up and remove redundant tests --- .../html-api/wpHtmlProcessor-serialize.php | 67 +------------------ 1 file changed, 3 insertions(+), 64 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php index 80cd51cfd64d3..4524472742568 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php @@ -413,16 +413,11 @@ public static function data_tokens_with_null_bytes(): array { } /** - * Ensures that the contents of IFRAME, NOEMBED, and NOFRAMES elements are - * preserved when serializing. - * - * These elements contain raw text which is part of the parsed document. - * Dropping it would change the document's contents across a serialize and - * re-parse cycle. + * Ensures that contents of rawtext elements are preserved when serializing. * * @ticket 65372 * - * @dataProvider data_rawtext_elements_with_contents + * @dataProvider data_rawtext_elements_with_html_syntax_character_contents * * @param string $html Normalized HTML containing a rawtext element with contents. */ @@ -439,7 +434,7 @@ public function test_rawtext_element_contents_are_preserved_when_normalizing( st * * @return array */ - public static function data_rawtext_elements_with_contents(): array { + public static function data_rawtext_elements_with_html_syntax_character_contents(): array { return array( 'IFRAME' => array( 'beforeafter' ), 'NOEMBED' => array( 'before < > &amp; " \' after' ), @@ -449,62 +444,6 @@ public static function data_rawtext_elements_with_contents(): array { ); } - /** - * Ensures that the contents of IFRAME, NOEMBED, and NOFRAMES elements are - * preserved when serializing full documents, including NOFRAMES elements - * in the HEAD or after a FRAMESET. - * - * @ticket 65372 - * - * @dataProvider data_full_documents_with_rawtext_elements - * - * @param string $html Input HTML document. - * @param string $expected Expected serialization of the full document. - */ - public function test_rawtext_element_contents_are_preserved_in_full_documents( string $html, string $expected ) { - $processor = WP_HTML_Processor::create_full_parser( $html ); - - $this->assertSame( - $expected, - $processor->serialize(), - 'Should have preserved the rawtext element contents.' - ); - } - - /** - * Data provider. - * - * @return array[] - */ - public static function data_full_documents_with_rawtext_elements() { - return array( - 'IFRAME in BODY' => array( - 'y', - 'y', - ), - 'NOEMBED in BODY' => array( - 'ax', - 'ax', - ), - 'NOFRAMES in BODY' => array( - 'ax', - 'ax', - ), - 'NOFRAMES in HEAD' => array( - 'xz', - 'xz', - ), - 'NOFRAMES in FRAMESET' => array( - 'x', - 'x', - ), - 'IFRAME before a comment' => array( - '

', - '

', - ), - ); - } - /** * @ticket 62396 * From d2c76f6e5034dcaa1739f6a6cbf7c9d2fafd0b02 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 13:05:15 +0200 Subject: [PATCH 05/10] Remove empty line --- tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php index 4524472742568..b25ea9b9db00c 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php @@ -440,7 +440,6 @@ public static function data_rawtext_elements_with_html_syntax_character_contents 'NOEMBED' => array( 'before < > &amp; " \' after' ), 'NOFRAMES' => array( 'before < > &amp; " \' after' ), 'XMP' => array( 'before < > &amp; " \' after' ), - ); } From 1018e334a9bc73948d0373fb78f3ebbe7dcc600b Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 13:07:47 +0200 Subject: [PATCH 06/10] Improve test function typing Co-authored-by: Jon Surrell --- tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php index b25ea9b9db00c..80e1d0dba404d 100644 --- a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php @@ -393,7 +393,7 @@ public function test_replaces_null_bytes_appropriately( string $html_with_nulls, /** * Data provider. * - * @return array[] + * @return array */ public static function data_tokens_with_null_bytes(): array { return array( @@ -421,7 +421,7 @@ public static function data_tokens_with_null_bytes(): array { * * @param string $html Normalized HTML containing a rawtext element with contents. */ - public function test_rawtext_element_contents_are_preserved_when_normalizing( string $html ) { + public function test_rawtext_element_contents_are_preserved_when_normalizing( string $html ): void { $this->assertSame( $html, WP_HTML_Processor::normalize( $html ), From 7a5300d160ea24cc59cd6465d1987f8c398d15d3 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 9 Jul 2026 19:34:43 +0200 Subject: [PATCH 07/10] HTML API: Allow rawtext modifiable text updates --- .../html-api/class-wp-html-tag-processor.php | 15 +++++++++++++++ .../html-api/wpHtmlTagProcessorModifiableText.php | 6 ++++++ 2 files changed, 21 insertions(+) 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 e41e1120550b5..9bdb8c54d3dd6 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 @@ -3880,6 +3880,21 @@ public function set_modifiable_text( string $plaintext_content ): bool { ); return true; + case 'IFRAME': + case 'NOEMBED': + case 'NOFRAMES': + if ( false !== stripos( $plaintext_content, "get_tag()}" ) ) { + return false; + } + + $this->lexical_updates['modifiable text'] = new WP_HTML_Text_Replacement( + $this->text_starts_at, + $this->text_length, + $plaintext_content + ); + + return true; + case 'STYLE': $plaintext_content = preg_replace_callback( '~style)~i', diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php index 4a09403b7b23e..3059392b0dc4e 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php @@ -430,6 +430,9 @@ public static function data_tokens_with_basic_modifiable_text_updates() { 'Text node (middle)' => array( 'Bold move', 2, 'yo', 'yo' ), 'Text node (end)' => array( 'of a dog', 2, 'of a cat', 'of a cat' ), 'Encoded text node' => array( '
birds and dogs
', 2, ' & ', '
<birds> & <dogs>
' ), + 'IFRAME tag' => array( 'beforeafter', 2, '

raw & text

', 'beforeafter' ), + 'NOEMBED tag' => array( 'beforeold contentafter', 2, '

raw & text

', 'before<p>raw & text</p>after' ), + 'NOFRAMES tag' => array( 'beforeold contentafter', 2, '

raw & text

', 'before<p>raw & text</p>after' ), 'SCRIPT tag' => array( 'beforeafter', 2, 'const img = " &
";', 'beforeafter' ), 'STYLE tag' => array( '', 1, 'p::before { content: " & "; }', '' ), 'TEXTAREA tag' => array( 'ab', 2, "so it ", "ab" ), @@ -490,6 +493,9 @@ public static function data_unallowed_modifiable_text_updates() { return array( 'Comment with -->' => array( '', 'Comments end in -->' ), 'Comment with --!>' => array( '', 'Invalid but legitimate comments end in --!>' ), + 'IFRAME with ' => array( '', 'Just a ' ), + 'NOEMBED with ' => array( 'Replace me', 'Just a ' ), + 'NOFRAMES with ' => array( 'Replace me', 'beforeafter' ), 'Non-JS SCRIPT with ', '