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 55d3fc5fe68e5..8a6638d7d6e6b 100644
--- a/src/wp-includes/html-api/class-wp-html-processor.php
+++ b/src/wp-includes/html-api/class-wp-html-processor.php
@@ -1511,9 +1511,6 @@ public function serialize_token(): string {
case 'IFRAME':
case 'NOEMBED':
case 'NOFRAMES':
- $text = '';
- break;
-
case 'SCRIPT':
case 'STYLE':
case 'XMP':
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 8e125ba461890..ace3e14bea565 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
@@ -298,7 +298,7 @@
* closing the SCRIPT from inside a JavaScript string. E.g. `console.log( '' )`.
* - `TITLE` and `TEXTAREA` whose contents are treated as plaintext and then any
* character references are decoded. E.g. `1 < 2 < 3` becomes `1 < 2 < 3`.
- * - `IFRAME`, `NOEMBED`, `NOFRAMES`, `STYLE` whose contents are treated as
+ * - `IFRAME`, `NOEMBED`, `NOFRAMES`, `STYLE`, `XMP` whose contents are treated as
* raw plaintext and left as-is. E.g. `1 < 2 < 3` remains `1 < 2 < 3`.
*
* #### Other tokens with modifiable text.
@@ -4075,6 +4075,32 @@ public function set_modifiable_text( string $plaintext_content ): bool {
);
return true;
+ case 'IFRAME':
+ case 'NOEMBED':
+ case 'NOFRAMES':
+ case 'XMP':
+ $tag_name = $this->get_tag();
+ if ( false !== stripos( $plaintext_content, "{$tag_name}" ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ sprintf(
+ /* translators: %s: HTML tag name. */
+ __( '%s text cannot contain its own closing tag.' ),
+ $tag_name
+ ),
+ '7.1.0'
+ );
+ 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(
'~(?Pstyle)~i',
@@ -4132,7 +4158,7 @@ static function ( $tag_match ) {
_doing_it_wrong(
__METHOD__,
- __( 'Only the SCRIPT, STYLE, TEXTAREA, and TITLE tags support setting modifiable text.' ),
+ __( 'This tag does not support setting modifiable text.' ),
'7.1.0'
);
return false;
diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php
index 81149dd5744ef..587d0a4d4bd8e 100644
--- a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php
+++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php
@@ -270,22 +270,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( 'onetwothree' ),
@@ -481,9 +465,9 @@ 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() {
+ public static function data_tokens_with_null_bytes(): array {
return array(
'Tag name' => array( "
", "
" ),
'Attribute name' => array( "
", "
" ),
@@ -492,11 +476,45 @@ public static function data_tokens_with_null_bytes() {
'Foreign content text' => array( "", "" ),
'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" ),
'XMP content' => array( "a\x00b", "a\u{FFFD}b" ),
'Comment text' => array( "", "" ),
);
}
+ /**
+ * Ensures that contents of rawtext elements are preserved when serializing.
+ *
+ * @ticket 65372
+ *
+ * @dataProvider data_rawtext_elements_with_html_syntax_character_contents
+ *
+ * @param string $html Normalized HTML containing a rawtext element with contents.
+ */
+ public function test_rawtext_element_contents_are_preserved_when_normalizing( string $html ): void {
+ $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_html_syntax_character_contents(): array {
+ return array(
+ 'IFRAME' => array( 'beforeafter' ),
+ 'NOEMBED' => array( 'before < > & " \' after' ),
+ 'NOFRAMES' => array( 'before < > & " \' after' ),
+ 'XMP' => array( 'before < > & " \' after' ),
+ );
+ }
+
/**
* @ticket 62396
*
diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php
index 35b5ed88baec0..589318daf3a70 100644
--- a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php
+++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php
@@ -434,6 +434,10 @@ public static function data_tokens_with_basic_modifiable_text_updates() {
'PI node (no separator)' => array( '', 1, '{"just": "kidding"}', '' ),
'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
', 'beforeraw & text
after' ),
+ 'NOFRAMES tag' => array( 'beforeold contentafter', 2, 'raw & text
', 'beforeraw & text
after' ),
+ 'XMP tag' => array( 'beforeold contentafter', 2, 'raw & text
', 'beforeraw & text
after' ),
'SCRIPT tag' => array( 'beforeafter', 2, 'const img = "
&
";', 'beforeafter' ),
'STYLE tag' => array( '', 1, 'p::before { content: "
& "; }', '' ),
'TEXTAREA tag' => array( 'ab', 2, "so it ", "ab" ),
@@ -624,6 +628,10 @@ 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' ),
+ 'XMP with ' => array( 'Replace me', 'Just a ' ),
'PI with >' => array( '', 'Processing instructions end at the first >' ),
'PI with leading space' => array( '', ' leading whitespace is skipped after the target' ),
'PI with leading tab' => array( '', "\tleading whitespace is skipped after the target" ),