Further Drupal 8.6.4 changes. Some core files were not committed before a commit...
[yaffs-website] / web / core / lib / Drupal / Component / Utility / Unicode.php
1 <?php
2
3 namespace Drupal\Component\Utility;
4
5 /**
6  * Provides Unicode-related conversions and operations.
7  *
8  * @ingroup utility
9  */
10 class Unicode {
11
12   /**
13    * Matches Unicode characters that are word boundaries.
14    *
15    * Characters with the following General_category (gc) property values are used
16    * as word boundaries. While this does not fully conform to the Word Boundaries
17    * algorithm described in http://unicode.org/reports/tr29, as PCRE does not
18    * contain the Word_Break property table, this simpler algorithm has to do.
19    * - Cc, Cf, Cn, Co, Cs: Other.
20    * - Pc, Pd, Pe, Pf, Pi, Po, Ps: Punctuation.
21    * - Sc, Sk, Sm, So: Symbols.
22    * - Zl, Zp, Zs: Separators.
23    *
24    * Non-boundary characters include the following General_category (gc) property
25    * values:
26    * - Ll, Lm, Lo, Lt, Lu: Letters.
27    * - Mc, Me, Mn: Combining Marks.
28    * - Nd, Nl, No: Numbers.
29    *
30    * Note that the PCRE property matcher is not used because we wanted to be
31    * compatible with Unicode 5.2.0 regardless of the PCRE version used (and any
32    * bugs in PCRE property tables).
33    *
34    * @see http://unicode.org/glossary
35    */
36   const PREG_CLASS_WORD_BOUNDARY = <<<'EOD'
37 \x{0}-\x{2F}\x{3A}-\x{40}\x{5B}-\x{60}\x{7B}-\x{A9}\x{AB}-\x{B1}\x{B4}
38 \x{B6}-\x{B8}\x{BB}\x{BF}\x{D7}\x{F7}\x{2C2}-\x{2C5}\x{2D2}-\x{2DF}
39 \x{2E5}-\x{2EB}\x{2ED}\x{2EF}-\x{2FF}\x{375}\x{37E}-\x{385}\x{387}\x{3F6}
40 \x{482}\x{55A}-\x{55F}\x{589}-\x{58A}\x{5BE}\x{5C0}\x{5C3}\x{5C6}
41 \x{5F3}-\x{60F}\x{61B}-\x{61F}\x{66A}-\x{66D}\x{6D4}\x{6DD}\x{6E9}
42 \x{6FD}-\x{6FE}\x{700}-\x{70F}\x{7F6}-\x{7F9}\x{830}-\x{83E}
43 \x{964}-\x{965}\x{970}\x{9F2}-\x{9F3}\x{9FA}-\x{9FB}\x{AF1}\x{B70}
44 \x{BF3}-\x{BFA}\x{C7F}\x{CF1}-\x{CF2}\x{D79}\x{DF4}\x{E3F}\x{E4F}
45 \x{E5A}-\x{E5B}\x{F01}-\x{F17}\x{F1A}-\x{F1F}\x{F34}\x{F36}\x{F38}
46 \x{F3A}-\x{F3D}\x{F85}\x{FBE}-\x{FC5}\x{FC7}-\x{FD8}\x{104A}-\x{104F}
47 \x{109E}-\x{109F}\x{10FB}\x{1360}-\x{1368}\x{1390}-\x{1399}\x{1400}
48 \x{166D}-\x{166E}\x{1680}\x{169B}-\x{169C}\x{16EB}-\x{16ED}
49 \x{1735}-\x{1736}\x{17B4}-\x{17B5}\x{17D4}-\x{17D6}\x{17D8}-\x{17DB}
50 \x{1800}-\x{180A}\x{180E}\x{1940}-\x{1945}\x{19DE}-\x{19FF}
51 \x{1A1E}-\x{1A1F}\x{1AA0}-\x{1AA6}\x{1AA8}-\x{1AAD}\x{1B5A}-\x{1B6A}
52 \x{1B74}-\x{1B7C}\x{1C3B}-\x{1C3F}\x{1C7E}-\x{1C7F}\x{1CD3}\x{1FBD}
53 \x{1FBF}-\x{1FC1}\x{1FCD}-\x{1FCF}\x{1FDD}-\x{1FDF}\x{1FED}-\x{1FEF}
54 \x{1FFD}-\x{206F}\x{207A}-\x{207E}\x{208A}-\x{208E}\x{20A0}-\x{20B8}
55 \x{2100}-\x{2101}\x{2103}-\x{2106}\x{2108}-\x{2109}\x{2114}
56 \x{2116}-\x{2118}\x{211E}-\x{2123}\x{2125}\x{2127}\x{2129}\x{212E}
57 \x{213A}-\x{213B}\x{2140}-\x{2144}\x{214A}-\x{214D}\x{214F}
58 \x{2190}-\x{244A}\x{249C}-\x{24E9}\x{2500}-\x{2775}\x{2794}-\x{2B59}
59 \x{2CE5}-\x{2CEA}\x{2CF9}-\x{2CFC}\x{2CFE}-\x{2CFF}\x{2E00}-\x{2E2E}
60 \x{2E30}-\x{3004}\x{3008}-\x{3020}\x{3030}\x{3036}-\x{3037}
61 \x{303D}-\x{303F}\x{309B}-\x{309C}\x{30A0}\x{30FB}\x{3190}-\x{3191}
62 \x{3196}-\x{319F}\x{31C0}-\x{31E3}\x{3200}-\x{321E}\x{322A}-\x{3250}
63 \x{3260}-\x{327F}\x{328A}-\x{32B0}\x{32C0}-\x{33FF}\x{4DC0}-\x{4DFF}
64 \x{A490}-\x{A4C6}\x{A4FE}-\x{A4FF}\x{A60D}-\x{A60F}\x{A673}\x{A67E}
65 \x{A6F2}-\x{A716}\x{A720}-\x{A721}\x{A789}-\x{A78A}\x{A828}-\x{A82B}
66 \x{A836}-\x{A839}\x{A874}-\x{A877}\x{A8CE}-\x{A8CF}\x{A8F8}-\x{A8FA}
67 \x{A92E}-\x{A92F}\x{A95F}\x{A9C1}-\x{A9CD}\x{A9DE}-\x{A9DF}
68 \x{AA5C}-\x{AA5F}\x{AA77}-\x{AA79}\x{AADE}-\x{AADF}\x{ABEB}
69 \x{E000}-\x{F8FF}\x{FB29}\x{FD3E}-\x{FD3F}\x{FDFC}-\x{FDFD}
70 \x{FE10}-\x{FE19}\x{FE30}-\x{FE6B}\x{FEFF}-\x{FF0F}\x{FF1A}-\x{FF20}
71 \x{FF3B}-\x{FF40}\x{FF5B}-\x{FF65}\x{FFE0}-\x{FFFD}
72 EOD;
73
74   /**
75    * Indicates that standard PHP (emulated) unicode support is being used.
76    */
77   const STATUS_SINGLEBYTE = 0;
78
79   /**
80    * Indicates that full unicode support with the PHP mbstring extension is
81    * being used.
82    */
83   const STATUS_MULTIBYTE = 1;
84
85   /**
86    * Indicates an error during check for PHP unicode support.
87    */
88   const STATUS_ERROR = -1;
89
90   /**
91    * Gets the current status of unicode/multibyte support on this environment.
92    *
93    * @return int
94    *   The status of multibyte support. It can be one of:
95    *   - \Drupal\Component\Utility\Unicode::STATUS_MULTIBYTE
96    *     Full unicode support using an extension.
97    *   - \Drupal\Component\Utility\Unicode::STATUS_SINGLEBYTE
98    *     Standard PHP (emulated) unicode support.
99    *   - \Drupal\Component\Utility\Unicode::STATUS_ERROR
100    *     An error occurred. No unicode support.
101    */
102   public static function getStatus() {
103     switch (static::check()) {
104       case 'mb_strlen':
105         return Unicode::STATUS_SINGLEBYTE;
106       case '':
107         return Unicode::STATUS_MULTIBYTE;
108     }
109     return Unicode::STATUS_ERROR;
110   }
111
112   /**
113    * Sets the value for multibyte support status for the current environment.
114    *
115    * The following status keys are supported:
116    *   - \Drupal\Component\Utility\Unicode::STATUS_MULTIBYTE
117    *     Full unicode support using an extension.
118    *   - \Drupal\Component\Utility\Unicode::STATUS_SINGLEBYTE
119    *     Standard PHP (emulated) unicode support.
120    *   - \Drupal\Component\Utility\Unicode::STATUS_ERROR
121    *     An error occurred. No unicode support.
122    *
123    * @param int $status
124    *   The new status of multibyte support.
125    *
126    * @deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In
127    *   Drupal 9 there will be no way to set the status and in Drupal 8 this
128    *   ability has been removed because mb_*() functions are supplied using
129    *   Symfony's polyfill.
130    *
131    * @see https://www.drupal.org/node/2850048
132    */
133   public static function setStatus($status) {
134     @trigger_error('\Drupal\Component\Utility\Unicode::setStatus() is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. In Drupal 9 there will be no way to set the status and in Drupal 8 this ability has been removed because mb_*() functions are supplied using Symfony\'s polyfill. See https://www.drupal.org/node/2850048.', E_USER_DEPRECATED);
135   }
136
137   /**
138    * Checks for Unicode support in PHP and sets the proper settings if possible.
139    *
140    * Because of the need to be able to handle text in various encodings, we do
141    * not support mbstring function overloading. HTTP input/output conversion
142    * must be disabled for similar reasons.
143    *
144    * @return string
145    *   A string identifier of a failed multibyte extension check, if any.
146    *   Otherwise, an empty string.
147    */
148   public static function check() {
149     // Set appropriate configuration.
150     mb_internal_encoding('utf-8');
151     mb_language('uni');
152
153     // Check for mbstring extension.
154     if (!extension_loaded('mbstring')) {
155       return 'mb_strlen';
156     }
157
158     // Check mbstring configuration.
159     if (ini_get('mbstring.func_overload') != 0) {
160       return 'mbstring.func_overload';
161     }
162     if (ini_get('mbstring.encoding_translation') != 0) {
163       return 'mbstring.encoding_translation';
164     }
165     // mbstring.http_input and mbstring.http_output are deprecated and empty by
166     // default in PHP 5.6.
167     if (version_compare(PHP_VERSION, '5.6.0') == -1) {
168       if (ini_get('mbstring.http_input') != 'pass') {
169         return 'mbstring.http_input';
170       }
171       if (ini_get('mbstring.http_output') != 'pass') {
172         return 'mbstring.http_output';
173       }
174     }
175
176     return '';
177   }
178
179   /**
180    * Decodes UTF byte-order mark (BOM) into the encoding's name.
181    *
182    * @param string $data
183    *   The data possibly containing a BOM. This can be the entire contents of
184    *   a file, or just a fragment containing at least the first five bytes.
185    *
186    * @return string|bool
187    *   The name of the encoding, or FALSE if no byte order mark was present.
188    */
189   public static function encodingFromBOM($data) {
190     static $bomMap = [
191       "\xEF\xBB\xBF" => 'UTF-8',
192       "\xFE\xFF" => 'UTF-16BE',
193       "\xFF\xFE" => 'UTF-16LE',
194       "\x00\x00\xFE\xFF" => 'UTF-32BE',
195       "\xFF\xFE\x00\x00" => 'UTF-32LE',
196       "\x2B\x2F\x76\x38" => 'UTF-7',
197       "\x2B\x2F\x76\x39" => 'UTF-7',
198       "\x2B\x2F\x76\x2B" => 'UTF-7',
199       "\x2B\x2F\x76\x2F" => 'UTF-7',
200       "\x2B\x2F\x76\x38\x2D" => 'UTF-7',
201     ];
202
203     foreach ($bomMap as $bom => $encoding) {
204       if (strpos($data, $bom) === 0) {
205         return $encoding;
206       }
207     }
208     return FALSE;
209   }
210
211   /**
212    * Converts data to UTF-8.
213    *
214    * Requires the iconv, GNU recode or mbstring PHP extension.
215    *
216    * @param string $data
217    *   The data to be converted.
218    * @param string $encoding
219    *   The encoding that the data is in.
220    *
221    * @return string|bool
222    *   Converted data or FALSE.
223    */
224   public static function convertToUtf8($data, $encoding) {
225     return @iconv($encoding, 'utf-8', $data);
226   }
227
228   /**
229    * Truncates a UTF-8-encoded string safely to a number of bytes.
230    *
231    * If the end position is in the middle of a UTF-8 sequence, it scans backwards
232    * until the beginning of the byte sequence.
233    *
234    * Use this function whenever you want to chop off a string at an unsure
235    * location. On the other hand, if you're sure that you're splitting on a
236    * character boundary (e.g. after using strpos() or similar), you can safely
237    * use substr() instead.
238    *
239    * @param string $string
240    *   The string to truncate.
241    * @param int $len
242    *   An upper limit on the returned string length.
243    *
244    * @return string
245    *   The truncated string.
246    */
247   public static function truncateBytes($string, $len) {
248     if (strlen($string) <= $len) {
249       return $string;
250     }
251     if ((ord($string[$len]) < 0x80) || (ord($string[$len]) >= 0xC0)) {
252       return substr($string, 0, $len);
253     }
254     // Scan backwards to beginning of the byte sequence.
255     // @todo Make the code more readable in https://www.drupal.org/node/2911497.
256     while (--$len >= 0 && ord($string[$len]) >= 0x80 && ord($string[$len]) < 0xC0) {
257     }
258
259     return substr($string, 0, $len);
260   }
261
262   /**
263    * Counts the number of characters in a UTF-8 string.
264    *
265    * This is less than or equal to the byte count.
266    *
267    * @param string $text
268    *   The string to run the operation on.
269    *
270    * @return int
271    *   The length of the string.
272    *
273    * @deprecated in Drupal 8.6.0, will be removed before Drupal 9.0.0. Use
274    *   mb_strlen() instead.
275    *
276    * @see https://www.drupal.org/node/2850048
277    */
278   public static function strlen($text) {
279     @trigger_error('\Drupal\Component\Utility\Unicode::strlen() is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Use mb_strlen() instead. See https://www.drupal.org/node/2850048.', E_USER_DEPRECATED);
280     return mb_strlen($text);
281   }
282
283   /**
284    * Converts a UTF-8 string to uppercase.
285    *
286    * @param string $text
287    *   The string to run the operation on.
288    *
289    * @return string
290    *   The string in uppercase.
291    *
292    * @deprecated in Drupal 8.6.0, will be removed before Drupal 9.0.0. Use
293    *   mb_strtoupper() instead.
294    *
295    * @see https://www.drupal.org/node/2850048
296    */
297   public static function strtoupper($text) {
298     @trigger_error('\Drupal\Component\Utility\Unicode::strtoupper() is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Use mb_strtoupper() instead. See https://www.drupal.org/node/2850048.', E_USER_DEPRECATED);
299     return mb_strtoupper($text);
300   }
301
302   /**
303    * Converts a UTF-8 string to lowercase.
304    *
305    * @param string $text
306    *   The string to run the operation on.
307    *
308    * @return string
309    *   The string in lowercase.
310    *
311    * @deprecated in Drupal 8.6.0, will be removed before Drupal 9.0.0. Use
312    *   mb_strtolower() instead.
313    *
314    * @see https://www.drupal.org/node/2850048
315    */
316   public static function strtolower($text) {
317     @trigger_error('\Drupal\Component\Utility\Unicode::strtolower() is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Use mb_strtolower() instead. See https://www.drupal.org/node/2850048.', E_USER_DEPRECATED);
318     return mb_strtolower($text);
319   }
320
321   /**
322    * Capitalizes the first character of a UTF-8 string.
323    *
324    * @param string $text
325    *   The string to convert.
326    *
327    * @return string
328    *   The string with the first character as uppercase.
329    */
330   public static function ucfirst($text) {
331     return mb_strtoupper(mb_substr($text, 0, 1)) . mb_substr($text, 1);
332   }
333
334   /**
335    * Converts the first character of a UTF-8 string to lowercase.
336    *
337    * @param string $text
338    *   The string that will be converted.
339    *
340    * @return string
341    *   The string with the first character as lowercase.
342    *
343    * @ingroup php_wrappers
344    */
345   public static function lcfirst($text) {
346     // Note: no mbstring equivalent!
347     return mb_strtolower(mb_substr($text, 0, 1)) . mb_substr($text, 1);
348   }
349
350   /**
351    * Capitalizes the first character of each word in a UTF-8 string.
352    *
353    * @param string $text
354    *   The text that will be converted.
355    *
356    * @return string
357    *   The input $text with each word capitalized.
358    *
359    * @ingroup php_wrappers
360    */
361   public static function ucwords($text) {
362     $regex = '/(^|[' . static::PREG_CLASS_WORD_BOUNDARY . '])([^' . static::PREG_CLASS_WORD_BOUNDARY . '])/u';
363     return preg_replace_callback($regex, function (array $matches) {
364       return $matches[1] . mb_strtoupper($matches[2]);
365     }, $text);
366   }
367
368   /**
369    * Cuts off a piece of a string based on character indices and counts.
370    *
371    * Follows the same behavior as PHP's own substr() function. Note that for
372    * cutting off a string at a known character/substring location, the usage of
373    * PHP's normal strpos/substr is safe and much faster.
374    *
375    * @param string $text
376    *   The input string.
377    * @param int $start
378    *   The position at which to start reading.
379    * @param int $length
380    *   The number of characters to read.
381    *
382    * @return string
383    *   The shortened string.
384    *
385    * @deprecated in Drupal 8.6.0, will be removed before Drupal 9.0.0. Use
386    *   mb_substr() instead.
387    *
388    * @see https://www.drupal.org/node/2850048
389    */
390   public static function substr($text, $start, $length = NULL) {
391     @trigger_error('\Drupal\Component\Utility\Unicode::substr() is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Use mb_substr() instead. See https://www.drupal.org/node/2850048.', E_USER_DEPRECATED);
392     return mb_substr($text, $start, $length);
393   }
394
395   /**
396    * Truncates a UTF-8-encoded string safely to a number of characters.
397    *
398    * @param string $string
399    *   The string to truncate.
400    * @param int $max_length
401    *   An upper limit on the returned string length, including trailing ellipsis
402    *   if $add_ellipsis is TRUE.
403    * @param bool $wordsafe
404    *   If TRUE, attempt to truncate on a word boundary. Word boundaries are
405    *   spaces, punctuation, and Unicode characters used as word boundaries in
406    *   non-Latin languages; see Unicode::PREG_CLASS_WORD_BOUNDARY for more
407    *   information. If a word boundary cannot be found that would make the length
408    *   of the returned string fall within length guidelines (see parameters
409    *   $max_length and $min_wordsafe_length), word boundaries are ignored.
410    * @param bool $add_ellipsis
411    *   If TRUE, add '...' to the end of the truncated string (defaults to
412    *   FALSE). The string length will still fall within $max_length.
413    * @param int $min_wordsafe_length
414    *   If $wordsafe is TRUE, the minimum acceptable length for truncation (before
415    *   adding an ellipsis, if $add_ellipsis is TRUE). Has no effect if $wordsafe
416    *   is FALSE. This can be used to prevent having a very short resulting string
417    *   that will not be understandable. For instance, if you are truncating the
418    *   string "See myverylongurlexample.com for more information" to a word-safe
419    *   return length of 20, the only available word boundary within 20 characters
420    *   is after the word "See", which wouldn't leave a very informative string. If
421    *   you had set $min_wordsafe_length to 10, though, the function would realise
422    *   that "See" alone is too short, and would then just truncate ignoring word
423    *   boundaries, giving you "See myverylongurl..." (assuming you had set
424    *   $add_ellipses to TRUE).
425    *
426    * @return string
427    *   The truncated string.
428    */
429   public static function truncate($string, $max_length, $wordsafe = FALSE, $add_ellipsis = FALSE, $min_wordsafe_length = 1) {
430     $ellipsis = '';
431     $max_length = max($max_length, 0);
432     $min_wordsafe_length = max($min_wordsafe_length, 0);
433
434     if (mb_strlen($string) <= $max_length) {
435       // No truncation needed, so don't add ellipsis, just return.
436       return $string;
437     }
438
439     if ($add_ellipsis) {
440       // Truncate ellipsis in case $max_length is small.
441       $ellipsis = mb_substr('…', 0, $max_length);
442       $max_length -= mb_strlen($ellipsis);
443       $max_length = max($max_length, 0);
444     }
445
446     if ($max_length <= $min_wordsafe_length) {
447       // Do not attempt word-safe if lengths are bad.
448       $wordsafe = FALSE;
449     }
450
451     if ($wordsafe) {
452       $matches = [];
453       // Find the last word boundary, if there is one within $min_wordsafe_length
454       // to $max_length characters. preg_match() is always greedy, so it will
455       // find the longest string possible.
456       $found = preg_match('/^(.{' . $min_wordsafe_length . ',' . $max_length . '})[' . Unicode::PREG_CLASS_WORD_BOUNDARY . ']/us', $string, $matches);
457       if ($found) {
458         $string = $matches[1];
459       }
460       else {
461         $string = mb_substr($string, 0, $max_length);
462       }
463     }
464     else {
465       $string = mb_substr($string, 0, $max_length);
466     }
467
468     if ($add_ellipsis) {
469       // If we're adding an ellipsis, remove any trailing periods.
470       $string = rtrim($string, '.');
471
472       $string .= $ellipsis;
473     }
474
475     return $string;
476   }
477
478   /**
479    * Compares UTF-8-encoded strings in a binary safe case-insensitive manner.
480    *
481    * @param string $str1
482    *   The first string.
483    * @param string $str2
484    *   The second string.
485    *
486    * @return int
487    *   Returns < 0 if $str1 is less than $str2; > 0 if $str1 is greater than
488    *   $str2, and 0 if they are equal.
489    */
490   public static function strcasecmp($str1, $str2) {
491     return strcmp(mb_strtoupper($str1), mb_strtoupper($str2));
492   }
493
494   /**
495    * Encodes MIME/HTTP headers that contain incorrectly encoded characters.
496    *
497    * For example, Unicode::mimeHeaderEncode('tést.txt') returns
498    * "=?UTF-8?B?dMOpc3QudHh0?=".
499    *
500    * See http://www.rfc-editor.org/rfc/rfc2047.txt for more information.
501    *
502    * Notes:
503    * - Only encode strings that contain non-ASCII characters.
504    * - We progressively cut-off a chunk with self::truncateBytes(). This ensures
505    *   each chunk starts and ends on a character boundary.
506    * - Using \n as the chunk separator may cause problems on some systems and
507    *   may have to be changed to \r\n or \r.
508    *
509    * @param string $string
510    *   The header to encode.
511    * @param bool $shorten
512    *   If TRUE, only return the first chunk of a multi-chunk encoded string.
513    *
514    * @return string
515    *   The mime-encoded header.
516    */
517   public static function mimeHeaderEncode($string, $shorten = FALSE) {
518     if (preg_match('/[^\x20-\x7E]/', $string)) {
519       // floor((75 - strlen("=?UTF-8?B??=")) * 0.75);
520       $chunk_size = 47;
521       $len = strlen($string);
522       $output = '';
523       while ($len > 0) {
524         $chunk = static::truncateBytes($string, $chunk_size);
525         $output .= ' =?UTF-8?B?' . base64_encode($chunk) . "?=\n";
526         if ($shorten) {
527           break;
528         }
529         $c = strlen($chunk);
530         $string = substr($string, $c);
531         $len -= $c;
532       }
533       return trim($output);
534     }
535     return $string;
536   }
537
538   /**
539    * Decodes MIME/HTTP encoded header values.
540    *
541    * @param string $header
542    *   The header to decode.
543    *
544    * @return string
545    *   The mime-decoded header.
546    */
547   public static function mimeHeaderDecode($header) {
548     $callback = function ($matches) {
549       $data = ($matches[2] == 'B') ? base64_decode($matches[3]) : str_replace('_', ' ', quoted_printable_decode($matches[3]));
550       if (strtolower($matches[1]) != 'utf-8') {
551         $data = static::convertToUtf8($data, $matches[1]);
552       }
553       return $data;
554     };
555     // First step: encoded chunks followed by other encoded chunks (need to collapse whitespace)
556     $header = preg_replace_callback('/=\?([^?]+)\?(Q|B)\?([^?]+|\?(?!=))\?=\s+(?==\?)/', $callback, $header);
557     // Second step: remaining chunks (do not collapse whitespace)
558     return preg_replace_callback('/=\?([^?]+)\?(Q|B)\?([^?]+|\?(?!=))\?=/', $callback, $header);
559   }
560
561   /**
562    * Flip U+C0-U+DE to U+E0-U+FD and back. Can be used as preg_replace callback.
563    *
564    * @param array $matches
565    *   An array of matches by preg_replace_callback().
566    *
567    * @return string
568    *   The flipped text.
569    */
570   public static function caseFlip($matches) {
571     return $matches[0][0] . chr(ord($matches[0][1]) ^ 32);
572   }
573
574   /**
575    * Checks whether a string is valid UTF-8.
576    *
577    * All functions designed to filter input should use drupal_validate_utf8
578    * to ensure they operate on valid UTF-8 strings to prevent bypass of the
579    * filter.
580    *
581    * When text containing an invalid UTF-8 lead byte (0xC0 - 0xFF) is presented
582    * as UTF-8 to Internet Explorer 6, the program may misinterpret subsequent
583    * bytes. When these subsequent bytes are HTML control characters such as
584    * quotes or angle brackets, parts of the text that were deemed safe by filters
585    * end up in locations that are potentially unsafe; An onerror attribute that
586    * is outside of a tag, and thus deemed safe by a filter, can be interpreted
587    * by the browser as if it were inside the tag.
588    *
589    * The function does not return FALSE for strings containing character codes
590    * above U+10FFFF, even though these are prohibited by RFC 3629.
591    *
592    * @param string $text
593    *   The text to check.
594    *
595    * @return bool
596    *   TRUE if the text is valid UTF-8, FALSE if not.
597    */
598   public static function validateUtf8($text) {
599     if (strlen($text) == 0) {
600       return TRUE;
601     }
602     // With the PCRE_UTF8 modifier 'u', preg_match() fails silently on strings
603     // containing invalid UTF-8 byte sequences. It does not reject character
604     // codes above U+10FFFF (represented by 4 or more octets), though.
605     return (preg_match('/^./us', $text) == 1);
606   }
607
608   /**
609    * Finds the position of the first occurrence of a string in another string.
610    *
611    * @param string $haystack
612    *   The string to search in.
613    * @param string $needle
614    *   The string to find in $haystack.
615    * @param int $offset
616    *   If specified, start the search at this number of characters from the
617    *   beginning (default 0).
618    *
619    * @return int|false
620    *   The position where $needle occurs in $haystack, always relative to the
621    *   beginning (independent of $offset), or FALSE if not found. Note that
622    *   a return value of 0 is not the same as FALSE.
623    *
624    * @deprecated in Drupal 8.6.0, will be removed before Drupal 9.0.0. Use
625    *   mb_strpos() instead.
626    *
627    * @see https://www.drupal.org/node/2850048
628    */
629   public static function strpos($haystack, $needle, $offset = 0) {
630     @trigger_error('\Drupal\Component\Utility\Unicode::strpos() is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Use mb_strpos() instead. See https://www.drupal.org/node/2850048.', E_USER_DEPRECATED);
631     return mb_strpos($haystack, $needle, $offset);
632   }
633
634 }