Skip to content

Commit 60e382b

Browse files
committed
HTML API: Apply deferred byte processing at read interfaces.
Ensure that input processing (intentionally deferred by the HTML API) is applied consistently. This includes newline normalization (CRLF and CR become LF) and null byte replacement (U+FFFD in tag names, attribute names, and attribute values). Developed in WordPress#12385. Props jonsurrell, dmsnell. See #65372. git-svn-id: https://develop.svn.wordpress.org/trunk@62667 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 8512f0f commit 60e382b

3 files changed

Lines changed: 504 additions & 20 deletions

File tree

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

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,7 +1217,7 @@ public function class_list() {
12171217
return;
12181218
}
12191219

1220-
$name = str_replace( "\x00", "\u{FFFD}", substr( $class, $at, $length ) );
1220+
$name = substr( $class, $at, $length );
12211221
if ( $is_quirks ) {
12221222
$name = strtolower( $name );
12231223
}
@@ -2261,8 +2261,13 @@ private function parse_next_attribute(): bool {
22612261
* - HTML 5 spec
22622262
*
22632263
* @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive
2264+
*
2265+
* The tokenizer replaces U+0000 NULL bytes in
2266+
* attribute names with U+FFFD.
2267+
*
2268+
* @see https://html.spec.whatwg.org/#attribute-name-state
22642269
*/
2265-
$comparable_name = strtolower( $attribute_name );
2270+
$comparable_name = strtolower( str_replace( "\x00", "\u{FFFD}", $attribute_name ) );
22662271

22672272
// If an attribute is listed many times, only use the first declaration and ignore the rest.
22682273
if ( ! isset( $this->attributes[ $comparable_name ] ) ) {
@@ -2389,13 +2394,7 @@ private function class_name_updates_to_attributes_updates(): void {
23892394
}
23902395

23912396
if ( false === $existing_class && isset( $this->attributes['class'] ) ) {
2392-
$existing_class = WP_HTML_Decoder::decode_attribute(
2393-
substr(
2394-
$this->html,
2395-
$this->attributes['class']->value_starts_at,
2396-
$this->attributes['class']->value_length
2397-
)
2398-
);
2397+
$existing_class = $this->get_decoded_attribute_value( $this->attributes['class'] );
23992398
}
24002399

24012400
if ( false === $existing_class ) {
@@ -2823,7 +2822,7 @@ public function get_attribute( $name ) {
28232822
* attribute values. If any exist, those enqueued class changes must first be flushed out
28242823
* into an attribute value update.
28252824
*/
2826-
if ( 'class' === $name ) {
2825+
if ( 'class' === $comparable ) {
28272826
$this->class_name_updates_to_attributes_updates();
28282827
}
28292828

@@ -2854,8 +2853,28 @@ public function get_attribute( $name ) {
28542853
return true;
28552854
}
28562855

2857-
$raw_value = substr( $this->html, $attribute->value_starts_at, $attribute->value_length );
2856+
return $this->get_decoded_attribute_value( $attribute );
2857+
}
28582858

2859+
/**
2860+
* Decode an attribute value from source.
2861+
*
2862+
* This method applies the following transformations that the processor defers:
2863+
* - Normalize newlines (input stream preprocessing)
2864+
* - Replace NULL bytes (tokenization)
2865+
* - Decode character references (tokenization)
2866+
*
2867+
* @since 7.1.0
2868+
* @ignore
2869+
*
2870+
* @param WP_HTML_Attribute_Token $attribute Attribute token from the input document.
2871+
* @return string Decoded attribute value.
2872+
*/
2873+
private function get_decoded_attribute_value( WP_HTML_Attribute_Token $attribute ): string {
2874+
$raw_value = substr( $this->html, $attribute->value_starts_at, $attribute->value_length );
2875+
$raw_value = str_replace( "\r\n", "\n", $raw_value );
2876+
$raw_value = str_replace( "\r", "\n", $raw_value );
2877+
$raw_value = str_replace( "\x00", "\u{FFFD}", $raw_value );
28592878
return WP_HTML_Decoder::decode_attribute( $raw_value );
28602879
}
28612880

@@ -2936,7 +2955,7 @@ public function get_tag(): ?string {
29362955
return null;
29372956
}
29382957

2939-
$tag_name = substr( $this->html, $this->tag_name_starts_at, $this->tag_name_length );
2958+
$tag_name = str_replace( "\x00", "\u{FFFD}", substr( $this->html, $this->tag_name_starts_at, $this->tag_name_length ) );
29402959

29412960
if ( self::STATE_MATCHED_TAG === $this->parser_state ) {
29422961
return strtoupper( $tag_name );
@@ -4798,14 +4817,14 @@ private function matches(): bool {
47984817
}
47994818

48004819
// Does the tag name match the requested tag name in a case-insensitive manner?
4801-
if (
4802-
isset( $this->sought_tag_name ) &&
4803-
(
4804-
strlen( $this->sought_tag_name ) !== $this->tag_name_length ||
4805-
0 !== substr_compare( $this->html, $this->sought_tag_name, $this->tag_name_starts_at, $this->tag_name_length, true )
4806-
)
4807-
) {
4808-
return false;
4820+
if ( isset( $this->sought_tag_name ) ) {
4821+
$tag_name = $this->get_tag();
4822+
if (
4823+
strlen( $this->sought_tag_name ) !== strlen( $tag_name ) ||
4824+
0 !== substr_compare( $tag_name, $this->sought_tag_name, 0, null, true )
4825+
) {
4826+
return false;
4827+
}
48094828
}
48104829

48114830
if ( null !== $this->sought_class_name && ! $this->has_class( $this->sought_class_name ) ) {

tests/phpunit/tests/html-api/wpHtmlDecoder.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,43 @@ static function ( int $errno, string $errstr ) use ( &$errors ) {
110110
$this->assertSame( "&\x00b", $decoded, 'Should have decoded the text without changing it.' );
111111
}
112112

113+
/**
114+
* Ensures that numeric character references for U+0000 decode to U+FFFD
115+
* while raw NULL bytes pass through the decoder untransformed.
116+
*
117+
* The tokenizer, not the decoder, is responsible for replacing raw NULL
118+
* bytes; in the Tag Processor that responsibility falls on the methods
119+
* which read values out of the input document.
120+
*
121+
* @ticket 65372
122+
*
123+
* @dataProvider data_null_code_points
124+
*
125+
* @param string $raw_value Raw attribute value.
126+
* @param string $decoded_value The expected decoded attribute value.
127+
*/
128+
public function test_null_code_points_in_attribute_values( string $raw_value, string $decoded_value ): void {
129+
$this->assertSame(
130+
$decoded_value,
131+
WP_HTML_Decoder::decode_attribute( $raw_value ),
132+
'Improperly decoded raw attribute value.'
133+
);
134+
}
135+
136+
/**
137+
* Data provider.
138+
*
139+
* @return array<string, array{string, string}>
140+
*/
141+
public static function data_null_code_points(): array {
142+
return array(
143+
'Decimal zero' => array( 'a&#0;b', "a\u{FFFD}b" ),
144+
'Hexadecimal zero' => array( 'a&#x0;b', "a\u{FFFD}b" ),
145+
'Multiple zeros' => array( 'a&#0000;b', "a\u{FFFD}b" ),
146+
'Raw NULL byte passes through' => array( "a\x00b", "a\x00b" ),
147+
);
148+
}
149+
113150
/**
114151
* Ensures unmatched named character references leave the by-ref match length unchanged.
115152
*

0 commit comments

Comments
 (0)