PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta.2
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta.2
16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / vendor / wp-php-toolkit / html / class-wp-html-decoder.php
jetpack / vendor / wp-php-toolkit / html Last commit date
PHP 6 days ago LICENSE.md 6 days ago README.md 6 days ago class-wp-html-active-formatting-elements.php 6 days ago class-wp-html-attribute-token.php 6 days ago class-wp-html-decoder.php 6 days ago class-wp-html-doctype-info.php 6 days ago class-wp-html-native-processor-wrapper.php 6 days ago class-wp-html-native-tag-processor-wrapper.php 6 days ago class-wp-html-open-elements.php 6 days ago class-wp-html-processor-state.php 6 days ago class-wp-html-processor.php 6 days ago class-wp-html-span.php 6 days ago class-wp-html-stack-event.php 6 days ago class-wp-html-tag-processor.php 6 days ago class-wp-html-text-replacement.php 6 days ago class-wp-html-token.php 6 days ago class-wp-html-unsupported-exception.php 6 days ago class-wp-token-map.php 6 days ago composer.json 6 days ago html5-named-character-references.php 6 days ago
class-wp-html-decoder.php
483 lines
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&colon;` or as `&#x68;ttp:` or as `h&#116;tp&colon;`, or in many other ways.
19 *
20 * Example:
21 *
22 * $value = 'http&colon;//wordpress.org/';
23 * true === WP_HTML_Decoder::attribute_starts_with( $value, 'http:', 'ascii-case-insensitive' );
24 * false === WP_HTML_Decoder::attribute_starts_with( $value, 'https:', 'ascii-case-insensitive' );
25 *
26 * @param string $haystack String containing the raw non-decoded attribute value.
27 * @param string $search_text Does the attribute value start with this plain string.
28 * @param string $case_sensitivity Optional. Pass 'ascii-case-insensitive' to ignore ASCII case when matching.
29 * Default 'case-sensitive'.
30 *
31 * @return bool Whether the attribute value starts with the given string.
32 * @since 6.6.0
33 */
34 public static function attribute_starts_with( $haystack, $search_text, $case_sensitivity = 'case-sensitive' ): bool {
35 $search_length = strlen( $search_text );
36 $loose_case = 'ascii-case-insensitive' === $case_sensitivity;
37 $haystack_end = strlen( $haystack );
38 $search_at = 0;
39 $haystack_at = 0;
40
41 while ( $search_at < $search_length && $haystack_at < $haystack_end ) {
42 $chars_match = $loose_case
43 ? strtolower( $haystack[ $haystack_at ] ) === strtolower( $search_text[ $search_at ] )
44 : $haystack[ $haystack_at ] === $search_text[ $search_at ];
45
46 $is_introducer = '&' === $haystack[ $haystack_at ];
47 $next_chunk = $is_introducer
48 ? self::read_character_reference( 'attribute', $haystack, $haystack_at, $token_length )
49 : null;
50
51 // If there's no character reference and the characters don't match, the match fails.
52 if ( null === $next_chunk && ! $chars_match ) {
53 return false;
54 }
55
56 // If there's no character reference but the character do match, then it could still match.
57 if ( null === $next_chunk && $chars_match ) {
58 ++$haystack_at;
59 ++$search_at;
60 continue;
61 }
62
63 // If there is a character reference, then the decoded value must exactly match what follows in the search string.
64 if ( 0 !== substr_compare( $search_text, $next_chunk, $search_at, strlen( $next_chunk ), $loose_case ) ) {
65 return false;
66 }
67
68 // The character reference matched, so continue checking.
69 $haystack_at += $token_length;
70 $search_at += strlen( $next_chunk );
71 }
72
73 return true;
74 }
75
76 /**
77 * Returns a string containing the decoded value of a given HTML text node.
78 *
79 * Text nodes appear in HTML DATA sections, which are the text segments inside
80 * and around tags, excepting SCRIPT and STYLE elements (and some others),
81 * whose inner text is not decoded. Use this function to read the decoded
82 * value of such a text span in an HTML document.
83 *
84 * Example:
85 *
86 * '“😄”' === WP_HTML_Decode::decode_text_node( '&#x93;&#x1f604;&#x94' );
87 *
88 * @param string $text Text containing raw and non-decoded text node to decode.
89 *
90 * @return string Decoded UTF-8 value of given text node.
91 * @since 6.6.0
92 */
93 public static function decode_text_node( $text ): string {
94 return static::decode( 'data', $text );
95 }
96
97 /**
98 * Returns a string containing the decoded value of a given HTML attribute.
99 *
100 * Text found inside an HTML attribute has different parsing rules than for
101 * text found inside other markup, or DATA segments. Use this function to
102 * read the decoded value of an HTML string inside a quoted attribute.
103 *
104 * Example:
105 *
106 * '“😄”' === WP_HTML_Decode::decode_attribute( '&#x93;&#x1f604;&#x94' );
107 *
108 * @param string $text Text containing raw and non-decoded attribute value to decode.
109 *
110 * @return string Decoded UTF-8 value of given attribute value.
111 * @since 6.6.0
112 */
113 public static function decode_attribute( $text ): string {
114 return static::decode( 'attribute', $text );
115 }
116
117 /**
118 * Decodes a span of HTML text, depending on the context in which it's found.
119 *
120 * This is a low-level method; prefer calling WP_HTML_Decoder::decode_attribute() or
121 * WP_HTML_Decoder::decode_text_node() instead. It's provided for cases where this
122 * may be difficult to do from calling code.
123 *
124 * Example:
125 *
126 * '©' = WP_HTML_Decoder::decode( 'data', '&copy;' );
127 *
128 * @param string $context `attribute` for decoding attribute values, `data` otherwise.
129 * @param string $text Text document containing span of text to decode.
130 *
131 * @return string Decoded UTF-8 string.
132 * @since 6.6.0
133 *
134 * @access private
135 */
136 public static function decode( $context, $text ): string {
137 $decoded = '';
138 $end = strlen( $text );
139 $at = 0;
140 $was_at = 0;
141
142 while ( $at < $end ) {
143 $next_character_reference_at = strpos( $text, '&', $at );
144 if ( false === $next_character_reference_at ) {
145 break;
146 }
147
148 $character_reference = self::read_character_reference( $context, $text, $next_character_reference_at, $token_length );
149 if ( isset( $character_reference ) ) {
150 $at = $next_character_reference_at;
151 $decoded .= substr( $text, $was_at, $at - $was_at );
152 $decoded .= $character_reference;
153 $at += $token_length;
154 $was_at = $at;
155 continue;
156 }
157
158 ++$at;
159 }
160
161 if ( 0 === $was_at ) {
162 return $text;
163 }
164
165 if ( $was_at < $end ) {
166 $decoded .= substr( $text, $was_at, $end - $was_at );
167 }
168
169 return $decoded;
170 }
171
172 /**
173 * Attempt to read a character reference at the given location in a given string,
174 * depending on the context in which it's found.
175 *
176 * If a character reference is found, this function will return the translated value
177 * that the reference maps to. It will then set `$match_byte_length` the
178 * number of bytes of input it read while consuming the character reference. This
179 * gives calling code the opportunity to advance its cursor when traversing a string
180 * and decoding.
181 *
182 * Example:
183 *
184 * null === WP_HTML_Decoder::read_character_reference( 'attribute', 'Ships&hellip;', 0 );
185 * '…' === WP_HTML_Decoder::read_character_reference( 'attribute', 'Ships&hellip;', 5, $token_length );
186 * 8 === $token_length; // `&hellip;`
187 *
188 * null === WP_HTML_Decoder::read_character_reference( 'attribute', '&notin', 0 );
189 * '∉' === WP_HTML_Decoder::read_character_reference( 'attribute', '&notin;', 0, $token_length );
190 * 7 === $token_length; // `&notin;`
191 *
192 * '¬' === WP_HTML_Decoder::read_character_reference( 'data', '&notin', 0, $token_length );
193 * 4 === $token_length; // `&not`
194 * '∉' === WP_HTML_Decoder::read_character_reference( 'data', '&notin;', 0, $token_length );
195 * 7 === $token_length; // `&notin;`
196 *
197 * @param string $context `attribute` for decoding attribute values, `data` otherwise.
198 * @param string $text Text document containing span of text to decode.
199 * @param int $at Optional. Byte offset into text where span begins, defaults to the beginning (0).
200 * @param int &$match_byte_length Optional. Set to byte-length of character reference if provided and if a match
201 * is found, otherwise not set. Default null.
202 *
203 * @return string|false Decoded character reference in UTF-8 if found, otherwise `false`.
204 * @global WP_Token_Map $html5_named_character_references Mappings for HTML5 named character references.
205 *
206 * @since 6.6.0
207 */
208 public static function read_character_reference( $context, $text, $at = 0, &$match_byte_length = null ) {
209 /**
210 * Mappings for HTML5 named character references.
211 *
212 * Loaded lazily on first call to avoid declaring WP_Token_Map at
213 * Composer bootstrap. Eager loading via autoload.files breaks
214 * consumers that also load WordPress core, since both shipments
215 * declare the same WP_* classes and PHP fatals on the second
216 * declaration. WordPress sets this global itself, so in a WP
217 * context we never include our copy.
218 *
219 * @var WP_Token_Map $html5_named_character_references
220 */
221 global $html5_named_character_references;
222
223 if ( ! isset( $html5_named_character_references ) ) {
224 require_once __DIR__ . '/html5-named-character-references.php';
225 }
226
227 $length = strlen( $text );
228 if ( $at + 1 >= $length ) {
229 return null;
230 }
231
232 if ( '&' !== $text[ $at ] ) {
233 return null;
234 }
235
236 /*
237 * Numeric character references.
238 *
239 * When truncated, these will encode the code point found by parsing the
240 * digits that are available. For example, when `&#x1f170;` is truncated
241 * to `&#x1f1` it will encode `DZ`. It does not:
242 * - know how to parse the original `�
243 �`.
244 * - fail to parse and return plaintext `&#x1f1`.
245 * - fail to parse and return the replacement character `�`
246 */
247 if ( '#' === $text[ $at + 1 ] ) {
248 if ( $at + 2 >= $length ) {
249 return null;
250 }
251
252 /** Tracks inner parsing within the numeric character reference. */
253 $digits_at = $at + 2;
254
255 if ( 'x' === $text[ $digits_at ] || 'X' === $text[ $digits_at ] ) {
256 $numeric_base = 16;
257 $numeric_digits = '0123456789abcdefABCDEF';
258 $max_digits = 6; // &#x10FFFF;.
259 ++$digits_at;
260 } else {
261 $numeric_base = 10;
262 $numeric_digits = '0123456789';
263 $max_digits = 7; // &#1114111;.
264 }
265
266 // Cannot encode invalid Unicode code points. Max is to U+10FFFF.
267 $zero_count = strspn( $text, '0', $digits_at );
268 $digit_count = strspn( $text, $numeric_digits, $digits_at + $zero_count );
269 $after_digits = $digits_at + $zero_count + $digit_count;
270 $has_semicolon = $after_digits < $length && ';' === $text[ $after_digits ];
271 $end_of_span = $has_semicolon ? $after_digits + 1 : $after_digits;
272
273 // `&#` or `&#x` without digits returns into plaintext.
274 if ( 0 === $digit_count && 0 === $zero_count ) {
275 return null;
276 }
277
278 // Whereas `&#` and only zeros is invalid.
279 if ( 0 === $digit_count ) {
280 $match_byte_length = $end_of_span - $at;
281
282 return '';
283 }
284
285 // If there are too many digits then it's not worth parsing. It's invalid.
286 if ( $digit_count > $max_digits ) {
287 $match_byte_length = $end_of_span - $at;
288
289 return '';
290 }
291
292 $digits = substr( $text, $digits_at + $zero_count, $digit_count );
293 $codepoint = intval( $digits, $numeric_base );
294
295 /*
296 * Noncharacters, 0x0D, and non-ASCII-whitespace control characters.
297 *
298 * > A noncharacter is a code point that is in the range U+FDD0 to U+FDEF,
299 * > inclusive, or U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF,
300 * > U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE,
301 * > U+6FFFF, U+7FFFE, U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF,
302 * > U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, U+DFFFE,
303 * > U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, or U+10FFFF.
304 *
305 * A C0 control is a code point that is in the range of U+00 to U+1F,
306 * but ASCII whitespace includes U+09, U+0A, U+0C, and U+0D.
307 *
308 * These characters are invalid but still decode as any valid character.
309 * This comment is here to note and explain why there's no check to
310 * remove these characters or replace them.
311 *
312 * @see https://infra.spec.whatwg.org/#noncharacter
313 */
314
315 /*
316 * Code points in the C1 controls area need to be remapped as if they
317 * were stored in Windows-1252. Note! This transformation only happens
318 * for numeric character references. The raw code points in the byte
319 * stream are not translated.
320 *
321 * > If the number is one of the numbers in the first column of
322 * > the following table, then find the row with that number in
323 * > the first column, and set the character reference code to
324 * > the number in the second column of that row.
325 */
326 if ( $codepoint >= 0x80 && $codepoint <= 0x9F ) {
327 $windows_1252_mapping = array(
328 0x20AC, // 0x80 -> EURO SIGN (€).
329 0x81, // 0x81 -> (no change).
330 0x201A, // 0x82 -> SINGLE LOW-9 QUOTATION MARK (‚).
331 0x0192, // 0x83 -> LATIN SMALL LETTER F WITH HOOK (ƒ).
332 0x201E, // 0x84 -> DOUBLE LOW-9 QUOTATION MARK („).
333 0x2026, // 0x85 -> HORIZONTAL ELLIPSIS (…).
334 0x2020, // 0x86 -> DAGGER (†).
335 0x2021, // 0x87 -> DOUBLE DAGGER (‡).
336 0x02C6, // 0x88 -> MODIFIER LETTER CIRCUMFLEX ACCENT (ˆ).
337 0x2030, // 0x89 -> PER MILLE SIGN (‰).
338 0x0160, // 0x8A -> LATIN CAPITAL LETTER S WITH CARON (Š).
339 0x2039, // 0x8B -> SINGLE LEFT-POINTING ANGLE QUOTATION MARK (‹).
340 0x0152, // 0x8C -> LATIN CAPITAL LIGATURE OE (Œ).
341 0x8D, // 0x8D -> (no change).
342 0x017D, // 0x8E -> LATIN CAPITAL LETTER Z WITH CARON (Ž).
343 0x8F, // 0x8F -> (no change).
344 0x90, // 0x90 -> (no change).
345 0x2018, // 0x91 -> LEFT SINGLE QUOTATION MARK (‘).
346 0x2019, // 0x92 -> RIGHT SINGLE QUOTATION MARK (’).
347 0x201C, // 0x93 -> LEFT DOUBLE QUOTATION MARK (“).
348 0x201D, // 0x94 -> RIGHT DOUBLE QUOTATION MARK (”).
349 0x2022, // 0x95 -> BULLET (•).
350 0x2013, // 0x96 -> EN DASH (–).
351 0x2014, // 0x97 -> EM DASH (—).
352 0x02DC, // 0x98 -> SMALL TILDE (˜).
353 0x2122, // 0x99 -> TRADE MARK SIGN (™).
354 0x0161, // 0x9A -> LATIN SMALL LETTER S WITH CARON (š).
355 0x203A, // 0x9B -> SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (›).
356 0x0153, // 0x9C -> LATIN SMALL LIGATURE OE (œ).
357 0x9D, // 0x9D -> (no change).
358 0x017E, // 0x9E -> LATIN SMALL LETTER Z WITH CARON (ž).
359 0x0178, // 0x9F -> LATIN CAPITAL LETTER Y WITH DIAERESIS (Ÿ).
360 );
361
362 $codepoint = $windows_1252_mapping[ $codepoint - 0x80 ];
363 }
364
365 $match_byte_length = $end_of_span - $at;
366
367 return self::codepoint_to_utf8_bytes( $codepoint );
368 }
369
370 /** Tracks inner parsing within the named character reference. */
371 $name_at = $at + 1;
372 // Minimum named character reference is two characters. E.g. `GT`.
373 if ( $name_at + 2 > $length ) {
374 return null;
375 }
376
377 $name_length = 0;
378 $replacement = $html5_named_character_references->read_token( $text, $name_at, $name_length );
379 if ( false === $replacement ) {
380 return null;
381 }
382
383 $after_name = $name_at + $name_length;
384
385 // If the match ended with a semicolon then it should always be decoded.
386 if ( ';' === $text[ $name_at + $name_length - 1 ] ) {
387 $match_byte_length = $after_name - $at;
388
389 return $replacement;
390 }
391
392 /*
393 * At this point though there's a match for an entry in the named
394 * character reference table but the match doesn't end in `;`.
395 * It may be allowed if it's followed by something unambiguous.
396 */
397 $ambiguous_follower = (
398 $after_name < $length &&
399 $name_at < $length &&
400 (
401 ctype_alnum( $text[ $after_name ] ) ||
402 '=' === $text[ $after_name ]
403 )
404 );
405
406 // It's non-ambiguous, safe to leave it in.
407 if ( ! $ambiguous_follower ) {
408 $match_byte_length = $after_name - $at;
409
410 return $replacement;
411 }
412
413 // It's ambiguous, which isn't allowed inside attributes.
414 if ( 'attribute' === $context ) {
415 return null;
416 }
417
418 $match_byte_length = $after_name - $at;
419
420 return $replacement;
421 }
422
423 /**
424 * Encode a code point number into the UTF-8 encoding.
425 *
426 * This encoder implements the UTF-8 encoding algorithm for converting
427 * a code point into a byte sequence. If it receives an invalid code
428 * point it will return the Unicode Replacement Character U+FFFD `�`.
429 *
430 * Example:
431 *
432 * '�
433 �' === WP_HTML_Decoder::codepoint_to_utf8_bytes( 0x1f170 );
434 *
435 * // Half of a surrogate pair is an invalid code point.
436 * '�' === WP_HTML_Decoder::codepoint_to_utf8_bytes( 0xd83c );
437 *
438 * @param int $codepoint Which code point to convert.
439 *
440 * @return string Converted code point, or `�` if invalid.
441 * @since 6.6.0
442 *
443 * @see https://www.rfc-editor.org/rfc/rfc3629 For the UTF-8 standard.
444 */
445 public static function codepoint_to_utf8_bytes( $codepoint ): string {
446 // Pre-check to ensure a valid code point.
447 if (
448 $codepoint <= 0 ||
449 ( $codepoint >= 0xD800 && $codepoint <= 0xDFFF ) ||
450 $codepoint > 0x10FFFF
451 ) {
452 return '';
453 }
454
455 if ( $codepoint <= 0x7F ) {
456 return chr( $codepoint );
457 }
458
459 if ( $codepoint <= 0x7FF ) {
460 $byte1 = chr( ( $codepoint >> 6 ) | 0xC0 );
461 $byte2 = chr( $codepoint & 0x3F | 0x80 );
462
463 return "{$byte1}{$byte2}";
464 }
465
466 if ( $codepoint <= 0xFFFF ) {
467 $byte1 = chr( ( $codepoint >> 12 ) | 0xE0 );
468 $byte2 = chr( ( $codepoint >> 6 ) & 0x3F | 0x80 );
469 $byte3 = chr( $codepoint & 0x3F | 0x80 );
470
471 return "{$byte1}{$byte2}{$byte3}";
472 }
473
474 // Any values above U+10FFFF are eliminated above in the pre-check.
475 $byte1 = chr( ( $codepoint >> 18 ) | 0xF0 );
476 $byte2 = chr( ( $codepoint >> 12 ) & 0x3F | 0x80 );
477 $byte3 = chr( ( $codepoint >> 6 ) & 0x3F | 0x80 );
478 $byte4 = chr( $codepoint & 0x3F | 0x80 );
479
480 return "{$byte1}{$byte2}{$byte3}{$byte4}";
481 }
482 }
483