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 / encoding / compat-utf8.php
jetpack / vendor / wp-php-toolkit / encoding Last commit date
LICENSE.md 6 days ago README.md 6 days ago compat-utf8.php 6 days ago composer.json 6 days ago utf8-encoder.php 6 days ago utf8.php 6 days ago
compat-utf8.php
568 lines
1 <?php
2
3 namespace WordPress\Encoding\compat;
4
5 /**
6 * Finds spans of valid and invalid UTF-8 bytes in a given string.
7 *
8 * This is a low-level tool to power various UTF-8 functionality.
9 * It scans through a string until it finds invalid byte spans.
10 * When it does this, it does three things:
11 *
12 * - Assigns `$at` to the position after the last successful code point.
13 * - Assigns `$invalid_length` to the length of the maximal subpart of
14 * the invalid bytes starting at `$at`.
15 * - Returns how many code points were successfully scanned.
16 *
17 * This information is enough to build a number of useful UTF-8 functions.
18 *
19 * Example:
20 *
21 * // ñ is U+F1, which in `ISO-8859-1`/`latin1`/`Windows-1252`/`cp1252` is 0xF1.
22 * "Pi\xF1a" === $pineapple = mb_convert_encoding( "Piña", 'Windows-1252', 'UTF-8' );
23 * $at = $invalid_length = 0;
24 *
25 * // The first step finds the invalid 0xF1 byte.
26 * 2 === _wp_scan_utf8( $pineapple, $at, $invalid_length );
27 * $at === 2; $invalid_length === 1;
28 *
29 * // The second step continues to the end of the string.
30 * 1 === _wp_scan_utf8( $pineapple, $at, $invalid_length );
31 * $at === 4; $invalid_length === 0;
32 *
33 * Note! While passing an options array here might be convenient from a calling-code standpoint,
34 * this function is intended to serve as a very low-level foundation upon which to build
35 * higher level functionality. For the sake of keeping costs explicit all arguments are
36 * passed directly.
37 *
38 * @since 6.9.0
39 * @access private
40 *
41 * @param string $bytes UTF-8 encoded string which might include invalid spans of bytes.
42 * @param int $at Where to start scanning.
43 * @param int $invalid_length Will be set to how many bytes are to be ignored after `$at`.
44 * @param int|null $max_bytes Stop scanning after this many bytes have been seen.
45 * @param int|null $max_code_points Stop scanning after this many code points have been seen.
46 * @param bool|null $has_noncharacters Set to indicate if scanned string contained noncharacters.
47 * @return int How many code points were successfully scanned.
48 */
49 function _wp_scan_utf8( string $bytes, int &$at, int &$invalid_length, ?int $max_bytes = null, ?int $max_code_points = null, ?bool &$has_noncharacters = null ): int {
50 $byte_length = strlen( $bytes );
51 $end = min( $byte_length, $at + ( $max_bytes ?? PHP_INT_MAX ) );
52 $invalid_length = 0;
53 $count = 0;
54 $max_count = $max_code_points ?? PHP_INT_MAX;
55 $has_noncharacters = false;
56
57 for ( $i = $at; $i < $end && $count <= $max_count; $i++ ) {
58 /*
59 * Quickly skip past US-ASCII bytes, all of which are valid UTF-8.
60 *
61 * This optimization step improves the speed from 10x to 100x
62 * depending on whether the JIT has optimized the function.
63 */
64 $ascii_byte_count = strspn(
65 $bytes,
66 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" .
67 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" .
68 " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f",
69 $i,
70 $end - $i
71 );
72
73 if ( $count + $ascii_byte_count >= $max_count ) {
74 $at = $i + ( $max_count - $count );
75 $count = $max_count;
76 return $count;
77 }
78
79 $count += $ascii_byte_count;
80 $i += $ascii_byte_count;
81
82 if ( $i >= $end ) {
83 $at = $end;
84 return $count;
85 }
86
87 /**
88 * The above fast-track handled all single-byte UTF-8 characters. What
89 * follows MUST be a multibyte sequence otherwise there’s invalid UTF-8.
90 *
91 * Therefore everything past here is checking those multibyte sequences.
92 *
93 * It may look like there’s a need to check against the max bytes here,
94 * but since each match of a single character returns, this functions will
95 * bail already if crossing the max-bytes threshold. This function SHALL
96 * NOT return in the middle of a multi-byte character, so if a character
97 * falls on each side of the max bytes, the entire character will be scanned.
98 *
99 * Because it’s possible that there are truncated characters, the use of
100 * the null-coalescing operator with "\xC0" is a convenience for skipping
101 * length checks on every continuation bytes. This works because 0xC0 is
102 * always invalid in a UTF-8 string, meaning that if the string has been
103 * truncated, it will find 0xC0 and reject as invalid UTF-8.
104 *
105 * > [The following table] lists all of the byte sequences that are well-formed
106 * > in UTF-8. A range of byte values such as A0..BF indicates that any byte
107 * > from A0 to BF (inclusive) is well-formed in that position. Any byte value
108 * > outside of the ranges listed is ill-formed.
109 *
110 * > Table 3-7. Well-Formed UTF-8 Byte Sequences
111 * ╭─────────────────────┬────────────┬──────────────┬─────────────┬──────────────╮
112 * │ Code Points │ First Byte │ Second Byte │ Third Byte │ Fourth Byte │
113 * ├─────────────────────┼────────────┼──────────────┼─────────────┼──────────────┤
114 * │ U+0000..U+007F │ 00..7F │ │ │ │
115 * │ U+0080..U+07FF │ C2..DF │ 80..BF │ │ │
116 * │ U+0800..U+0FFF │ E0 │ A0..BF │ 80..BF │ │
117 * │ U+1000..U+CFFF │ E1..EC │ 80..BF │ 80..BF │ │
118 * │ U+D000..U+D7FF │ ED │ 80..9F │ 80..BF │ │
119 * │ U+E000..U+FFFF │ EE..EF │ 80..BF │ 80..BF │ │
120 * │ U+10000..U+3FFFF │ F0 │ 90..BF │ 80..BF │ 80..BF │
121 * │ U+40000..U+FFFFF │ F1..F3 │ 80..BF │ 80..BF │ 80..BF │
122 * │ U+100000..U+10FFFF │ F4 │ 80..8F │ 80..BF │ 80..BF │
123 * ╰─────────────────────┴────────────┴──────────────┴─────────────┴──────────────╯
124 *
125 * @see https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G27506
126 */
127
128 // Valid two-byte code points.
129 $b1 = ord( $bytes[ $i ] );
130 $b2 = ord( $bytes[ $i + 1 ] ?? "\xC0" );
131
132 if ( $b1 >= 0xC2 && $b1 <= 0xDF && $b2 >= 0x80 && $b2 <= 0xBF ) {
133 ++$count;
134 ++$i;
135 continue;
136 }
137
138 // Valid three-byte code points.
139 $b3 = ord( $bytes[ $i + 2 ] ?? "\xC0" );
140
141 if ( $b3 < 0x80 || $b3 > 0xBF ) {
142 goto invalid_utf8;
143 }
144
145 if (
146 ( 0xE0 === $b1 && $b2 >= 0xA0 && $b2 <= 0xBF ) ||
147 ( $b1 >= 0xE1 && $b1 <= 0xEC && $b2 >= 0x80 && $b2 <= 0xBF ) ||
148 ( 0xED === $b1 && $b2 >= 0x80 && $b2 <= 0x9F ) ||
149 ( $b1 >= 0xEE && $b1 <= 0xEF && $b2 >= 0x80 && $b2 <= 0xBF )
150 ) {
151 ++$count;
152 $i += 2;
153
154 // Covers the range U+FDD0–U+FDEF, U+FFFE, U+FFFF.
155 if ( 0xEF === $b1 ) {
156 $has_noncharacters |= (
157 ( 0xB7 === $b2 && $b3 >= 0x90 && $b3 <= 0xAF ) ||
158 ( 0xBF === $b2 && ( 0xBE === $b3 || 0xBF === $b3 ) )
159 );
160 }
161
162 continue;
163 }
164
165 // Valid four-byte code points.
166 $b4 = ord( $bytes[ $i + 3 ] ?? "\xC0" );
167
168 if ( $b4 < 0x80 || $b4 > 0xBF ) {
169 goto invalid_utf8;
170 }
171
172 if (
173 ( 0xF0 === $b1 && $b2 >= 0x90 && $b2 <= 0xBF ) ||
174 ( $b1 >= 0xF1 && $b1 <= 0xF3 && $b2 >= 0x80 && $b2 <= 0xBF ) ||
175 ( 0xF4 === $b1 && $b2 >= 0x80 && $b2 <= 0x8F )
176 ) {
177 ++$count;
178 $i += 3;
179
180 // Covers U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF, …, U+10FFFE, U+10FFFF.
181 $has_noncharacters |= (
182 ( 0x0F === ( $b2 & 0x0F ) ) &&
183 0xBF === $b3 &&
184 ( 0xBE === $b4 || 0xBF === $b4 )
185 );
186
187 continue;
188 }
189
190 /**
191 * When encountering invalid byte sequences, Unicode suggests finding the
192 * maximal subpart of a text and replacing that subpart with a single
193 * replacement character.
194 *
195 * > This practice is more secure because it does not result in the
196 * > conversion consuming parts of valid sequences as though they were
197 * > invalid. It also guarantees at least one replacement character will
198 * > occur for each instance of an invalid sequence in the original text.
199 * > Furthermore, this practice can be defined consistently for better
200 * > interoperability between different implementations of conversion.
201 *
202 * @see https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-5/#G40630
203 */
204 invalid_utf8:
205 $at = $i;
206 $invalid_length = 1;
207
208 // Single-byte and two-byte characters.
209 if ( ( 0x00 === ( $b1 & 0x80 ) ) || ( 0xC0 === ( $b1 & 0xE0 ) ) ) {
210 return $count;
211 }
212
213 $b2 = ord( $bytes[ $i + 1 ] ?? "\xC0" );
214 $b3 = ord( $bytes[ $i + 2 ] ?? "\xC0" );
215
216 // Find the maximal subpart and skip past it.
217 if ( 0xE0 === ( $b1 & 0xF0 ) ) {
218 // Three-byte characters.
219 $b2_valid = (
220 ( 0xE0 === $b1 && $b2 >= 0xA0 && $b2 <= 0xBF ) ||
221 ( $b1 >= 0xE1 && $b1 <= 0xEC && $b2 >= 0x80 && $b2 <= 0xBF ) ||
222 ( 0xED === $b1 && $b2 >= 0x80 && $b2 <= 0x9F ) ||
223 ( $b1 >= 0xEE && $b1 <= 0xEF && $b2 >= 0x80 && $b2 <= 0xBF )
224 );
225
226 $invalid_length = min( $end - $i, $b2_valid ? 2 : 1 );
227 return $count;
228 } elseif ( 0xF0 === ( $b1 & 0xF8 ) ) {
229 // Four-byte characters.
230 $b2_valid = (
231 ( 0xF0 === $b1 && $b2 >= 0x90 && $b2 <= 0xBF ) ||
232 ( $b1 >= 0xF1 && $b1 <= 0xF3 && $b2 >= 0x80 && $b2 <= 0xBF ) ||
233 ( 0xF4 === $b1 && $b2 >= 0x80 && $b2 <= 0x8F )
234 );
235
236 $b3_valid = $b3 >= 0x80 && $b3 <= 0xBF;
237
238 $invalid_length = min( $end - $i, $b2_valid ? ( $b3_valid ? 3 : 2 ) : 1 );
239 return $count;
240 }
241
242 return $count;
243 }
244
245 $at = $i;
246 return $count;
247 }
248
249 /**
250 * Fallback mechanism for safely validating UTF-8 bytes.
251 *
252 * @since 6.9.0
253 * @access private
254 *
255 * @see wp_is_valid_utf8()
256 *
257 * @param string $bytes String which might contain text encoded as UTF-8.
258 * @return bool Whether the provided bytes can decode as valid UTF-8.
259 */
260 function _wp_is_valid_utf8_fallback( string $bytes ): bool {
261 $bytes_length = strlen( $bytes );
262 if ( 0 === $bytes_length ) {
263 return true;
264 }
265
266 $next_byte_at = 0;
267 $invalid_length = 0;
268
269 _wp_scan_utf8( $bytes, $next_byte_at, $invalid_length );
270
271 return $bytes_length === $next_byte_at && 0 === $invalid_length;
272 }
273
274 /**
275 * Fallback mechanism for replacing invalid spans of UTF-8 bytes.
276 *
277 * Example:
278 *
279 * 'Pi�a' === _wp_scrub_utf8_fallback( "Pi\xF1a" ); // “ñ” is 0xF1 in Windows-1252.
280 *
281 * @since 6.9.0
282 * @access private
283 *
284 * @see wp_scrub_utf8()
285 *
286 * @param string $bytes UTF-8 encoded string which might contain spans of invalid bytes.
287 * @return string Input string with spans of invalid bytes swapped with the replacement character.
288 */
289 function _wp_scrub_utf8_fallback( string $bytes ): string {
290 $bytes_length = strlen( $bytes );
291 $next_byte_at = 0;
292 $was_at = 0;
293 $invalid_length = 0;
294 $scrubbed = '';
295
296 while ( $next_byte_at <= $bytes_length ) {
297 _wp_scan_utf8( $bytes, $next_byte_at, $invalid_length );
298
299 if ( $next_byte_at >= $bytes_length ) {
300 if ( 0 === $was_at ) {
301 return $bytes;
302 }
303
304 return $scrubbed . substr( $bytes, $was_at, $next_byte_at - $was_at - $invalid_length );
305 }
306
307 $scrubbed .= substr( $bytes, $was_at, $next_byte_at - $was_at );
308 $scrubbed .= "\u{FFFD}";
309
310 $next_byte_at += $invalid_length;
311 $was_at = $next_byte_at;
312 }
313
314 return $scrubbed;
315 }
316
317 /**
318 * Returns how many code points are found in the given UTF-8 string.
319 *
320 * Invalid spans of bytes count as a single code point according
321 * to the maximal subpart rule. This function is a fallback method
322 * for calling `mb_strlen( $text, 'UTF-8' )`.
323 *
324 * When negative values are provided for the byte offsets or length,
325 * this will always report zero code points.
326 *
327 * Example:
328 *
329 * 4 === _wp_utf8_codepoint_count( 'text' );
330 *
331 * // Groups are 'test', "\x90" as '�', 'wp', "\xE2\x80" as '�', "\xC0" as '�', and 'test'.
332 * 13 === _wp_utf8_codepoint_count( "test\x90wp\xE2\x80\xC0test" );
333 *
334 * @since 6.9.0
335 * @access private
336 *
337 * @param string $text Count code points in this string.
338 * @param ?int $byte_offset Start counting after this many bytes in `$text`. Must be positive.
339 * @param ?int $max_byte_length Optional. Stop counting after having scanned past this many bytes.
340 * Default is to scan until the end of the string. Must be positive.
341 * @return int How many code points were found.
342 */
343 function _wp_utf8_codepoint_count( string $text, ?int $byte_offset = 0, ?int $max_byte_length = PHP_INT_MAX ): int {
344 if ( $byte_offset < 0 ) {
345 return 0;
346 }
347
348 $count = 0;
349 $at = $byte_offset;
350 $end = strlen( $text );
351 $invalid_length = 0;
352 $max_byte_length = min( $end - $at, $max_byte_length );
353
354 while ( $at < $end && ( $at - $byte_offset ) < $max_byte_length ) {
355 $count += _wp_scan_utf8( $text, $at, $invalid_length, $max_byte_length - ( $at - $byte_offset ) );
356 $count += $invalid_length > 0 ? 1 : 0;
357 $at += $invalid_length;
358 }
359
360 return $count;
361 }
362
363 /**
364 * Given a starting offset within a string and a maximum number of code points,
365 * return how many bytes are occupied by the span of characters.
366 *
367 * Invalid spans of bytes count as a single code point according to the maximal
368 * subpart rule. This function is a fallback method for calling
369 * `strlen( mb_substr( substr( $text, $at ), 0, $max_code_points ) )`.
370 *
371 * @since 6.9.0
372 * @access private
373 *
374 * @param string $text Count bytes of span in this text.
375 * @param int $byte_offset Start counting at this byte offset.
376 * @param int $max_code_points Stop counting after this many code points have been seen,
377 * or at the end of the string.
378 * @param ?int $found_code_points Optional. Will be set to number of found code points in
379 * span, as this might be smaller than the maximum count if
380 * the string is not long enough.
381 * @return int Number of bytes spanned by the code points.
382 */
383 function _wp_utf8_codepoint_span( string $text, int $byte_offset, int $max_code_points, ?int &$found_code_points = 0 ): int {
384 $was_at = $byte_offset;
385 $invalid_length = 0;
386 $end = strlen( $text );
387 $found_code_points = 0;
388
389 while ( $byte_offset < $end && $found_code_points < $max_code_points ) {
390 $needed = $max_code_points - $found_code_points;
391 $chunk_count = _wp_scan_utf8( $text, $byte_offset, $invalid_length, null, $needed );
392
393 $found_code_points += $chunk_count;
394
395 // Invalid spans only convey one code point count regardless of how long they are.
396 if ( 0 !== $invalid_length && $found_code_points < $max_code_points ) {
397 ++$found_code_points;
398 $byte_offset += $invalid_length;
399 }
400 }
401
402 return $byte_offset - $was_at;
403 }
404
405 /**
406 * Fallback support for determining if a string contains Unicode noncharacters.
407 *
408 * @since 6.9.0
409 * @access private
410 *
411 * @see \wp_has_noncharacters()
412 *
413 * @param string $text Are there noncharacters in this string?
414 * @return bool Whether noncharacters were found in the string.
415 */
416 function _wp_has_noncharacters_fallback( string $text ): bool {
417 $at = 0;
418 $invalid_length = 0;
419 $has_noncharacters = false;
420 $end = strlen( $text );
421
422 while ( $at < $end && ! $has_noncharacters ) {
423 _wp_scan_utf8( $text, $at, $invalid_length, null, null, $has_noncharacters );
424 $at += $invalid_length;
425 }
426
427 return $has_noncharacters;
428 }
429
430 /**
431 * Converts a string from ISO-8859-1 to UTF-8, maintaining backwards compatibility
432 * with the deprecated function from the PHP standard library.
433 *
434 * @since 6.9.0
435 * @access private
436 *
437 * @see \utf8_encode()
438 *
439 * @param string $iso_8859_1_text Text treated as ISO-8859-1 (latin1) bytes.
440 * @return string Text converted into UTF-8.
441 */
442 function _wp_utf8_encode_fallback( $iso_8859_1_text ) {
443 $iso_8859_1_text = (string) $iso_8859_1_text;
444 $at = 0;
445 $was_at = 0;
446 $end = strlen( $iso_8859_1_text );
447 $utf8 = '';
448
449 while ( $at < $end ) {
450 // US-ASCII bytes are identical in ISO-8859-1 and UTF-8. These are 0x00–0x7F.
451 $ascii_byte_count = strspn(
452 $iso_8859_1_text,
453 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" .
454 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" .
455 " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f",
456 $at
457 );
458
459 if ( $ascii_byte_count > 0 ) {
460 $at += $ascii_byte_count;
461 continue;
462 }
463
464 // All other bytes transform into two-byte UTF-8 sequences.
465 $code_point = ord( $iso_8859_1_text[ $at ] );
466 $byte1 = chr( 0xC0 | ( $code_point >> 6 ) );
467 $byte2 = chr( 0x80 | ( $code_point & 0x3F ) );
468
469 $utf8 .= substr( $iso_8859_1_text, $was_at, $at - $was_at );
470 $utf8 .= "{$byte1}{$byte2}";
471
472 ++$at;
473 $was_at = $at;
474 }
475
476 if ( 0 === $was_at ) {
477 return $iso_8859_1_text;
478 }
479
480 $utf8 .= substr( $iso_8859_1_text, $was_at );
481 return $utf8;
482 }
483
484 /**
485 * Converts a string from UTF-8 to ISO-8859-1, maintaining backwards compatibility
486 * with the deprecated function from the PHP standard library.
487 *
488 * @since 6.9.0
489 * @access private
490 *
491 * @see \utf8_decode()
492 *
493 * @param string $utf8_text Text treated as UTF-8 bytes.
494 * @return string Text converted into ISO-8859-1.
495 */
496 function _wp_utf8_decode_fallback( $utf8_text ) {
497 $utf8_text = (string) $utf8_text;
498 $at = 0;
499 $was_at = 0;
500 $end = strlen( $utf8_text );
501 $iso_8859_1_text = '';
502
503 while ( $at < $end ) {
504 // US-ASCII bytes are identical in ISO-8859-1 and UTF-8. These are 0x00–0x7F.
505 $ascii_byte_count = strspn(
506 $utf8_text,
507 "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" .
508 "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" .
509 " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f",
510 $at
511 );
512
513 if ( $ascii_byte_count > 0 ) {
514 $at += $ascii_byte_count;
515 continue;
516 }
517
518 $next_at = $at;
519 $invalid_length = 0;
520 $found = _wp_scan_utf8( $utf8_text, $next_at, $invalid_length, null, 1 );
521 $span_length = $next_at - $at;
522 $next_byte = '?';
523
524 if ( 1 !== $found ) {
525 if ( $invalid_length > 0 ) {
526 $next_byte = '';
527 goto flush_sub_part;
528 }
529
530 break;
531 }
532
533 // All convertible code points are two-bytes long.
534 $byte1 = ord( $utf8_text[ $at ] );
535 if ( 0xC0 !== ( $byte1 & 0xE0 ) ) {
536 goto flush_sub_part;
537 }
538
539 // All convertible code points are not greater than U+FF.
540 $byte2 = ord( $utf8_text[ $at + 1 ] );
541 $code_point = ( ( $byte1 & 0x1F ) << 6 ) | ( ( $byte2 & 0x3F ) );
542 if ( $code_point > 0xFF ) {
543 goto flush_sub_part;
544 }
545
546 $next_byte = chr( $code_point );
547
548 flush_sub_part:
549 $iso_8859_1_text .= substr( $utf8_text, $was_at, $at - $was_at );
550 $iso_8859_1_text .= $next_byte;
551 $at += $span_length;
552 $was_at = $at;
553
554 if ( $invalid_length > 0 ) {
555 $iso_8859_1_text .= '?';
556 $at += $invalid_length;
557 $was_at = $at;
558 }
559 }
560
561 if ( 0 === $was_at ) {
562 return $utf8_text;
563 }
564
565 $iso_8859_1_text .= substr( $utf8_text, $was_at );
566 return $iso_8859_1_text;
567 }
568