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 88487fb068d11..a28aeb7b857bb 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 @@ -3815,7 +3815,7 @@ public function get_modifiable_text(): string { ? $this->lexical_updates['modifiable text']->text : substr( $this->html, $this->text_starts_at, $this->text_length ); - /* + /** * An enqueued processing instruction update holds normalized raw * syntax spanning from the end of the target through the end of * the token: a separating space, the data, and the `?>` closer. @@ -4161,12 +4161,19 @@ static function ( $tag_match ) { case 'TEXTAREA': case 'TITLE': - $plaintext_content = preg_replace_callback( - "~{$this->get_tag()})~i", - static function ( $tag_match ) { - return "</{$tag_match['TAG_NAME']}"; - }, - $plaintext_content + /** + * While not expressly required, escaping syntax characters in these + * elements will help avoid problems with downstream parser which + * attempt to parse tags and other markup within. {@see \DOMDocument}, + * for example, will claim to find elements as children of a `TITLE`. + */ + $plaintext_content = strtr( + $plaintext_content, + array( + '<' => '<', + '&' => '&', + '>' => '>', + ) ); /* diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php index 589318daf3a70..d651a07f30d7c 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php @@ -414,9 +414,10 @@ public function test_updates_basic_modifiable_text_on_supported_nodes( string $h 'Should have modified the text at the target node.' ); - $this->assertSame( + $this->assertEqualHTML( $transformed, $processor->get_updated_html(), + '', "Should have transformed the HTML as expected when modifying the target node's modifiable text." ); } @@ -577,6 +578,79 @@ public function test_replaces_previous_processing_instruction_data_update(): voi ); } + /** + * Ensures that RCDATA contents are eagerly escaped, despite being + * optional, to protect downstream parsers from misparsing. + * + * @ticket {TICKET_NUMBER} + * + * @dataProvider data_rcdata_element_names + * + * @param 'TITLE'|'TEXTAREA' $element_name Which RCDATA element to verify. + */ + public function test_escapes_rcdata_content( string $element_name ): void { + $text = 'the is text'; + $html = "<{$element_name}>{$text}"; + $processor = new WP_HTML_Tag_Processor( $html ); + + $this->assertTrue( + $processor->next_token(), + "Failed to advance into the {$element_name} element: check test setup." + ); + + /* + * While this may look like a no-op, it should change the inner text + * from `` to `<img>`, which will be asserted below. + */ + $this->assertTrue( + $processor->set_modifiable_text( $text ), + 'Failed to set the modifiable text: check test setup.' + ); + + $output = $processor->get_updated_html(); + + // The updated output must still remain equivalent to the input. + $this->assertEqualHTML( + $html, + $output, + '', + 'Failed to update modifiable text without changing the parsed HTML.' + ); + + // Ensure that the content is not left unescaped. + $this->assertStringNotContainsString( + '', + $output, + 'Should have escaped the into <img<.' + ); + + // Finally, ensure that weaker parsers still interpret this correctly. + if ( class_exists( '\DOMDocument' ) ) { + $dom = new \DOMDocument(); + $dom->loadHTML( $output, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOERROR | LIBXML_NOWARNING ); + + $first_child = $dom->getElementsByTagName( strtolower( $element_name ) )->item( 0 )->childNodes[0]; + + $this->assertSame( + $first_child->nodeValue, + $text, + 'Failed to protect weaker parsers from detecting elements as RCDATA content.' + ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_rcdata_element_names(): array { + return array( + array( 'TEXTAREA' ), + array( 'TITLE' ), + ); + } + /** * Ensures that updates with potentially-compromising values aren't accepted. *