Skip to content

Commit f50f50b

Browse files
authored
Merge branch 'WordPress:trunk' into trunk
2 parents 6383a1e + 0c5aa8e commit f50f50b

2 files changed

Lines changed: 70 additions & 7 deletions

File tree

src/wp-includes/html-api/class-wp-html-processor.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,7 +1387,7 @@ public function serialize_token(): string {
13871387
break;
13881388

13891389
case '#text':
1390-
$html .= htmlspecialchars( $this->get_modifiable_text(), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8' );
1390+
$html .= self::escape_text_for_serialization( $this->get_modifiable_text() );
13911391
break;
13921392

13931393
// Unlike the `<>` which is interpreted as plaintext, this is ignored entirely.
@@ -1417,10 +1417,9 @@ public function serialize_token(): string {
14171417
return $html;
14181418
}
14191419

1420-
$tag_name = str_replace( "\x00", "\u{FFFD}", $this->get_tag() );
1420+
$tag_name = $this->get_tag();
14211421
$in_html = 'html' === $this->get_namespace();
14221422
$qualified_name = $in_html ? strtolower( $tag_name ) : $this->get_qualified_tag_name();
1423-
$qualified_name = str_replace( "\x00", "\u{FFFD}", $qualified_name );
14241423

14251424
if ( $this->is_tag_closer() ) {
14261425
$html .= "</{$qualified_name}>";
@@ -1439,7 +1438,6 @@ public function serialize_token(): string {
14391438
$seen_attribute_names = array();
14401439
foreach ( $attribute_names as $attribute_name ) {
14411440
$qualified_attribute_name = $this->get_qualified_attribute_name( $attribute_name );
1442-
$qualified_attribute_name = str_replace( "\x00", "\u{FFFD}", $qualified_attribute_name );
14431441
$qualified_attribute_name = wp_scrub_utf8( $qualified_attribute_name );
14441442
/**
14451443
* Spaces only appear via the foreign attribute adjustment table.
@@ -1464,11 +1462,10 @@ public function serialize_token(): string {
14641462
$value = $this->get_attribute( $attribute_name );
14651463

14661464
if ( is_string( $value ) ) {
1467-
$html .= '="' . htmlspecialchars( $value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5 ) . '"';
1465+
$html .= '="' . self::escape_text_for_serialization( $value ) . '"';
14681466
}
14691467

14701468
$previous_attribute_was_true = true === $value;
1471-
$html = str_replace( "\x00", "\u{FFFD}", $html );
14721469
}
14731470

14741471
if ( ! $in_html && $this->has_self_closing_flag() ) {
@@ -1517,7 +1514,7 @@ public function serialize_token(): string {
15171514
break;
15181515

15191516
default:
1520-
$text = htmlspecialchars( $text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8' );
1517+
$text = self::escape_text_for_serialization( $text );
15211518
}
15221519

15231520
$html .= "{$text}</{$qualified_name}>";
@@ -1526,6 +1523,33 @@ public function serialize_token(): string {
15261523
return $html;
15271524
}
15281525

1526+
/**
1527+
* Escapes decoded text for HTML serialization.
1528+
*
1529+
* Use for:
1530+
* - Attribute values.
1531+
* - Text in ordinary (data-state) elements and in the RCDATA elements
1532+
* (TITLE and TEXTAREA).
1533+
* - Text in foreign content (elements not in the HTML namespace).
1534+
*
1535+
* Do not use for text in the RAWTEXT elements (STYLE, XMP, IFRAME,
1536+
* NOEMBED, NOFRAMES), HTML SCRIPT elements, or PLAINTEXT elements,
1537+
* whose contents serialize without escaping.
1538+
*
1539+
* @since 7.1.0
1540+
* @ignore
1541+
*
1542+
* @param string $text Decoded text to escape.
1543+
* @return string Escaped text.
1544+
*/
1545+
private static function escape_text_for_serialization( string $text ): string {
1546+
$text = htmlspecialchars( $text, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8' );
1547+
1548+
$text = str_replace( "\r", '&#xD;', $text );
1549+
1550+
return str_replace( "\x00", "\u{FFFD}", $text );
1551+
}
1552+
15291553
/**
15301554
* Parses next element in the 'initial' insertion mode.
15311555
*

tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,45 @@ public static function data_provider_normalized_fuzzer_cases_that_should_be_idem
749749
);
750750
}
751751

752+
/**
753+
* Ensures that carriage returns are correctly serialized.
754+
*
755+
* @ticket 65372
756+
*
757+
* @dataProvider data_provider_carriage_returns
758+
*
759+
* @param string $input HTML input containing a decoded carriage return.
760+
* @param string $expected Expected normalized output.
761+
*/
762+
public function test_normalize_serializes_decoded_carriage_returns_as_character_references( string $input, string $expected ): void {
763+
$normalized = WP_HTML_Processor::normalize( $input );
764+
765+
$this->assertSame( $expected, $normalized, 'Should have serialized the carriage return as a character reference.' );
766+
$this->assertSame(
767+
$expected,
768+
WP_HTML_Processor::normalize( $normalized ),
769+
'Normalizing already-normalized HTML should not change the serialized carriage return.'
770+
);
771+
}
772+
773+
/**
774+
* Data provider.
775+
*
776+
* @return array<string, array{string, string}>
777+
*/
778+
public static function data_provider_carriage_returns(): array {
779+
return array(
780+
'Decimal character reference' => array( 'a&#13;b', 'a&#xD;b' ),
781+
'Hex character reference' => array( 'a&#x0D;b', 'a&#xD;b' ),
782+
'RCDATA title' => array( '<title>a&#13;b</title>', '<title>a&#xD;b</title>' ),
783+
'Attribute value' => array( '<p attr="a&#13;b"></p>', '<p attr="a&#xD;b"></p>' ),
784+
'Table text' => array( '<table><td>x&#13;', '<table><tbody><tr><td>x&#xD;</td></tr></tbody></table>' ),
785+
'Template text' => array( '<template>a&#13;b</template>', '<template>a&#xD;b</template>' ),
786+
'Raw CR' => array( "<p title=\"a\rb\"></p>", "<p title=\"a\nb\"></p>" ),
787+
'Raw CRLF pair' => array( "<p title=\"a\r\nb\">", "<p title=\"a\nb\"></p>" ),
788+
);
789+
}
790+
752791
/**
753792
* Data provider.
754793
*

0 commit comments

Comments
 (0)