Skip to content

Commit 0c5aa8e

Browse files
committed
HTML API: Ensure carriage returns are serialized in HTML.
HTML parsers always replace carriage returns with line feeds when normalizing input. This ensures that serializing or normalizing an HTML document with the HTML API will preserve escaped U+000D CARRIAGE RETURN in the output. Developed in WordPress#12466. See #65372. git-svn-id: https://develop.svn.wordpress.org/trunk@62716 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 17c99e5 commit 0c5aa8e

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)