Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions src/wp-includes/html-api/class-wp-html-tag-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4090,11 +4090,14 @@ public function set_modifiable_text( string $plaintext_content ): bool {
* Because of this, content which could potentially modify the SCRIPT tag’s
* HTML structure is rejected here. It’s the responsibility of calling code to
* perform whatever semantic escaping is necessary to avoid problematic strings.
*
* A tag name ends only at one of the characters matched below, so text
* such as `</scriptx>` cannot change that structure and is safe to set.
*
* @link https://html.spec.whatwg.org/#script-data-end-tag-name-state
* @link https://html.spec.whatwg.org/#script-data-double-escape-start-state
*/
if (
false !== stripos( $plaintext_content, '<script' ) ||
false !== stripos( $plaintext_content, '</script' )
) {
if ( 1 === preg_match( '~</?script[ \t\f\r\n/>]~i', $plaintext_content ) ) {
_doing_it_wrong(
__METHOD__,
__( 'SCRIPT text with an unrecognized content type cannot contain a SCRIPT tag. Apply the escaping appropriate for the content type.' ),
Expand All @@ -4114,7 +4117,14 @@ public function set_modifiable_text( string $plaintext_content ): bool {
case 'NOFRAMES':
case 'XMP':
$tag_name = $this->get_tag();
if ( false !== stripos( $plaintext_content, "</{$tag_name}" ) ) {

/*
* A tag name ends only at one of the characters matched below, so text
* such as `</xmp-tag>` cannot close the element and is safe to set.
*
* @link https://html.spec.whatwg.org/#rawtext-end-tag-name-state
*/
if ( 1 === preg_match( '~</' . preg_quote( $tag_name, '~' ) . '[ \t\f\r\n/>]~i', $plaintext_content ) ) {
_doing_it_wrong(
__METHOD__,
sprintf(
Expand Down Expand Up @@ -4155,7 +4165,7 @@ static function ( $tag_match ) {
case 'TEXTAREA':
case 'TITLE':
$plaintext_content = preg_replace_callback(
"~</(?P<TAG_NAME>{$this->get_tag()})~i",
'~</(?P<TAG_NAME>' . preg_quote( $this->get_tag(), '~' ) . ')~i',
static function ( $tag_match ) {
return "&lt;/{$tag_match['TAG_NAME']}";
},
Expand Down
56 changes: 56 additions & 0 deletions tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ public function test_replaces_previous_processing_instruction_data_update(): voi
*
* @ticket 61617
* @ticket 62797
* @ticket 65824
*
* @dataProvider data_unallowed_modifiable_text_updates
*
Expand Down Expand Up @@ -640,6 +641,61 @@ public static function data_unallowed_modifiable_text_updates() {
'Non-JS SCRIPT with </script>' => array( '<script type="text/plain">Replace me</script>', 'Just a </script>' ),
'Non-JS SCRIPT with <script attributes>' => array( '<script language="text">Replace me</script>', '<!-- <script sneaky>after' ),
'Non-JS SCRIPT with </script attributes>' => array( '<script language="text">Replace me</script>', 'before</script sneaky>after' ),
'XMP with </xmp/>' => array( '<xmp>Replace me</xmp>', 'Also closed by </xmp/>' ),
);
}

/**
* Ensures that raw text which resembles a closing tag, but cannot close its
* element, is allowed as modifiable text.
*
* @ticket 65824
*
* @dataProvider data_raw_text_resembling_a_closing_tag
*
* @param string $html HTML whose first tag holds the raw text to replace.
* @param string $update Text resembling, but not forming, that element's closing tag.
* @param string $expected Expected document after the update.
*/
public function test_allows_raw_text_which_cannot_close_its_element( string $html, string $update, string $expected ): void {
$processor = new WP_HTML_Tag_Processor( $html );
$processor->next_tag();

$this->assertTrue(
$processor->set_modifiable_text( $update ),
'Should have allowed text which cannot close the element.'
);

$this->assertSame(
$expected,
$processor->get_updated_html(),
'Should have updated the document as expected.'
);

$reparsed = new WP_HTML_Tag_Processor( $expected );
$reparsed->next_tag();

$this->assertSame(
$update,
$reparsed->get_modifiable_text(),
'Should have preserved the text when re-parsing the updated document.'
);
}

/**
* Data provider.
*
* @return array[]
*/
public static function data_raw_text_resembling_a_closing_tag(): array {
return array(
'IFRAME with </iframely>' => array( '<iframe>Replace me</iframe>', 'Just a </iframely>', '<iframe>Just a </iframely></iframe>' ),
'NOEMBED with </NOEMBEDDED>' => array( '<noembed>Replace me</noembed>', 'Just a </NOEMBEDDED>', '<noembed>Just a </NOEMBEDDED></noembed>' ),
'NOFRAMES with </noframes->' => array( '<noframes>Replace me</noframes>', 'before</noframes->after', '<noframes>before</noframes->after</noframes>' ),
'XMP with </xmp-tag>' => array( '<xmp>Replace me</xmp>', 'Just a </xmp-tag>', '<xmp>Just a </xmp-tag></xmp>' ),
'XMP ending in </xmp' => array( '<xmp>Replace me</xmp>', 'Trailing </xmp', '<xmp>Trailing </xmp</xmp>' ),
'Non-JS SCRIPT with <scriptish>' => array( '<script type="text/plain">Replace me</script>', '<!-- <scriptish> -->', '<script type="text/plain"><!-- <scriptish> --></script>' ),
'Non-JS SCRIPT with </scriptx>' => array( '<script type="text/plain">Replace me</script>', 'Just a </scriptx>', '<script type="text/plain">Just a </scriptx></script>' ),
);
}

Expand Down
Loading