Skip to content

Commit b965238

Browse files
authored
Merge branch 'WordPress:trunk' into trunk
2 parents 50aa902 + 60e382b commit b965238

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)