| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * HTML API: WP_HTML_Decoder class 5 * 6 * Decodes spans of raw text found inside HTML content. 7 * 8 * @package WordPress 9 * @subpackage HTML-API 10 * @since 6.6.0 11 */ 12 class WP_HTML_Decoder { 13 /** 14 * Indicates if an attribute value starts with a given raw string value. 15 * 16 * Use this method to determine if an attribute value starts with a given string, regardless 17 * of how it might be encoded in HTML. For instance, `http:` could be represented as `http:` 18 * or as `http:` or as `http:` or as `http:`, or in many other ways. 19 * 20 * This is equivalent to a byte-prefix test against the decoded attribute value, without 21 * the need to allocate and decode the full string. 22 * 23 * Example: 24 * 25 * $value = 'http://wordpress.org/'; 26 * true === WP_HTML_Decoder::attribute_starts_with( $value, 'http:', 'ascii-case-insensitive' ); 27 * false === WP_HTML_Decoder::attribute_starts_with( $value, 'https:', 'ascii-case-insensitive' ); 28 * 29 * @since 6.6.0 30 * 31 * @param string $haystack String containing the raw non-decoded attribute value. 32 * @param string $search_text Does the attribute value start with this plain string. 33 * @param string $case_sensitivity Optional. Pass 'ascii-case-insensitive' to ignore ASCII case when matching. 34 * Default 'case-sensitive'. 35 * @return bool Whether the attribute value starts with the given string. 36 */ 37 public static function attribute_starts_with( $haystack, $search_text, $case_sensitivity = 'case-sensitive' ): bool { 38 $search_length = strlen( $search_text ); 39 $loose_case = 'ascii-case-insensitive' === $case_sensitivity; 40 $haystack_end = strlen( $haystack ); 41 $search_at = 0; 42 $haystack_at = 0; 43 44 while ( $search_at < $search_length && $haystack_at < $haystack_end ) { 45 $chars_match = $loose_case 46 ? strtolower( $haystack[ $haystack_at ] ) === strtolower( $search_text[ $search_at ] ) 47 : $haystack[ $haystack_at ] === $search_text[ $search_at ]; 48 49 $is_introducer = '&' === $haystack[ $haystack_at ]; 50 $next_chunk = $is_introducer 51 ? self::read_character_reference( 'attribute', $haystack, $haystack_at, $token_length ) 52 : null; 53 54 // If there's no character reference and the characters don't match, the match fails. 55 if ( null === $next_chunk && ! $chars_match ) { 56 return false; 57 } 58 59 // If there's no character reference but the characters do match, then it could still match. 60 if ( null === $next_chunk && $chars_match ) { 61 ++$haystack_at; 62 ++$search_at; 63 continue; 64 } 65 66 /** 67 * The decoded character reference in `$next_chunk` must be compared with the 68 * corresponding `$search_text` bytes checking for matching prefixes. The remaining 69 * search text may be shorter than the decoded chunk, in which case a partial match 70 * satisfies the prefix. Otherwise, if the decoded chunk is fully matched, the 71 * comparison must continue after advancing the appropriate byte lengths: the character 72 * reference token length in the haystack and the decoded chunk length in the 73 * search text. 74 * 75 * For example, consider searches that have reached the character reference 76 * `fj` (7 bytes), decoded into the 2-byte chunk `fj`: 77 * 78 * $haystack_at 79 * │ 80 * │ ┌─after matching `fj` continue here 81 * │ │ (advance by $token_length, 7 bytes) 82 * ↓ ↓ 83 * Haystack: startfjord 84 * ╰──┬──╯ 85 * fj - the decoded chunk, tested against the search text. 86 * 87 * $search_at 88 * │ 89 * │ ┌─after matching `fj` continue here 90 * │ │ (advance by $match_length, 2 bytes) 91 * ↓ ↓ 92 * Search A: startfjord Compare 2 bytes: `fj` matches, 93 * continue matching at `o`. 94 * 95 * $search_at 96 * ↓ 97 * Search B: startf Compare 1 byte: `f` matches and the 98 * search text is exhausted — prefix confirmed. 99 * 100 * $search_at 101 * ↓ 102 * Search C: startfr Compare 2 bytes: `fj` differs 103 * from `fr`, no match is possible. 104 * 105 * The `min()` is required in both directions: Search A fails if the 106 * comparison length comes from the search text, Search B if it comes 107 * from the chunk. 108 * 109 * After a match each cursor must advance by the appropriate length, the haystack 110 * cursor by the character reference token length, and the search cursor by the 111 * matched length. 112 */ 113 $match_length = min( strlen( $next_chunk ), $search_length - $search_at ); 114 if ( 0 !== substr_compare( $search_text, $next_chunk, $search_at, $match_length, $loose_case ) ) { 115 return false; 116 } 117 118 // The character reference matched, so continue checking. 119 $haystack_at += $token_length; 120 $search_at += $match_length; 121 } 122 123 return $search_at === $search_length; 124 } 125 126 /** 127 * Returns a string containing the decoded value of a given HTML text node. 128 * 129 * Text nodes appear in HTML DATA sections, which are the text segments inside 130 * and around tags, excepting SCRIPT and STYLE elements (and some others), 131 * whose inner text is not decoded. Use this function to read the decoded 132 * value of such a text span in an HTML document. 133 * 134 * Example: 135 * 136 * '“😄”' === WP_HTML_Decoder::decode_text_node( '“😄”' ); 137 * 138 * @since 6.6.0 139 * 140 * @param string $text Text containing raw and non-decoded text node to decode. 141 * @return string Decoded UTF-8 value of given text node. 142 */ 143 public static function decode_text_node( $text ): string { 144 return static::decode( 'data', $text ); 145 } 146 147 /** 148 * Returns a string containing the decoded value of a given HTML attribute. 149 * 150 * Text found inside an HTML attribute has different parsing rules than for 151 * text found inside other markup, or DATA segments. Use this function to 152 * read the decoded value of an HTML string inside a quoted attribute. 153 * 154 * Example: 155 * 156 * '“😄”' === WP_HTML_Decoder::decode_attribute( '“😄”' ); 157 * 158 * @since 6.6.0 159 * 160 * @param string $text Text containing raw and non-decoded attribute value to decode. 161 * @return string Decoded UTF-8 value of given attribute value. 162 */ 163 public static function decode_attribute( $text ): string { 164 return static::decode( 'attribute', $text ); 165 } 166 167 /** 168 * Decodes a span of HTML text, depending on the context in which it's found. 169 * 170 * This is a low-level method; prefer calling WP_HTML_Decoder::decode_attribute() or 171 * WP_HTML_Decoder::decode_text_node() instead. It's provided for cases where this 172 * may be difficult to do from calling code. 173 * 174 * Example: 175 * 176 * '©' = WP_HTML_Decoder::decode( 'data', '©' ); 177 * 178 * @since 6.6.0 179 * 180 * @access private 181 * 182 * @param string $context `attribute` for decoding attribute values, `data` otherwise. 183 * @param string $text Text document containing span of text to decode. 184 * @return string Decoded UTF-8 string. 185 */ 186 public static function decode( $context, $text ): string { 187 $decoded = ''; 188 $end = strlen( $text ); 189 $at = 0; 190 $was_at = 0; 191 192 while ( $at < $end ) { 193 $next_character_reference_at = strpos( $text, '&', $at ); 194 if ( false === $next_character_reference_at ) { 195 break; 196 } 197 198 $character_reference = self::read_character_reference( $context, $text, $next_character_reference_at, $token_length ); 199 if ( isset( $character_reference ) ) { 200 $at = $next_character_reference_at; 201 $decoded .= substr( $text, $was_at, $at - $was_at ); 202 $decoded .= $character_reference; 203 $at += $token_length; 204 $was_at = $at; 205 continue; 206 } 207 208 ++$at; 209 } 210 211 if ( 0 === $was_at ) { 212 return $text; 213 } 214 215 if ( $was_at < $end ) { 216 $decoded .= substr( $text, $was_at, $end - $was_at ); 217 } 218 219 return $decoded; 220 } 221 222 /** 223 * Attempt to read a character reference at the given location in a given string, 224 * depending on the context in which it's found. 225 * 226 * If a character reference is found, this function will return the translated value 227 * that the reference maps to. It will then set `$match_byte_length` the 228 * number of bytes of input it read while consuming the character reference. This 229 * gives calling code the opportunity to advance its cursor when traversing a string 230 * and decoding. 231 * 232 * Example: 233 * 234 * null === WP_HTML_Decoder::read_character_reference( 'attribute', 'Ships…', 0 ); 235 * '…' === WP_HTML_Decoder::read_character_reference( 'attribute', 'Ships…', 5, $token_length ); 236 * 8 === $token_length; // `…` 237 * 238 * null === WP_HTML_Decoder::read_character_reference( 'attribute', '¬in', 0 ); 239 * '∉' === WP_HTML_Decoder::read_character_reference( 'attribute', '∉', 0, $token_length ); 240 * 7 === $token_length; // `∉` 241 * 242 * '¬' === WP_HTML_Decoder::read_character_reference( 'data', '¬in', 0, $token_length ); 243 * 4 === $token_length; // `¬` 244 * '∉' === WP_HTML_Decoder::read_character_reference( 'data', '∉', 0, $token_length ); 245 * 7 === $token_length; // `∉` 246 * 247 * @since 6.6.0 248 * 249 * @global WP_Token_Map $html5_named_character_references Mappings for HTML5 named character references. 250 * 251 * @param string $context `attribute` for decoding attribute values, `data` otherwise. 252 * @param string $text Text document containing span of text to decode. 253 * @param int $at Optional. Byte offset into text where span begins, defaults to the beginning (0). 254 * @param int &$match_byte_length Optional. Set to byte-length of character reference if provided and if a match 255 * is found, otherwise not set. Default null. 256 * @return ?string Decoded character reference in UTF-8 if found, otherwise null. 257 */ 258 public static function read_character_reference( $context, $text, $at = 0, &$match_byte_length = null ) { 259 /** 260 * Mappings for HTML5 named character references. 261 * 262 * @var WP_Token_Map $html5_named_character_references 263 */ 264 global $html5_named_character_references; 265 266 $length = strlen( $text ); 267 if ( $at + 1 >= $length ) { 268 return null; 269 } 270 271 if ( '&' !== $text[ $at ] ) { 272 return null; 273 } 274 275 /* 276 * Numeric character references. 277 * 278 * When truncated, these will encode the code point found by parsing the 279 * digits that are available. For example, when `🅰` is truncated 280 * to `DZ` it will encode `DZ`. It does not: 281 * - know how to parse the original `🅰`. 282 * - fail to parse and return plaintext `DZ`. 283 * - fail to parse and return the replacement character `�` 284 */ 285 if ( '#' === $text[ $at + 1 ] ) { 286 if ( $at + 2 >= $length ) { 287 return null; 288 } 289 290 /** Tracks inner parsing within the numeric character reference. */ 291 $digits_at = $at + 2; 292 293 if ( 'x' === $text[ $digits_at ] || 'X' === $text[ $digits_at ] ) { 294 $numeric_base = 16; 295 $numeric_digits = '0123456789abcdefABCDEF'; 296 $max_digits = 6; //  297 ++$digits_at; 298 } else { 299 $numeric_base = 10; 300 $numeric_digits = '0123456789'; 301 $max_digits = 7; //  302 } 303 304 // Cannot encode invalid Unicode code points. Max is to U+10FFFF. 305 $zero_count = strspn( $text, '0', $digits_at ); 306 $digit_count = strspn( $text, $numeric_digits, $digits_at + $zero_count ); 307 $after_digits = $digits_at + $zero_count + $digit_count; 308 $has_semicolon = $after_digits < $length && ';' === $text[ $after_digits ]; 309 $end_of_span = $has_semicolon ? $after_digits + 1 : $after_digits; 310 311 // `&#` or `&#x` without digits returns into plaintext. 312 if ( 0 === $digit_count && 0 === $zero_count ) { 313 return null; 314 } 315 316 // Whereas `&#` and only zeros is invalid. 317 if ( 0 === $digit_count ) { 318 $match_byte_length = $end_of_span - $at; 319 return '�'; 320 } 321 322 // If there are too many digits then it's not worth parsing. It's invalid. 323 if ( $digit_count > $max_digits ) { 324 $match_byte_length = $end_of_span - $at; 325 return '�'; 326 } 327 328 $digits = substr( $text, $digits_at + $zero_count, $digit_count ); 329 $code_point = intval( $digits, $numeric_base ); 330 331 /* 332 * Noncharacters, 0x0D, and non-ASCII-whitespace control characters. 333 * 334 * > A noncharacter is a code point that is in the range U+FDD0 to U+FDEF, 335 * > inclusive, or U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF, 336 * > U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE, 337 * > U+6FFFF, U+7FFFE, U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF, 338 * > U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, U+DFFFE, 339 * > U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, or U+10FFFF. 340 * 341 * A C0 control is a code point that is in the range of U+00 to U+1F, 342 * but ASCII whitespace includes U+09, U+0A, U+0C, and U+0D. 343 * 344 * These characters are invalid but still decode as any valid character. 345 * This comment is here to note and explain why there's no check to 346 * remove these characters or replace them. 347 * 348 * @see https://infra.spec.whatwg.org/#noncharacter 349 */ 350 351 /* 352 * Code points in the C1 controls area need to be remapped as if they 353 * were stored in Windows-1252. Note! This transformation only happens 354 * for numeric character references. The raw code points in the byte 355 * stream are not translated. 356 * 357 * > If the number is one of the numbers in the first column of 358 * > the following table, then find the row with that number in 359 * > the first column, and set the character reference code to 360 * > the number in the second column of that row. 361 */ 362 if ( $code_point >= 0x80 && $code_point <= 0x9F ) { 363 $windows_1252_mapping = array( 364 0x20AC, // 0x80 -> EURO SIGN (€). 365 0x81, // 0x81 -> (no change). 366 0x201A, // 0x82 -> SINGLE LOW-9 QUOTATION MARK (‚). 367 0x0192, // 0x83 -> LATIN SMALL LETTER F WITH HOOK (ƒ). 368 0x201E, // 0x84 -> DOUBLE LOW-9 QUOTATION MARK („). 369 0x2026, // 0x85 -> HORIZONTAL ELLIPSIS (…). 370 0x2020, // 0x86 -> DAGGER (†). 371 0x2021, // 0x87 -> DOUBLE DAGGER (‡). 372 0x02C6, // 0x88 -> MODIFIER LETTER CIRCUMFLEX ACCENT (ˆ). 373 0x2030, // 0x89 -> PER MILLE SIGN (‰). 374 0x0160, // 0x8A -> LATIN CAPITAL LETTER S WITH CARON (Š). 375 0x2039, // 0x8B -> SINGLE LEFT-POINTING ANGLE QUOTATION MARK (‹). 376 0x0152, // 0x8C -> LATIN CAPITAL LIGATURE OE (Œ). 377 0x8D, // 0x8D -> (no change). 378 0x017D, // 0x8E -> LATIN CAPITAL LETTER Z WITH CARON (Ž). 379 0x8F, // 0x8F -> (no change). 380 0x90, // 0x90 -> (no change). 381 0x2018, // 0x91 -> LEFT SINGLE QUOTATION MARK (‘). 382 0x2019, // 0x92 -> RIGHT SINGLE QUOTATION MARK (’). 383 0x201C, // 0x93 -> LEFT DOUBLE QUOTATION MARK (“). 384 0x201D, // 0x94 -> RIGHT DOUBLE QUOTATION MARK (”). 385 0x2022, // 0x95 -> BULLET (•). 386 0x2013, // 0x96 -> EN DASH (–). 387 0x2014, // 0x97 -> EM DASH (—). 388 0x02DC, // 0x98 -> SMALL TILDE (˜). 389 0x2122, // 0x99 -> TRADE MARK SIGN (™). 390 0x0161, // 0x9A -> LATIN SMALL LETTER S WITH CARON (š). 391 0x203A, // 0x9B -> SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (›). 392 0x0153, // 0x9C -> LATIN SMALL LIGATURE OE (œ). 393 0x9D, // 0x9D -> (no change). 394 0x017E, // 0x9E -> LATIN SMALL LETTER Z WITH CARON (ž). 395 0x0178, // 0x9F -> LATIN CAPITAL LETTER Y WITH DIAERESIS (Ÿ). 396 ); 397 398 $code_point = $windows_1252_mapping[ $code_point - 0x80 ]; 399 } 400 401 $match_byte_length = $end_of_span - $at; 402 return self::code_point_to_utf8_bytes( $code_point ); 403 } 404 405 /** Tracks inner parsing within the named character reference. */ 406 $name_at = $at + 1; 407 // Minimum named character reference is two characters. E.g. `GT`. 408 if ( $name_at + 2 > $length ) { 409 return null; 410 } 411 412 $name_length = 0; 413 $replacement = $html5_named_character_references->read_token( $text, $name_at, $name_length ); 414 if ( null === $replacement ) { 415 return null; 416 } 417 418 $after_name = $name_at + $name_length; 419 420 /** 421 * For historical reasons, a matched named character reference is left as literal 422 * text (its decoded replacement is not used) when all of the following hold: 423 * 424 * 1. It was matched in attribute context. 425 * 2. The match does not end in U+003B SEMICOLON (;) — i.e. it is one of the 426 * legacy forms recognized without a trailing semicolon. 427 * 3. The next input character is U+003D EQUALS SIGN (=) or an ASCII alphanumeric. 428 * 429 * Some illustrative examples follow. Note that both `not` and `not;` appear in the 430 * named character references list. References start with `&` and typically end with 431 * `;`, but the legacy forms are recognized without one. 432 * 433 * - In _data context_, "¬me" is decoded to "¬me": condition 1 fails (not an 434 * attribute), so the reference is decoded. 435 * - In _attribute context_, "¬me" is decoded to "¬me": the longest match is 436 * "not;", which ends in a semicolon, so condition 2 fails. 437 * - In _attribute context_, "¬己" is decoded to "¬己": the following character 438 * "己" is a letter but not an ASCII alphanumeric (nor "="), so condition 3 fails. 439 * - In _attribute context_, "¬" is decoded to "¬": there is no next input 440 * character, so condition 3 fails. 441 * - In _attribute context_, "¬=me" is left as the literal text "¬=me": all 442 * three conditions hold. 443 * - In _attribute context_, "¬me" is left as the literal text "¬me": all 444 * three conditions hold. 445 * 446 * Without these special rules, ordinary URL query strings could have surprising 447 * replacements applied. Consider: 448 * 449 * <a href="/?random°ree>=0<=360¬=90"> 450 * 451 * The literal attribute value `/?random°ree>=0<=360¬=90` is preserved 452 * by the special handling. Otherwise, the value would decode to 453 * `/?random°ree>=0<=360¬=90`, which is unlikely to be the author's intent. 454 * 455 * (Authors should not rely on this. Escaping the example as 456 * `/?random&degree&gt=0&lt=360&not=90` produces the intended 457 * value regardless of the following character.) 458 * 459 * @see https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state 460 * @see https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references 461 */ 462 if ( 'attribute' !== $context || ';' === $text[ $after_name - 1 ] || $after_name >= $length ) { 463 $match_byte_length = $after_name - $at; 464 return $replacement; 465 } 466 467 $follower_byte = ord( $text[ $after_name ] ); 468 if ( 469 0x3D === $follower_byte || // EQUALS SIGN 470 ( $follower_byte >= 0x30 && $follower_byte <= 0x39 ) || // ASCII digits 0-9 471 ( $follower_byte >= 0x41 && $follower_byte <= 0x5A ) || // ASCII upper alpha A-Z 472 ( $follower_byte >= 0x61 && $follower_byte <= 0x7A ) // ASCII lower alpha a-z 473 ) { 474 return null; 475 } 476 477 $match_byte_length = $after_name - $at; 478 return $replacement; 479 } 480 481 /** 482 * Encode a code point number into the UTF-8 encoding. 483 * 484 * This encoder implements the UTF-8 encoding algorithm for converting 485 * a code point into a byte sequence. If it receives an invalid code 486 * point it will return the Unicode Replacement Character U+FFFD `�`. 487 * 488 * Example: 489 * 490 * '🅰' === WP_HTML_Decoder::code_point_to_utf8_bytes( 0x1f170 ); 491 * 492 * // Half of a surrogate pair is an invalid code point. 493 * '�' === WP_HTML_Decoder::code_point_to_utf8_bytes( 0xd83c ); 494 * 495 * @since 6.6.0 496 * 497 * @see https://www.rfc-editor.org/rfc/rfc3629 For the UTF-8 standard. 498 * 499 * @param int $code_point Which code point to convert. 500 * @return string Converted code point, or `�` if invalid. 501 */ 502 public static function code_point_to_utf8_bytes( $code_point ): string { 503 $string = mb_chr( $code_point, 'UTF-8' ); 504 505 return false !== $string ? $string : '�'; 506 } 507 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Jul 25 08:20:20 2026 | Cross-referenced by PHPXref |