diff --git a/src/wp-includes/html-api/class-wp-html-template.php b/src/wp-includes/html-api/class-wp-html-template.php
index 3169b10dfc317..14ee1b3c670e9 100644
--- a/src/wp-includes/html-api/class-wp-html-template.php
+++ b/src/wp-includes/html-api/class-wp-html-template.php
@@ -26,7 +26,7 @@ class WP_HTML_Template extends WP_HTML_Tag_Processor {
*
* This function looks for placeholders in the template string and will replace
* them with appropriately-escaped substitutions from the given arguments, if
- * provided and if those arguments are strings.
+ * provided and if those arguments are strings or valid attribute values.
*
* Example:
*
@@ -64,7 +64,7 @@ class WP_HTML_Template extends WP_HTML_Tag_Processor {
* If provided any other type of value the attribute will be ignored and its existing value persists.
*
* - If multiple HTML attributes are specified for a given tag they will be applied as if calling
- * `set_attribute()` in the order they are specified in the temlpate. This includes any attributes
+ * `set_attribute()` in the order they are specified in the template. This includes any attributes
* assigned through the attribute spread syntax.
*
* - Substitutions in text nodes may only contain string values. If provided any other type of value
@@ -74,7 +74,7 @@ class WP_HTML_Template extends WP_HTML_Tag_Processor {
* it may provide the ability to nest pre-rendered HTML into the template, but this functionality
* is deferred for a future update.
*
- * - This function will not replace content inside of TEXTAREA, TITLE, SCRIPT, or STYLE elements.
+ * - This function will not replace content inside of SCRIPT, or STYLE elements.
*
* @since 6.5.0
*
@@ -162,6 +162,25 @@ static function ( $matches ) use ( $args ) {
$processor->set_attribute( $attribute_name, $new_value );
}
}
+
+ // Update TEXTAREA and TITLE contents.
+ $tag_name = $processor->get_tag();
+ if ( 'TEXTAREA' === $tag_name || 'TITLE' === $tag_name ) {
+ // Replace placeholders inside these RCDATA tags.
+ $new_text = preg_replace_callback(
+ '~%([^>]+)>~',
+ static function ( $matches ) use ( $args ) {
+ return is_string( $args[ $matches[1] ] )
+ ? $args[ $matches[1] ]
+ : '';
+ },
+ $text
+ );
+
+ if ( $new_text !== $text ) {
+ $processor->set_modifiable_text( $new_text );
+ }
+ }
}
}
diff --git a/src/wp-includes/html-api/class-wp-html.php b/src/wp-includes/html-api/class-wp-html.php
index 6443acac91508..a1c38e866af59 100644
--- a/src/wp-includes/html-api/class-wp-html.php
+++ b/src/wp-includes/html-api/class-wp-html.php
@@ -20,7 +20,7 @@ class WP_HTML {
*
* This function looks for placeholders in the template string and will replace
* them with appropriately-escaped substitutions from the given arguments, if
- * provided and if those arguments are strings.
+ * provided and if those arguments are strings or valid attribute values.
*
* Example:
*
@@ -58,7 +58,7 @@ class WP_HTML {
* If provided any other type of value the attribute will be ignored and its existing value persists.
*
* - If multiple HTML attributes are specified for a given tag they will be applied as if calling
- * `set_attribute()` in the order they are specified in the temlpate. This includes any attributes
+ * `set_attribute()` in the order they are specified in the template. This includes any attributes
* assigned through the attribute spread syntax.
*
* - Substitutions in text nodes may only contain string values. If provided any other type of value
@@ -68,6 +68,8 @@ class WP_HTML {
* it may provide the ability to nest pre-rendered HTML into the template, but this functionality
* is deferred for a future update.
*
+ * - This function will not replace content inside of SCRIPT, or STYLE elements.
+ *
* @since 6.5.0
*
* @access private
diff --git a/tests/phpunit/tests/html-api/wpHtmlTemplate.php b/tests/phpunit/tests/html-api/wpHtmlTemplate.php
index cc63eba954fd2..dd4859fb2f14b 100644
--- a/tests/phpunit/tests/html-api/wpHtmlTemplate.php
+++ b/tests/phpunit/tests/html-api/wpHtmlTemplate.php
@@ -85,4 +85,22 @@ public function test_cannot_break_out_of_tag_with_malicious_attribute_name() {
"Should not have found any other tags but found {$processor->get_tag()} instead."
);
}
+
+ /**
+ * Ensures that basic replacement inside a TEXTAREA subtitutes placeholders.
+ *
+ * @ticket 60229
+ */
+ public function test_replaces_textarea_placeholders() {
+ $html = WP_HTML_Template::render(
+ '',
+ array( 'big' => ' ()' )
+ );
+
+ $this->assertSame(
+ '',
+ $html,
+ 'Should have replaced placeholder with RCDATA escaping rules.'
+ );
+ }
}